From c2dcb27f8cd2c575c2040c617cf9a637d9ddeee5 Mon Sep 17 00:00:00 2001 From: Jean Date: Sun, 3 Aug 2025 09:48:22 -0700 Subject: [PATCH] Initial commit --- flake.lock | 39 +++ flake.nix | 21 ++ library.svg | 82 ++++++ metamorpov.js | 630 ++++++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 54 ++++ package.json | 18 ++ 6 files changed, 844 insertions(+) create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 library.svg create mode 100644 metamorpov.js create mode 100644 package-lock.json create mode 100644 package.json diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..9750de2 --- /dev/null +++ b/flake.lock @@ -0,0 +1,39 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 0, + "narHash": "sha256-463SNPWmz46iLzJKRzO3Q2b0Aurff3U1n0nYItxq7jU=", + "path": "/nix/store/yw6kg4rb9v8s3ypjbpspig5r81m4lr5s-source", + "type": "path" + }, + "original": { + "id": "nixpkgs", + "type": "indirect" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "systems": "systems" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..a5883ef --- /dev/null +++ b/flake.nix @@ -0,0 +1,21 @@ +{ + inputs = { + # nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + systems.url = "github:nix-systems/default"; + }; + + outputs = + { systems, nixpkgs, ... }@inputs: + let + eachSystem = f: nixpkgs.lib.genAttrs (import systems) (system: f nixpkgs.legacyPackages.${system}); + in + { + devShells = eachSystem (pkgs: { + default = pkgs.mkShell { + buildInputs = [ + pkgs.nodejs_22 + ]; + }; + }); + }; +} diff --git a/library.svg b/library.svg new file mode 100644 index 0000000..5532a7d --- /dev/null +++ b/library.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + diff --git a/metamorpov.js b/metamorpov.js new file mode 100644 index 0000000..8df6e7d --- /dev/null +++ b/metamorpov.js @@ -0,0 +1,630 @@ +const verbsHelper = require("english-verbs-helper"); +const verbsIrregular = require("english-verbs-irregular/dist/verbs.json"); +const verbsGerunds = require("english-verbs-gerunds/dist/gerunds.json"); +const verbsData = verbsHelper.mergeVerbsData(verbsIrregular, verbsGerunds); +const articleHelper = require("indefinite"); + +/* Replaces MetamorPOV markers with English according to the options */ +exports.translate = function(input, options) { + let request = { + input: input, + options: options, + } + + if (typeof(input) == "object" && input instanceof Node) { + request.loop = loopNode; + } + else if (typeof(input) == "string") { + request.loop = loopString; + } + else { + console.error("Invalid input type"); + return; + } + + const isNameSet = !(options.name === undefined || options.name == ""); + const isPronounsSet = !(options.preset === undefined || options.preset == ""); + const isPovSet = !(options.pov === undefined || options.pov == ""); + const isPovLegal = isPovSet && (options.pov != "third" || (isNameSet && isPronounsSet)); + + if (isPovLegal) { + vrb(request); + pov(request); + plv(request); + } + + if (isNameSet) { + yn(request); + } + + if (isPronounsSet) { + prn(request); + } + + const isAlsoSet = !(options.also === undefined); + if (isAlsoSet) { + also(request); + } + + const isAllSet = isNameSet && isPronounsSet && isPovLegal; + if (isAllSet) { + alt(request); + ife(request); + cut(request); + cap(request); + mrr(request); + aan(request); + } + + return request.input; +} + +/* Iterator for HTML Node elements */ +function loopNode(request, params, method) { + const node = request.input; + if (node.contentEditable == "true" || node.type == "textarea" || node.type == "input") { return; } + let child, next; + switch (node.nodeType) { + case 1: /* ELEMENT_NODE */ + case 9: /* DOCUMENT_NODE */ + case 11: /* DOCUMENT_FRAGMENT_NODE */ + child = node.firstChild; + while (child) { + next = child.nextSibling; + loopNode(node, params, method); + child = next; + } + break; + case 3: /* TEXT_NODE */ + loopString(node.nodeValue, params, method); + } +} + +/* Iterator for string elements */ +function loopString(request, params, method) { + if (request.input.match(params.regexp) == null) { return; } + request.input = method(request.input, params); + loopString(request, params, method); +} + +/* vrb/be/ becomes "were" in second-person and "was" in third-person POV */ +function vrb(request) { + const regexp = new RegExp([ + /(?v)r/, + /(?[nb])\//, + /(?[\w\s]+)\//, + /(?:(?(?:(?:simple|progressive|perfect|perfect[ _]progressive|participle(?![ _]future))[ _])*(?:past|present|future))\/)*/ + ].map(r => r.source).join(''),'i'); + + request.loop(request, { + regexp: regexp, + options: request.options + }, (input, params) => { + const options = params.options; + const vars = input.match(params.regexp).groups; + let tense = vars.tense; + if (tense == null) { + tense = "PAST"; + } else { + tense = tense.toUpperCase().replaceAll(" ", "_"); + } + + let povIndex; + switch (vars.category) { + case "B": /* B and b vary by POV, capitalized to indicate a named subject */ + if (options.pov == "third") { + povIndex = getPovIndex("third-singular", null); + } else { + povIndex = getPovIndex(options.pov, options); + } + break; + case "b": + povIndex = getPovIndex(options.pov, options); + break; + case "N": /* N and n are always third-person */ + povIndex = getPovIndex("third-singular", null); + break; + case "n": + povIndex = getPovIndex("third", options); + } + + let replacement = verbsHelper.getConjugation(verbsData, vars.verb, tense, povIndex); + const isCapital = /V/.test(vars.firstLetter); + if (isCapital) { + replacement = capitalize(replacement); + } + return input.replace(params.regexp, replacement); + }); +} + +/* pov/s becomes "I" or "you" or "she" by POV */ +function pov(request) { + const options = request.options; + let pronouns, params; + if (request.options.pov == "third") { + pronouns = getPronouns(options.preset, options.other); + params = [ + { + regexp: /([Pp])ov\/S/, + pronoun: options.name + }, { + regexp: /([Pp])ov\/O/, + pronoun: options.name + }, { + regexp: /([Pp])ov\/P/, + pronoun: options.name + "’s" + }, { + regexp: /([Pp])ov\/A/, + pronoun: options.name + "’s" + }, { + regexp: /([Pp])ov\/R/, + pronoun: options.name + "’s self" + } + ]; + } else { + pronouns = getPronouns(options.pov, null); + params = [ + { + regexp: /([Pp])ov\/S/, + pronoun: pronouns["subjective"] + }, { + regexp: /([Pp])ov\/O/, + pronoun: pronouns["objective"] + }, { + regexp: /([Pp])ov\/P/, + pronoun: pronouns["possessive"] + }, { + regexp: /([Pp])ov\/A/, + pronoun: pronouns["adjective"] + }, { + regexp: /([Pp])ov\/R/, + pronoun: pronouns["reflexive"] + } + ]; + } + + params.push( + { + regexp: /([Pp])ov\/s/, + pronoun: pronouns["subjective"] + }, { + regexp: /([Pp])ov\/o/, + pronoun: pronouns["objective"] + }, { + regexp: /([Pp])ov\/p/, + pronoun: pronouns["possessive"] + }, { + regexp: /([Pp])ov\/a/, + pronoun: pronouns["adjective"] + }, { + regexp: /([Pp])ov\/r/, + pronoun: pronouns["reflexive"] + } + ); + + params.forEach((pronoun) => request.loop(request, pronoun, prnHelper)); +} + +/* prn/s becomes "we" or "they" by POV */ +function plv(request) { + const pronouns = getPronouns(request.options.pov + "-plural", null); + const params = [ + { + regexp: /([Pp])lv\/s/, + pronoun: pronouns["subjective"] + }, { + regexp: /([Pp])lv\/o/, + pronoun: pronouns["objective"] + }, { + regexp: /([Pp])lv\/p/, + pronoun: pronouns["possessive"] + }, { + regexp: /([Pp])lv\/a/, + pronoun: pronouns["adjective"] + }, { + regexp: /([Pp])lv\/r/, + pronoun: pronouns["reflexive"] + } + ]; + + params.forEach((pronoun) => request.loop(request, pronoun, prnHelper)); +} + +/* Y/n becomes the name indicated in the options */ +function yn(request) { + request.loop(request, { + regexp: /\by\/n\b|\(y\/n\)|\[y\/n\]/ig, + replacement: request.options["name"] + }, (input, params) => { + return input.replaceAll(params.regexp, params.replacement); + }); +} + +/* prn/s becomes "he" or "she" and are always third-person */ +function prn(request) { + const pronouns = getPronouns(request.options.preset, request.options.other); + const params = [ + { + regexp: /([Pp])rn\/s/, + pronoun: pronouns["subjective"] + }, { + regexp: /([Pp])rn\/o/, + pronoun: pronouns["objective"] + }, { + regexp: /([Pp])rn\/p/, + pronoun: pronouns["possessive"] + }, { + regexp: /([Pp])rn\/a/, + pronoun: pronouns["adjective"] + }, { + regexp: /([Pp])rn\/r/, + pronoun: pronouns["reflexive"] + }, { + regexp: /([Pp])rn\/H/, + pronoun: pronouns["honorific-abbr"] + }, { + regexp: /([Pp])rn\/h/, + pronoun: pronouns["honorific"] + }, { + regexp: /([Pp])rn\/N/, + pronoun: pronouns["adult"] + }, { + regexp: /([Pp])rn\/n/, + pronoun: pronouns["youth"] + }, { + regexp: /([Pp])rn\/F/, + pronoun: pronouns["parent"] + }, { + regexp: /([Pp])rn\/f/, + pronoun: pronouns["child"] + }, { + regexp: /([Pp])rn\/k/, + pronoun: pronouns["sibling"] + }, { + regexp: /([Pp])rn\/m/, + pronoun: pronouns["married"] + }, { + regexp: /([Pp])rn\/d/, + pronoun: pronouns["dating"] + } + ] + + params.forEach((pronoun) => request.loop(request, pronoun, prnHelper)); +} + +/* Replaces specific words indicated in the options */ +function also(request) { + Object.entries(options.also).forEach(([searchTerm, replacement]) => { + let searchTermEscaped = searchTerm.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + + // Remove whitespace from the front and back + if (searchTermEscaped[0].match(/[a-z]/i)) { + searchTermEscaped = `\\b${searchTermEscaped}`; + } + if (searchTermEscaped[searchTermEscaped.length - 1].match((/[a-z]/i))) { + searchTermEscaped = `${searchTermEscaped}\\b`; + } + + const searchTermRegexp = new RegExp(searchTermEscaped); + request.loop(request, { + regexp: searchTermRegexp, + replacement: replacement + }, (input, params) => { + return input.replaceAll(params.regexp, params.replacement); + }); + }); +} + +/* alt/first and second or third/word1/word2/ becomes "word1" in first and second-person POV but "word2" in third-person */ +function alt(request) { + const regexp1 = new RegExp([ + /alt\/(?first|second|third)/, + /\/(?[^\/]*)\// + ].map(r => r.source).join(''),'i'); + request.loop(request, { + regexp: regexp1, + pov: request.options.pov + }, (input, params) => { + const vars = input.match(params.regexp).groups; + if (vars.targetPov == params.pov) { + return input.replace(params.regexp, vars.replacement); + } else { + return input.replace(params.regexp, null); + } + }); + + const regexp2 = new RegExp([ + /alt\/(?first|second|third) /, + /(?and|or) /, + /(?!\1)(?first|second|third)/, + /(?: (?!\2)(?and|or) (?!\1|\3)(?:first|second|third))?/, + /\/(?[^\/]*)\//, + /(?[^\/]*)\// + ].map(r => r.source).join(''),'i'); + request.loop(request, { + regexp: regexp2, + pov: request.options.pov + }, (input, params) => { + const vars = input.match(params.regexp).groups; + if (vars.targetPov1 == params.pov || vars.conjunction1.toLowerCase() == "and" && vars.targetPov2 == params.pov) { + return input.replace(params.regexp, vars.replacement1); + } else if (vars.conjunction1.toLowerCase() == "or" && vars.targetPov2 == params.pov) { + return input.replace(params.regexp, vars.replacement2); + } else if (vars.conjunction2 == null) { + return input.replace(params.regexp, null); + } else { + return input.replace(params.regexp, vars.replacement2); + } + }); + + const regexp3 = new RegExp([ + /alt\/(?[^\/]*)\//, + /(?[^\/]*)\//, + /(?[^\/]*)\// + ].map(r => r.source).join(''),'i'); + request.loop(request, { + regexp: regexp3, + pov: request.options.pov + }, (input, params) => { + const vars = input.match(params.regexp).groups; + switch (params.pov) { + case "first": + return input.replace(params.regexp, vars.replacement1); + case "second": + return input.replace(params.regexp, vars.replacement2); + case "third": + return input.replace(params.regexp, vars.replacement3); + } + }); +} + +/* if/a is a/word/ becomes "word" because the condition is met */ +function ife(request) { + const regexp1 = new RegExp([ + /if\/(?[^\/]*) is /, + /(?[^\/]*)\//, + /(?[^\/]*)\// + ].map(r => r.source).join(''),'i'); + request.loop(request, { + regexp: regexp1 + }, (input, params) => { + const vars = input.match(params.regexp).groups; + if (vars.lhs === vars.rhs) { + return input.replace(params.regexp, vars.replacement); + } else { + return null; + } + }); + + const regexp2 = new RegExp([ + /ife\/(?[^\/]*) is /, + /(?[^\/]*)\//, + /(?[^\/]*)\//, + /(?[^\/]*)\// + ].map(r => r.source).join(''),'i'); + request.loop(request, { + regexp: regexp1 + }, (input, params) => { + const vars = input.match(params.regexp).groups; + if (vars.lhs === vars.rhs) { + return input.replace(params.regexp, vars.replacement); + } else { + return input.replace(params.regexp, vars.else); + } + }); +} + +/* cut/off first 1/word/ shortens into "ord" */ +function cut(request) { + const regexp = new RegExp([ + /cut\/(?[^\/]+)\//, + /(?off|only) /, + /(?first|last) /, + /(?[1-9][0-9]*)\// + ].map(r => r.source).join(''),'i'); + request.loop(request, { + regexp: regexp + }, (input, params) => { + const vars = input.match(params.regexp).groups; + const length = vars.target.length; + let start, end; + if (vars.offonly == "only") { + if (isFirst) { // only first n + start = 0; + end = Math.min(vars.index, length - 1); + } else { // only last n + start = Math.max(length - vars.index, 1); + end = length; + } + } else { + if (vars.firstlast == "first") { // off first n + start = Math.min(vars.index, length - 1);; + end = length; + } else { // off last n + start = 0; + end = Math.max(length - vars.index, 1); + } + } + return vars.target.slice(start, end); + }); +} + +/* cap/WORD/ changes the case to "word" (Cap/word/ for "Word" and CAP/word/ for "WORD") */ +function cap(request) { + const regexp = new RegExp([ + /(?