"use strict"; const workerpool = require("workerpool"); const fs = require("fs"); const { JSDOM } = require("jsdom"); const substitutions = require("./substitutions.json"); workerpool.worker({ convertChapter }); function convertChapter(chapter, book, inputPath, outputPath) { const contents = fs.readFileSync(inputPath, { encoding: "utf-8" }); const rawChapterJSDOM = new JSDOM(contents); const { output, warnings } = getChapterString(chapter, book, rawChapterJSDOM.window.document); // TODO: this should probably not be necessary... jsdom bug I guess!? rawChapterJSDOM.window.close(); fs.writeFileSync(outputPath, output); return warnings; } function getChapterString(chapter, book, rawChapterDoc) { const { xml, warnings } = getBodyXML(chapter, book, rawChapterDoc.querySelector(".entry-content")); const output = ` ${chapter.title} ${xml} `; return { output, warnings }; } function getBodyXML(chapter, book, contentEl) { const warnings = []; // Remove initial Next Chapter and Previous Chapter

contentEl.firstElementChild.remove(); // Remove everything after the last

(e.g. analytics

s) const lastP = contentEl.querySelector("p:last-of-type"); while (contentEl.lastElementChild !== lastP) { contentEl.lastElementChild.remove(); } // Remove empty

s or Last Chapter/Next Chapter

s while (isEmptyOrGarbage(contentEl.lastElementChild)) { contentEl.lastElementChild.remove(); } // Remove redundant attributes and style for (const child of contentEl.children) { if (child.getAttribute("dir") === "ltr") { child.removeAttribute("dir"); } // Only ever appears with align="LEFT" (useless) or align="CENTER" overridden by style="text-align: left;" (also // useless) child.removeAttribute("align"); const style = child.getAttribute("style"); if (style === "text-align:left;" || style === "text-align: left;") { child.removeAttribute("style"); } // Worm uses 30px; Ward mostly uses 40px but sometimes uses 30px/60px. Let's standardize on 30px. if (style === "text-align:left;padding-left:30px;" || style === "text-align: left;padding-left: 40px;" || style === "text-align: left; padding-left: 40px;" || style === "padding-left: 40px;") { child.setAttribute("style", "padding-left: 30px;"); } } // Remove empty inline elements. // Remove style attributes from inline elements, as they're always messed up. for (const el of contentEl.querySelectorAll("em, i, strong, b")) { const { textContent } = el; if (textContent === "") { el.remove(); } else if (textContent.trim() === "") { if (el.childElementCount === 0) { el.replaceWith(" "); } else if (el.childElementCount === 1 && el.children[0].localName === "br") { el.outerHTML = "
\n"; } } else { el.removeAttribute("style"); } } // In https://parahumans.wordpress.com/2013/01/05/monarch-16-13/ there are some

s that should be

s O_o for (const address of contentEl.querySelectorAll("address")) { const p = contentEl.ownerDocument.createElement("p"); p.innerHTML = address.innerHTML; address.replaceWith(p); } // Every except underline ones is pointless at best and frequently messed up. (Weird font size, line spacing, // etc.) for (const span of contentEl.querySelectorAll("span")) { if (span.getAttribute("style") === "text-decoration:underline;") { continue; } if (span.textContent.trim() === "") { span.remove(); } else { const docFrag = contentEl.ownerDocument.createDocumentFragment(); while (span.firstChild) { docFrag.appendChild(span.firstChild); } span.replaceWith(docFrag); } } // In Ward, CloudFlare email protection obfuscates the email addresses: // https://usamaejaz.com/cloudflare-email-decoding/ for (const emailEl of contentEl.querySelectorAll("[data-cfemail]")) { const decoded = decodeCloudFlareEmail(emailEl.dataset.cfemail); emailEl.replaceWith(contentEl.ownerDocument.createTextNode(decoded)); } // Synthesize a tag to serialize const bodyEl = contentEl.ownerDocument.createElement("body"); const h1El = contentEl.ownerDocument.createElement("h1"); h1El.textContent = chapter.title; bodyEl.appendChild(h1El); while (contentEl.firstChild) { bodyEl.appendChild(contentEl.firstChild); } const xmlSerializer = new contentEl.ownerDocument.defaultView.XMLSerializer(); let xml = xmlSerializer.serializeToString(bodyEl); // Fix recurring strange pattern of extra
in

......
\n

xml = xml.replace(/
\s*<\/em><\/p>/ug, "

"); // Replace single-word s with s. Other s are probably erroneous too, but these are known-bad. xml = xml.replace(/([^ ]+)<\/i>/ug, "$1"); xml = xml.replace(/([^ ]+)( +)<\/i>/ug, "$1$2"); // There are way too many nonbreaking spaces where they don't belong. If they show up three in a row, then let them // live; they're maybe being used for alignment or something. Otherwise, they die. // // Also, normalize spaces after a period/quote mark to two (normal) spaces. The second one is invisible when // rendered, but it helps future heuristics detect end of sentences. xml = xml.replace(/\xA0{1,2}(?!\x20\xA0)/ug, " "); xml = xml.replace(/([.”])\x20*\xA0[\xA0\x20]*/ug, "$1 "); xml = xml.replace(/([.”])\x20{3,}/ug, "$1 "); function fixEms() { // Fix recurring broken-up or erroneous s xml = xml.replace(/<\/em>‘s/ug, "’s"); xml = xml.replace(/<\/em>/ug, ""); xml = xml.replace(/<\/em>/ug, ""); xml = xml.replace(/(\s?\s?[^A-Za-z]\s?\s?)<\/em>/ug, "$1"); xml = xml.replace(/<\/em>(\s?\s?[^A-Za-z]\s?\s?)/ug, "$1"); xml = xml.replace(/“([^>]+)<\/em>(!|\?|\.)”/ug, "“$1$2”"); xml = xml.replace(/

([^>]+)<\/em>(!|\?|\.)<\/p>/ug, "

$1$2

"); xml = xml.replace(/(!|\?|\.)\s{2}<\/em><\/p>/ug, "$1

"); xml = xml.replace(/([a-z]+)(\?|\.)<\/em>/ug, "$1$2"); xml = xml.replace(/([^>]+?)( +)<\/em>/ug, "$1$2"); xml = xml.replace(/ ([a-zA-Z]+)<\/em>/ug, " $1"); xml = xml.replace(/‘\s*([^<]+)\s*’<\/em>/ug, "‘$1’"); xml = xml.replace(/‘\s*([^<]+)\s*<\/em>\s*’/ug, "‘$1’"); xml = xml.replace(/‘\s*\s*([^<]+)\s*’<\/em>/ug, "‘$1’"); xml = xml.replace(/“\s*([^<”]+)\s*”<\/em>/ug, "“$1”"); xml = xml.replace(/“\s*([^<”]+)\s*<\/em>\s*”/ug, "“$1”"); xml = xml.replace(/“\s*\s*([^<”]+)\s*”<\/em>/ug, "“$1”"); xml = xml.replace(/([^\n>]) ?/ug, "$1 "); xml = xml.replace(/ ?<\/em>/ug, " "); xml = xml.replace(/]+)> /ug, ""); xml = xml.replace(/<\/em> <\/p>/ug, "

"); xml = xml.replace(/([a-z]+),<\/em>/ug, "$1,"); } // These quote/apostrophe/em fixes interact with each other. TODO: try to disentangle so we don't repeat all of // fixEms. xml = xml.replace(/,” <\/em>/ug, ",” "); fixEms(); xml = xml.replace(/

”/ug, "

“"); xml = xml.replace(/“\s*<\/p>/ug, "”

"); xml = xml.replace(/“\s*<\/em><\/p>/ug, "

"); xml = xml.replace(/‘\s*<\/p>/ug, "’

"); xml = xml.replace(/‘\s*<\/em><\/p>/ug, "’

"); xml = xml.replace(/,” <\/em>/ug, ",” "); xml = xml.replace(/′/ug, "’"); xml = xml.replace(/″/ug, "”"); xml = xml.replace(/([A-Za-z])‘s(\s?)/ug, "$1’s$2"); xml = xml.replace(/I‘m/ug, "I’m"); xml = xml.replace(/

“\s+/ug, "

“"); xml = xml.replace(/\s+”/ug, "”"); xml = xml.replace(/'/ug, "’"); xml = xml.replace(/’([A-Za-z]+)’/ug, "‘$1’"); xml = xml.replace(/([a-z])”<\/p>/ug, "$1.”

"); fixEms(); xml = xml.replace(/‘([^<]+)<\/em>‘/ug, "‘$1’"); xml = xml.replace(/([a-z]+)!<\/em>/ug, "$1!"); xml = xml.replace(/(?([\w ’]+)([!.?])”<\/em>/ug, "$1$2”"); xml = xml.replace(/([\w ’]+[!.?])”<\/em>/ug, "$1”"); xml = xml.replace(/I”(m|ll)/ug, "I’$1"); xml = xml.replace(/””<\/p>/ug, "”

"); xml = xml.replace(/^([^“]+?) ?”(?![ —<])/ugm, "$1 “"); xml = xml.replace(/(?([A-Za-z]+),<\/em>(?!”| +[A-Za-z]+ thought)/u, "$1,"); xml = xml.replace(/‘([Kk])ay(?!’)/ug, "’$1ay"); xml = xml.replace(/(Why|What|Who|How|Where|When)<\/em>\?/ug, "$1?"); xml = xml.replace(/,<\/em>/ug, ","); xml = xml.replace(/,”<\/p>/ug, ".”

"); xml = xml.replace(/

(.*),<\/p>/ug, "

$1.

"); xml = xml.replace(/‘(\w+)‘(\w+)’/ug, "‘$1’$2’"); xml = xml.replace(/([a-z]+), ([a-z]+)<\/em>/ug, "$1, $2"); // Similar problems occur in Ward with and as do in Worm with s xml = xml.replace(//ug, ""); xml = xml.replace(/(\s*
\s*)<\/b>/ug, "$1"); xml = xml.replace(/(\s*
\s*)<\/strong>/ug, "$1"); xml = xml.replace(/<\/strong>(\s*)/ug, "$1"); xml = xml.replace(/@<\/strong>/ug, "@"); xml = xml.replace(/
(\s*)<\/strong>/ug, "

$1"); xml = xml.replace(/(\s*)<\/strong>/ug, "
$1"); xml = xml.replace(/>(.*)<\/strong>:$1:<"); // No need for line breaks before paragraph ends or after paragraph starts // These often occur with the
s inside /// fixed above. xml = xml.replace(/
\s*<\/p>/ug, "

