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, } try { if (typeof(input) == "object" && input instanceof Node) { request.loop = loopNode; } else if (typeof(input) == "string") { request.loop = loopString; } else { console.error("Input of type " + typeof(input) + " is unsupported"); return; } } catch { console.error("Translation of Node objects is unsupported outside of a browser context"); 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({ input: child, options: request.options }, params, method); child = next; } break; case 3: /* TEXT_NODE */ loopNodeValue({ input: node, options: request.options }, params, method); } } /* Iterator for strings belonging to HTML Node elements */ function loopNodeValue(request, params, method) { if (request.input.nodeValue.match(params.regexp) == null) { return; } request.input.nodeValue = method(request.input.nodeValue, params); loopNodeValue(request, 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])\//, /(?:(?(?:(?:simple|progressive|perfect|perfect[ _]progressive|participle(?![ _]future))[ _])*(?:past|present|future))\/)*/, /(?[\w\s]+)\// ].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(request.options.also).forEach(([key, replacement]) => { const regexp = new RegExp(RegExp.escape(key), "gi"); request.input = request.input.replaceAll(regexp, 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)/, /(?: and (?!\1)(?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.targetPov1.toLowerCase() == params.pov) { return input.replace(params.regexp, vars.replacement); } else if (vars.targetPov2 == null) { return input.replace(params.regexp, ""); } else if (vars.targetPov2.toLowerCase() == params.pov) { return input.replace(params.regexp, vars.replacement); } else { return input.replace(params.regexp, ""); } }); 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.toLowerCase() == params.pov) { return input.replace(params.regexp, vars.replacement1); } else if (vars.targetPov2.toLowerCase() == params.pov) { if (vars.conjunction1.toLowerCase() == "and") { return input.replace(params.regexp, vars.replacement1); } else { return input.replace(params.regexp, vars.replacement2); } } else if (vars.conjunction2 == null) { return input.replace(params.regexp, ""); } else { return input.replace(params.regexp, vars.replacement2); } }); const regexp3 = new RegExp([ /alt\/(?:(first|second|third) or (?!\1)(first|second|third) or (?!\1|\2)(?:first|second|third)\/)?/, /(?[^\/]*)\//, /(?[^\/]*)\//, /(?[^\/]*)\// ].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 input.replace(params.regexp, ""); } }); const regexp2 = new RegExp([ /ife\/(?[^\/]*) is /, /(?[^\/]*)\//, /(?[^\/]*)\//, /(?[^\/]*)\// ].map(r => r.source).join(''), 'i'); request.loop(request, { regexp: regexp2 }, (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]*)\//, /(?(?!cut\/)[^\/]+)\// ].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.toLowerCase() == "only") { if (vars.firstlast.toLowerCase() == "first") { // 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.toLowerCase() == "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 input.replace(params.regexp, 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([ /(?