"use strict"; const path = require("path"); const fs = require("mz/fs"); const throat = require("throat"); const serializeToXML = require("xmlserializer").serializeToString; const { JSDOM } = require("jsdom"); const substitutions = require("./substitutions.json"); module.exports = async (cachePath, manifestPath, contentPath) => { const manifestContents = await fs.readFile(manifestPath, { encoding: "utf-8" }); const chapters = JSON.parse(manifestContents); console.log("All chapters downloaded; beginning conversion to EPUB chapters"); const mapper = throat(10, chapter => convertChapter(chapter, cachePath, contentPath)); await Promise.all(chapters.map(mapper)); console.log("All chapters converted"); }; async function convertChapter(chapter, cachePath, contentPath) { const filename = chapter.filename; const filePath = path.resolve(cachePath, filename); console.log(`- Reading ${filename}`); const contents = await fs.readFile(filePath, { encoding: "utf-8" }); console.log(`- Read ${filename}`); const rawChapterJSDOM = new JSDOM(contents); const output = getChapterString(chapter, rawChapterJSDOM.window.document); // TODO: this should probably not be necessary... jsdom bug I guess!? rawChapterJSDOM.window.close(); const destFileName = `${path.basename(filename, ".html")}.xhtml`; const destFilePath = path.resolve(contentPath, destFileName); await fs.writeFile(destFilePath, output); console.log(`- Finished converting ${filename}`); } function getChapterString(chapter, rawChapterDoc) { const body = getBodyXML(chapter, rawChapterDoc.querySelector(".entry-content")); return `
contentEl.removeChild(contentEl.firstElementChild); // Remove everything after the last
(e.g. analytics
s or Last Chapter/Next Chapter
s
while (isEmptyOrGarbage(contentEl.lastElementChild)) {
contentEl.removeChild(contentEl.lastElementChild);
}
// Remove redundant attributes
Array.prototype.forEach.call(contentEl.children, child => {
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");
if (child.getAttribute("style") === "text-align:left;") {
child.removeAttribute("style");
}
});
// Remove empty s and s
// Remove style attributes from them, as they're always messed up.
const ems = contentEl.querySelectorAll("em, i");
Array.prototype.forEach.call(ems, em => {
if (em.textContent.trim() === "") {
const replacement = contentEl.ownerDocument.createTextNode(" ");
em.parentNode.replaceChild(replacement, em);
} else {
em.removeAttribute("style");
}
});
// In https://parahumans.wordpress.com/2013/01/05/monarch-16-13/ there are some s that should be s O_o
const addresses = contentEl.querySelectorAll("address");
Array.prototype.forEach.call(addresses, address => {
const p = contentEl.ownerDocument.createElement("p");
p.innerHTML = address.innerHTML;
address.parentNode.replaceChild(p, address);
});
// Every except underline ones is pointless at best and frequently messed up. (Weird font size, line spacing,
// etc.)
const spans = contentEl.querySelectorAll("span");
Array.prototype.forEach.call(spans, span => {
if (span.getAttribute("style") === "text-decoration:underline;") {
return;
}
if (span.textContent.trim() === "") {
span.parentNode.removeChild(span);
} else {
const docFrag = contentEl.ownerDocument.createDocumentFragment();
while (span.firstChild) {
docFrag.appendChild(span.firstChild);
}
span.parentNode.replaceChild(docFrag, span);
}
});
// 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);
}
let xml = serializeToXML(bodyEl);
// Fix recurring strange pattern of extra ......
in
\n
\s*<\/em><\/p>/g, "
([^>]+)<\/em>(!|\?|\.)<\/p>/g, " $1$2
”/g, "
“"); xml = xml.replace(/“\s*<\/p>/g, "”
"); xml = xml.replace(/“\s*<\/em><\/p>/g, "”"); xml = xml.replace(/‘\s*<\/p>/g, "’"); xml = xml.replace(/‘\s*<\/em><\/p>/g, "’"); xml = xml.replace(/,” <\/em>/g, ",” "); xml = xml.replace(/′/g, "’"); xml = xml.replace(/″/g, "”"); xml = xml.replace(/([A-Za-z])‘s(\s?)/g, "$1’s$2"); xml = xml.replace(/I‘m/g, "I’m"); xml = xml.replace(/“\s+/g, "
“"); xml = xml.replace(/'/g, "’"); // Fixes dashes xml = xml.replace(/ – /g, "—"); xml = xml.replace(/“-/g, "“—"); xml = xml.replace(/-[,.]?”/g, "—”"); xml = xml.replace(/-(!|\?)”/g, "—$1”"); xml = xml.replace(/-[,.]?<\/em>”/g, "—
”"); xml = xml.replace(/-“/g, "—”"); xml = xml.replace(/-/g, "
—"); xml = xml.replace(/-<\/p>/g, "—
"); xml = xml.replace(/-<\/em><\/p>/g, "—"); xml = xml.replace(/\s?\s?–\s?\s?/g, "—"); xml = xml.replace(/-\s\s?/g, "—"); xml = xml.replace(/\s?\s-/g, "—"); // Fix recurring miscapitalization with questions xml = xml.replace(/\?”\s\s?She asked/g, "?” she asked"); xml = xml.replace(/\?”\s\s?He asked/g, "?” he asked"); // Fix extra periods at the end of paragraphs xml = xml.replace(/\.\.<\/p>/g, "."); // Replace single-word s with s. Other s are probably erroneous too, but these are known-bad. xml = xml.replace(/([A-Za-z]+)<\/i>/g, "$1"); // 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. xml = xml.replace(/the clairvoyant/g, "the Clairvoyant"); // This is consistently missing accents xml = xml.replace(/Yangban/g, "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(/Thanda<\/em>/g, "Thanda"); xml = xml.replace(/Sifara([^<]*)<\/em>/g, "Sifara$1"); // One-off fixes (substitutions[chapter.url] || []).forEach(substitution => { const indexOf = xml.indexOf(substitution.before); if (indexOf === -1) { console.warn(`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)) { console.warn(`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)), substitution.after); }); // Serializer inserts extra xmlns for us since it doesn't know we're going to put this into a xml = xml.replace(//, ""); return xml; } function isEmptyOrGarbage(el) { const text = el.textContent.trim(); return text === "" || text.startsWith("Last Chapter") || text.startsWith("Next Chapter"); } function escapeRegExp(str) { return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&"); }