"); xml = xml.replace(/


\s*/ug, "

"); // This is another quote fix but it needs to happen after the line break deletion... so entangled, ugh. xml = xml.replace(/<\/em>\s*“\s*<\/p>/ug, "

"); // Fix missing spaces after commas xml = xml.replace(/([a-zA-Z]+),([a-zA-Z]+)/ug, "$1, $2"); // Fix bad periods and spacing/markup surrounding them xml = xml.replace(/\.\.<\/p>/ug, ".

"); xml = xml.replace(/\.\.”<\/p>/ug, ".”

"); xml = xml.replace(/ \. /ug, ". "); xml = xml.replace(/ \.<\/p>/ug, ".

"); xml = xml.replace(/\.\.\./ug, "…"); xml = xml.replace(/\.\. {2}/ug, ". "); xml = xml.replace(/\.\./ug, "…"); xml = xml.replace(/(?/ug, "

"); xml = xml.replace(/([a-z]) ,/ug, "$1,"); // Use actual emojis instead of images xml = xml.replace( // eslint-disable-next-line max-len /O_o/ug, "🤨" ); xml = fixTruncatedWords(xml); xml = fixDialogueTags(xml); xml = fixForeignNames(xml); xml = standardizeNames(xml); xml = fixEmDashes(xml); xml = enDashJointNames(xml); xml = fixPossessives(xml); xml = cleanSceneBreaks(xml); xml = fixCapitalization(xml, book); xml = fixMispellings(xml); xml = fixHyphens(xml); xml = standardizeSpellings(xml); xml = fixCaseNumbers(xml); // One-off fixes for (const substitution of substitutions[chapter.url] || []) { if (substitution.before) { const indexOf = xml.indexOf(substitution.before); if (indexOf === -1) { warnings.push(`Could not find text "${substitution.before}" in ${chapter.url}. The chapter may have been ` + `updated at the source, in which case, you should edit substitutions.json.`); } if (indexOf !== xml.lastIndexOf(substitution.before)) { warnings.push(`The text "${substitution.before}" occurred twice, and so the substitution was ambiguous. ` + `Update substitutions.json for a more precise substitution.`); } xml = xml.replace(new RegExp(escapeRegExp(substitution.before), "u"), substitution.after); } else if (substitution.regExp) { xml = xml.replace(new RegExp(substitution.regExp, "ug"), substitution.replacement); } else { warnings.push(`Invalid substitution specified for ${chapter.url}`); } } // Serializer inserts extra xmlns for us since it doesn't know we're going to put this into a . // Use this opportunity to insert a comment pointing to the original URL, for reference. xml = xml.replace( //u, `\n\n` ); return { xml, warnings }; } function fixTruncatedWords(xml) { xml = xml.replace(/‘Sup/ug, "’Sup"); xml = xml.replace(/‘cuz/ug, "’cuz"); // Short for "Sidepeace" xml = xml.replace(/[‘’][Pp]iece(?![a-z])/ug, "’Piece"); // Short for "Disjoint" xml = xml.replace(/[‘’][Jj]oint(?![a-z])/ug, "’Joint"); // Short for "Contender" xml = xml.replace(/[‘’][Tt]end(?![a-z])/ug, "’Tend"); // Short for "Anelace" xml = xml.replace(/[‘’][Ll]ace(?![a-z])/ug, "’Lace"); // Short for "Birdcage" xml = xml.replace(/[‘’][Cc]age(?![a-z])/ug, "’Cage"); // We can't do "’Clear" (short for Crystalclear) here because it appears too much as a normal word preceded by an // open quote, so we do that in substitutions.json. return xml; } function fixDialogueTags(xml) { // Fix recurring miscapitalization with questions xml = xml.replace(/\?”\s\s?She asked/ug, "?” she asked"); xml = xml.replace(/\?”\s\s?He asked/ug, "?” he asked"); // The author often fails to terminate a sentence, instead using a comma after a dialogue tag. For example, // > “I didn’t get much done,” Greg said, “I got distracted by... // This should instead be // > “I didn’t get much done,” Greg said. “I got distracted by... // // Our heuristic is to try to automatically fix this if the dialogue tag is two words (X said/admitted/sighed/etc.). // // This sometimes overcorrects, as in the following example: // > “Basically,” Alec said, “For your powers to manifest, ... // Here instead we should lowercase the "f". We handle that via one-offs in substitutions.json. // // This applies to ~800 instances, so although we have to correct back in substitutions.json a decent number of // times, it definitely pays for itself. Most of the instances we have to correct back we also need to fix the // capitalization anyway, and that's harder to do automatically, since proper names/"I"/etc. stay capitalized. xml = xml.replace(/,” ([A-Za-z]+ [A-Za-z]+), “([A-Z])/ug, ",” $1. “$2"); return xml; } function fixForeignNames(xml) { // This is consistently missing diacritics xml = xml.replace(/Yangban/ug, "Yàngbǎn"); // These are usually not italicized, but sometimes are. Other foreign-language names (like Yàngbǎn) are not // italicized, so we go in the direction of removing the italics. xml = xml.replace(/Garama<\/em>/ug, "Garama"); xml = xml.replace(/Thanda<\/em>/ug, "Thanda"); xml = xml.replace(/Sifara([^<]*)<\/em>/ug, "Sifara$1"); xml = xml.replace(/Moord Nag([^<]*)<\/em>/ug, "Moord Nag$1"); xml = xml.replace(/Califa de Perro([^<]*)<\/em>/ug, "Califa de Perro$1"); xml = xml.replace(/Turanta([^<]*)<\/em>/ug, "Turanta$1"); return xml; } function standardizeNames(xml) { // 197 instances of "Mrs." to 21 of "Ms." xml = xml.replace(/Ms\. Yamada/ug, "Mrs. Yamada"); // 25 instances of "Amias" to 3 of "Amais" xml = xml.replace(/Amais/ug, "Amias"); // 185 instances of Juliette to 4 of Juliet xml = xml.replace(/Juliet(?=\b)/ug, "Juliette"); // Earlier chapters have a space; later ones do not. They're separate words, so side with the earlier chapters. // One location is missing the "k". xml = xml.replace(/Crock? o[‘’]Shit/ug, "Crock o’ Shit"); // 5 instances of "Jotun" to 2 of "Jotunn" xml = xml.replace(/Jotunn/ug, "Jotun"); // 13 instances of Elman to 1 of Elmann xml = xml.replace(/Elmann/ug, "Elman"); // Thousands of instances of Tattletale to 4 instances of Tatteltale xml = xml.replace(/Tatteltale/ug, "Tattletale"); // 73 instances of Über to 2 of Uber xml = xml.replace(/Uber/ug, "Über"); return xml; } function fixEmDashes(xml) { xml = xml.replace(/ – /ug, "—"); xml = xml.replace(/“((?:)?)-/ug, "“$1—"); xml = xml.replace(/-[,.]?”/ug, "—”"); xml = xml.replace(/-(!|\?)”/ug, "—$1”"); xml = xml.replace(/-[,.]?<\/([a-z]+)>”/ug, "—”"); xml = xml.replace(/-“/ug, "—”"); xml = xml.replace(/

