"use strict"; const path = require("path"); const fs = require("mz/fs"); const throat = require("throat"); const jsdom = require("./jsdom.js"); const serializeToXml = require("xmlserializer").serializeToString; module.exports = function (cachePath, contentPath) { return getChapterFilePaths(cachePath) .then(function (chapterFilePaths) { console.log("All chapters downloaded; beginning conversion to EPUB chapters"); const mapper = throat(10, function (filePath) { return convertChapter(filePath, contentPath); }); return Promise.all(chapterFilePaths.map(mapper)); }) .then(function () { console.log("All chapters converted"); }); }; function getChapterFilePaths(cachePath) { return fs.readdir(cachePath).then(function (filenames) { return filenames.filter(function (f) { return f.endsWith(".html"); }) .map(function (f) { return path.resolve(cachePath, f); }); }); } function convertChapter(filePath, contentPath) { const filename = path.basename(filePath); console.log(`- Reading ${filename}`); return fs.readFile(filePath, { encoding: "utf-8" }).then(function (contents) { console.log(`- Read ${filename}`); const rawChapterDoc = jsdom(contents); const output = getChapterString(rawChapterDoc); // TODO: this should probably not be necessary... jsdom bug I guess!? rawChapterDoc.defaultView.close(); const destFileName = `${path.basename(filename, ".html")}.xhtml`; const destFilePath = path.resolve(contentPath, destFileName); return fs.writeFile(destFilePath, output); }) .then(function () { console.log(`- Finished converting ${filename}`); }); } function getChapterString(rawChapterDoc) { const headingEl = rawChapterDoc.querySelector("h1.entry-title"); const title = headingEl.textContent; const body = getBodyXml(headingEl, rawChapterDoc.querySelector(".entry-content")); return ` ${title} ${body} `; } function getBodyXml(headingEl, contentEl) { // Remove initial Next Chapter and Previous Chapter

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

(e.g. analytics

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

s or Last Chapter/Next Chapter

s while (isEmptyOrGarbage(contentEl.lastElementChild)) { contentEl.removeChild(contentEl.lastElementChild); } // Remove redundant dir="ltr" and align="LEFT" and style="text-align: left;" Array.prototype.forEach.call(contentEl.children, function (child) { if (child.getAttribute("dir") === "ltr") { child.removeAttribute("dir"); } if ((child.getAttribute("align") || "").toLowerCase() === "left") { child.removeAttribute("align"); } if (child.getAttribute("style") === "text-align:left;") { child.removeAttribute("style"); } }); // Remove empty s and s const ems = contentEl.querySelectorAll("em, i"); Array.prototype.forEach.call(ems, function (em) { if (em.textContent.trim() === "") { em.parentNode.removeChild(em); } }); // Synthesize a tag to serialize const bodyEl = contentEl.ownerDocument.createElement("body"); const h1El = contentEl.ownerDocument.createElement("h1"); h1El.textContent = headingEl.textContent; bodyEl.appendChild(h1El); while (contentEl.firstChild) { bodyEl.appendChild(contentEl.firstChild); } let xml = serializeToXml(bodyEl); // Fix recurring strange pattern of extra
in

......
\n

xml = xml.replace(/\s*<\/em><\/p>/g, '

'); // One-off fixes xml = xml.replace(/truck reached\nthe other Nine/, 'truck reached the other Nine'); // 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"); }