metamorpov-library/metamorpov.js

645 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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([
/(?<firstLetter>v)r/,
/(?<category>[nb])\//,
/(?:(?<tense>(?:(?:simple|progressive|perfect|perfect[ _]progressive|participle(?![ _]future))[ _])*(?:past|present|future))\/)*/,
/(?<verb>[\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\/(?<targetPov1>first|second|third)/,
/(?: and (?!\1)(?<targetPov2>first|second|third))?/,
/\/(?<replacement>[^\/]*)\//
].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\/(?<targetPov1>first|second|third) /,
/(?<conjunction1>and|or) /,
/(?!\1)(?<targetPov2>first|second|third)/,
/(?: (?!\2)(?<conjunction2>and|or) (?!\1|\3)(?:first|second|third))?/,
/\/(?<replacement1>[^\/]*)\//,
/(?<replacement2>[^\/]*)\//
].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)\/)?/,
/(?<replacement1>[^\/]*)\//,
/(?<replacement2>[^\/]*)\//,
/(?<replacement3>[^\/]*)\//
].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\/(?<lhs>[^\/]*) is /,
/(?<rhs>[^\/]*)\//,
/(?<replacement>[^\/]*)\//
].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\/(?<lhs>[^\/]*) is /,
/(?<rhs>[^\/]*)\//,
/(?<replacement>[^\/]*)\//,
/(?<else>[^\/]*)\//
].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\/(?<offonly>off|only) /,
/(?<firstlast>first|last) /,
/(?<index>[1-9][0-9]*)\//,
/(?<target>[^\/]+)\//
].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([
/(?<style>cap|Cap|CAP)\//,
/(?<target>[^\/]+)\//
].map(r => r.source).join(''));
request.loop(request, {
regexp: regexp
}, (input, params) => {
const vars = input.match(params.regexp).groups;
const isLowerCase = /c/.test(vars.style.slice(0,1)); // cap
if (isLowerCase) {
return input.replace(params.regexp, vars.target.toLowerCase());
} else {
const isStartCase = /a/.test(vars.style.slice(1,2)); // Cap
if (isStartCase) {
return input.replace(params.regexp, capitalize(vars.target));
} else { // CAP
return input.replace(params.regexp, vars.target.toUpperCase());
}
}
});
}
/* mrr/word/ flips into "dorw" */
function mrr(request) {
request.loop(request, {
regexp: /mrr\/(?<target>[^\/]+)\//i
}, (input, params) => {
const target = input.match(params.regexp).groups.target;
return input.replace(params.regexp, target.split('').reverse().join(''));
});
}
/* a/an determines "a" or "an" by the pronunciation of the following word */
function aan(request) {
request.loop(request, {
regexp: /a\/an (?<target>\w+)/i
}, (input, params) => {
const target = input.match(params.regexp).groups.target;
return input.replace(params.regexp, articleHelper(target, { numbers: 'colloquial' }));
});
}
/* Retrieves an index used by the verbs library for conjugation */
function getPovIndex(pov, options) {
switch (pov) {
case "first":
return 0;
case "second":
return 1;
case "third-singular":
return 2;
case "third":
if (getPronouns(options.preset, options.other).plurality == "singular") {
return 2;
} else { /* Plurality is plural, "they go" */
return 5;
}
return getThirdIndex(options);
}
}
/* Returns the input word, capitalized */
function capitalize(word) {
return word.charAt(0).toUpperCase() + word.slice(1);
}
/* Returns pronouns by preset */
function getPronouns(key, other) {
switch (key) {
case "she":
return {
"subjective": "she",
"objective": "her",
"possessive": "her",
"adjective": "hers",
"reflexive": "herself",
"honorific-abbr": "Ms.",
"honorific": "miss",
"adult": "woman",
"youth": "girl",
"parent": "mother",
"child": "daughter",
"sibling": "sister",
"married": "wife",
"dating": "girlfriend",
"plurality": "singular"
};
case "he":
return {
"subjective": "he",
"objective": "him",
"possessive": "his",
"adjective": "his",
"reflexive": "himself",
"honorific-abbr": "Mr.",
"honorific": "mister",
"adult": "man",
"youth": "boy",
"parent": "father",
"child": "son",
"sibling": "brother",
"married": "husband",
"dating": "boyfriend",
"plurality": "singular"
};
case "they":
return {
"subjective": "they",
"objective": "them",
"possessive": "their",
"adjective": "theirs",
"reflexive": "themself",
"honorific-abbr": "Mx.",
"honorific": "mix",
"adult": "person",
"youth": "kid",
"parent": "parent",
"child": "child",
"sibling": "sibling",
"married": "spouse",
"dating": "partner",
"plurality": "plural"
};
case "first":
return {
"subjective": "I",
"objective": "me",
"possessive": "my",
"adjective": "mine",
"reflexive": "myself"
};
case "second":
return {
"subjective": "you",
"objective": "you",
"possessive": "your",
"adjective": "yours",
"reflexive": "yourself"
};
case "first-plural":
return {
"subjective": "we",
"objective": "us",
"possessive": "our",
"adjective": "ours",
"reflexive": "ourselves"
};
case "second-plural":
return {
"subjective": "you",
"objective": "you",
"possessive": "your",
"adjective": "yours",
"reflexive": "yourselves"
};
case "third-plural":
return {
"subjective": "they",
"objective": "them",
"possessive": "their",
"adjective": "theirs",
"reflexive": "themselves"
};
case "other":
return other;
}
}
/* Handles capitalization and replacement for pronouns */
function prnHelper(input, params) {
let replacement = params.pronoun;
const isCapital = /P/.test(input.match(params.regexp)[1]);
if (isCapital) {
replacement = capitalize(replacement);
}
return input.replace(params.regexp, replacement);
}