-/ug, "

—"); xml = xml.replace(/-<\/p>/ug, "—

"); xml = xml.replace(/-
/ug, "—
"); xml = xml.replace(/-<\/([a-z]+)><\/p>/ug, "—

"); xml = xml.replace(/\s?\s?–\s?\s?/ug, "—"); xml = xml.replace(/-\s\s?/ug, "—"); xml = xml.replace(/\s?\s-/ug, "—"); xml = xml.replace(/\s+—”/ug, "—”"); xml = xml.replace(/I-I/ug, "I—I"); xml = xml.replace(/I-uh/ug, "I—uh"); xml = xml.replace(/-\?/ug, "—?"); return xml; } function enDashJointNames(xml) { // Joint names should use en dashes xml = xml.replace(/Dallon-Pelham/ug, "Dallon–Pelham"); xml = xml.replace(/Bet-Gimel/ug, "Bet–Gimel"); xml = xml.replace(/Cheit-Gimel/ug, "Bet–Gimel"); xml = xml.replace(/Tristan-Capricorn/ug, "Tristan–Capricorn"); xml = xml.replace(/Capricorn-Byron/ug, "Capricorn–Byron"); xml = xml.replace(/Tristan-Byron/ug, "Tristan–Byron"); xml = xml.replace(/Gimel-Europe/ug, "Gimel–Europe"); xml = xml.replace(/G-N/ug, "G–N"); xml = xml.replace(/Imp-Damsel/ug, "Imp–Damsel"); xml = xml.replace(/Damsel-Ashley/ug, "Damsel–Ashley"); xml = xml.replace(/Antares-Anelace/ug, "Antares–Anelace"); xml = xml.replace(/Challenger-Gallant/ug, "Challenger–Gallant"); xml = xml.replace(/Undersider(s?)-(Breakthrough|Ambassador)/ug, "Undersider$1–$2"); xml = xml.replace(/Norwalk-Fairfield/ug, "Norwalk–Fairfield"); xml = xml.replace(/East-West/ug, "east–west"); xml = xml.replace(/Creutzfeldt-Jakob/ug, "Creutzfeldt–Jakob"); xml = xml.replace(/Astaroth-Nidhug/ug, "Astaroth–Nidhug"); xml = xml.replace(/Capulet-Montague/ug, "Capulet–Montague"); xml = xml.replace(/Weaver-Clockblocker/ug, "Weaver–Clockblocker"); xml = xml.replace(/Alexandria-Pretender/ug, "Alexandria–Pretender"); xml = xml.replace(/Night Hag-Nyx/ug, "Night Hag–Nyx"); xml = xml.replace(/Crawler-Breed/ug, "Crawler–Breed"); xml = xml.replace(/Simurgh-Myrddin-plant/ug, "Simurgh–Myrddin–plant"); xml = xml.replace(/Armsmaster-Defiant/ug, "Armsmaster–Defiant"); xml = xml.replace(/Matryoshka-Valentin/ug, "Matryoshka–Valentin"); xml = xml.replace(/Gaea-Eden/ug, "Gaea–Eden"); xml = xml.replace(/([Aa])gent-parahuman/ug, "$1gent–parahuman"); xml = xml.replace(/([Pp])arahuman-agent/ug, "$1arahuman–agent"); return xml; } function fixPossessives(xml) { // Fix possessive of names ending in "s". xml = xml.replace( // eslint-disable-next-line max-len /(? would be more semantically appropriate, but loses the author's intent. This is // especially the case in Ward, which uses a variety of different scene breaks. xml = xml.replace(/]*)>■<\/p>/ug, `

`); xml = xml.replace( /

⊙<\/strong><\/p>/ug, `

` ); xml = xml.replace( /

⊙<\/strong><\/em><\/p>/ug, `

` ); xml = xml.replace( /

⊙⊙<\/strong><\/p>/ug, `

` ); xml = xml.replace( /

⊙ *⊙ *⊙ *⊙ *⊙<\/strong><\/p>/ug, `

⊙ ⊙ ⊙ ⊙ ⊙

` ); return xml; } function fixCapitalization(xml, book) { // This occurs enough times it's better to do here than in one-off fixes. We correct the single instance where // it's incorrect to capitalize in the one-off fixes. // Note that Ward contains much talk of "the clairvoyants", so we don't want to capitalize plurals. xml = xml.replace(/([Tt])he clairvoyant(?!s)/ug, "$1he Clairvoyant"); // ReSound's name is sometimes miscapitalized. The word is never used in a non-name context. xml = xml.replace(/Resound/ug, "ReSound"); // The Speedrunners team name is missing its capitalization a couple times. xml = xml.replace(/speedrunners/ug, "Speedrunners"); // The Machine Army is missing its capitalization a couple times. xml = xml.replace(/machine army/ug, "Machine Army"); // "patrol block" is capitalized three different ways: "patrol block", "Patrol block", and "Patrol Block". "patrol // group" is always lowercased. It seems like "Patrol" is a proper name, and is used as a capitalized modifier in // other contexts (e.g. Patrol leader). So let's standardize on "Patrol ". xml = xml.replace( /patrol (block|group|leader|guard|student|uniform|squad|soldier|officer|crew|girl|bus|training)/uig, (_, $1) => `Patrol ${$1.toLowerCase()}` ); // This usually works in Ward (some instances corrected back in substitutions.json), and has a few false positives in // Worm, where it is never needed: if (book === "ward") { xml = xml.replace(/the patrol(?!s|ling)/ug, "the Patrol"); } // This is sometimes missing its capitalization. xml = xml.replace(/the birdcage/ug, "the Birdcage"); // There's no reason why these should be capitalized. xml = xml.replace(/(?)Halberd/ug, "halberd"); xml = xml.replace(/(?)Loft/ug, "loft"); // These are treated as common nouns and not traditionally capitalized. "Krav Maga" remains capitalized, // interestingly (according to dictionaries and Wikipedia). xml = xml.replace(/(?)Judo/ug, "judo"); xml = xml.replace(/(?)Aikido/ug, "aikido"); xml = xml.replace(/(?)Karate/ug, "karate"); xml = xml.replace(/(?)Tae Kwon Do/ug, "tae kwon do"); // There's no reason why university should be capitalized in most contexts, although sometimes it's used as part of // a compound noun or at the beginning of a sentence. xml = xml.replace(/(?|Cornell |Nilles )University(?! Road)/ug, "university"); // Organ names (e.g. brain, arm) or scientific names are not capitalized, so the "corona pollentia" and friends should // not be either. The books are inconsistent. xml = xml.replace(/(?|-)Corona/ug, "corona"); xml = xml.replace(/Pollentia/ug, "pollentia"); xml = xml.replace(/Radiata/ug, "radiata"); xml = xml.replace(/Gemma/ug, "gemma"); // We de-capitalize Valkyrie's "flock", since most uses are de-capitalized (e.g. the many instances in Gleaming // Interlude 9, or Dying 15.z). This is a bit surprising; it seems like an organization name. But I guess it's // informal. xml = xml.replace(/(?)Flock/ug, "flock"); // Especially early in Worm, PRT designations are capitalized; they should not be. This fixes the cases where we // can be reasonably sure they don't start a sentence, although more specific instances are done in // substitutions.json, and some need to be back-corrected. // // Note: "Master" is specifically omitted because it fails poorly on Worm Interlude 4. Other instances need to be // corrected via substitutions.json. // // This also over-de-capitalizes "The Stranger" in Ward (a titan name). Those also get fixed in substitutions.json. xml = xml.replace( // eslint-disable-next-line max-len /(?|\n|: )(Mover|Shaker|Brute|Breaker|Tinker|Blaster|Thinker|Striker|Changer|Trump|Stranger|Shifter|Shaper)(?! [A-Z])/ug, (_, designation) => designation.toLowerCase() ); xml = xml.replace( /(mover|shaker|brute|breaker|tinker|blaster|thinker|master|striker|changer|trump|stranger|shifter|shaper)-(\d+)/ugi, "$1 $2" ); xml = xml.replace( // eslint-disable-next-line max-len /(mover|shaker|brute|breaker|tinker|blaster|thinker|master|striker|changer|trump|stranger|shifter|shaper)[ -/](mover|shaker|brute|breaker|tinker|blaster|thinker|master|striker|changer|trump|stranger|shifter|shaper)/ugi, "$1–$2" ); // Capitalization is inconsistent, but shard names seems to usually be capitalized. xml = xml.replace(/Grasping self/ug, "Grasping Self"); xml = xml.replace(/Cloven stranger/ug, "Cloven Stranger"); xml = xml.replace(/Princess shaper/ug, "Princess Shaper"); xml = xml.replace(/Fragile one/ug, "Fragile One"); // Place names need to always be capitalized xml = xml.replace(/North end/ug, "North End"); xml = xml.replace(/(Stonemast|Shale) avenue/ug, "$1 Avenue"); xml = xml.replace(/(Lord|Slater) street/ug, "$1 Street"); xml = xml.replace(/(Hollow|Cedar) point/ug, "$1 Point"); xml = xml.replace(/(Norwalk|Fenway|Stratford) station/ug, "$1 Station"); xml = xml.replace(/the megalopolis/ug, "the Megalopolis"); xml = xml.replace(/earths(?![a-z])/ug, "Earths"); if (book === "ward") { xml = xml.replace(/the bunker/ug, "the Bunker"); xml = xml.replace(/‘bunker’/ug, "‘Bunker’"); } // "Mom" and "Dad" should be capitalized when used as a proper name. These regexps are tuned to catch a good amount of // instances, without over-correcting for non-proper-name-like cases. Many other instances are handled in // substitutions.json. xml = xml.replace(/(?)Giants/ug, "giants"); return xml; } function fixMispellings(xml) { // This is commonly misspelled. xml = xml.replace(/([Ss])houlderblade/ug, "$1houlder blade"); // All dictionaries agree this is capitalized. xml = xml.replace(/u-turn/ug, "U-turn"); // https://www.dictionary.com/browse/scot-free xml = xml.replace(/scott(?: |-)free/ug, "scot-free"); // https://ugrammarist.com/idiom/change-tack/ xml = xml.replace(/changed tacks/ug, "changed tack"); return xml; } function fixHyphens(xml) { // "X-year-old" should use hyphens; all grammar guides agree. The books are very inconsistent but most often omit // them. xml = xml.replace(/(\w+)[ -]year[ -]old(s?)(?!\w)/ug, "$1-year-old$2"); xml = xml.replace(/(\w+) or (\w+)-year-old/ug, "$1- or $2-year-old"); // Compound numbers from 11 through 99 must be hyphenated, but others should not be. xml = xml.replace( /(? `Case ${caseNumber[0].toUpperCase()}${caseNumber.substring(1)}` ); return xml; } function isEmptyOrGarbage(el) { const text = el.textContent.trim(); return text === "" || text.startsWith("Last Chapter") || text.startsWith("Previous Chapter") || text.startsWith("Next Chapter"); } function escapeRegExp(str) { return str.replace(/[[\]/{}()*+?.\\^$|]/ug, "\\$&"); } function decodeCloudFlareEmail(hash) { let email = ""; const xorWithThis = parseInt(hash.substring(0, 2), 16); for (let i = 2; i < hash.length; i += 2) { const charCode = parseInt(hash.substring(i, i + 2), 16) ^ xorWithThis; email += String.fromCharCode(charCode); } return email; }