You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

426 lines
19 KiB
JavaScript

// Generated by CoffeeScript 2.3.2
(function() {
var EPub, Q, _, archiver, cheerio, ejs, entities, fs, fsextra, mime, path, removeDiacritics, request, rimraf, uslug, uuid,
indexOf = [].indexOf;
path = require("path");
fs = require("fs");
Q = require("q");
_ = require("underscore");
uslug = require("uslug");
ejs = require("ejs");
cheerio = require("cheerio");
entities = require("entities");
request = require("superagent");
fsextra = require("fs-extra");
removeDiacritics = require("diacritics").remove;
mime = require("mime");
archiver = require("archiver");
// provides rm -rf for deleting temp directory across various platforms.
rimraf = require("rimraf");
uuid = function() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r;
r = Math.random() * 16 | 0;
return (c === 'x' ? r : r & 0x3 | 0x8).toString(16);
});
};
EPub = class EPub {
constructor(options, output) {
var self;
this.options = options;
self = this;
this.defer = new Q.defer();
if (output) {
this.options.output = output;
}
if (!this.options.output) {
console.error(new Error("No Output Path"));
this.defer.reject(new Error("No output path"));
return;
}
if (!options.title || !options.content) {
console.error(new Error("Title and content are both required"));
this.defer.reject(new Error("Title and content are both required"));
return;
}
this.options = _.extend({
description: options.title,
publisher: "anonymous",
author: ["anonymous"],
tocTitle: "Table Of Contents",
appendChapterTitles: true,
date: new Date().toISOString(),
lang: "en",
fonts: [],
customOpfTemplatePath: null,
customNcxTocTemplatePath: null,
customHtmlTocTemplatePath: null,
version: 3
}, options);
if (this.options.version === 2) {
this.options.docHeader = `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n<html xmlns="http://www.w3.org/1999/xhtml" lang="${self.options.lang}">`;
} else {
this.options.docHeader = `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE html>\n<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" lang="${self.options.lang}">`;
}
if (_.isString(this.options.author)) {
this.options.author = [this.options.author];
}
if (_.isEmpty(this.options.author)) {
this.options.author = ["anonymous"];
}
if (!this.options.tempDir) {
this.options.tempDir = path.resolve(__dirname, "../tempDir/");
}
this.id = uuid();
this.uuid = path.resolve(this.options.tempDir, this.id);
this.options.uuid = this.uuid;
this.options.id = this.id;
this.options.images = [];
this.options.content = _.map(this.options.content, function(content, index) {
var $, allowedAttributes, allowedXhtml11Tags, titleSlug;
if (!content.filename) {
titleSlug = uslug(removeDiacritics(content.title || "no title"));
content.href = `${index}_${titleSlug}.xhtml`;
content.filePath = path.resolve(self.uuid, `./OEBPS/${index}_${titleSlug}.xhtml`);
} else {
content.href = content.filename.match(/\.xhtml$/) ? content.filename : `${content.filename}.xhtml`;
if (content.filename.match(/\.xhtml$/)) {
content.filePath = path.resolve(self.uuid, `./OEBPS/${content.filename}`);
} else {
content.filePath = path.resolve(self.uuid, `./OEBPS/${content.filename}.xhtml`);
}
}
content.id = `item_${index}`;
content.dir = path.dirname(content.filePath);
content.excludeFromToc || (content.excludeFromToc = false);
content.beforeToc || (content.beforeToc = false);
//fix Author Array
content.author = content.author && _.isString(content.author) ? [content.author] : !content.author || !_.isArray(content.author) ? [] : content.author;
allowedAttributes = ["content", "alt", "id", "title", "src", "href", "about", "accesskey", "aria-activedescendant", "aria-atomic", "aria-autocomplete", "aria-busy", "aria-checked", "aria-controls", "aria-describedat", "aria-describedby", "aria-disabled", "aria-dropeffect", "aria-expanded", "aria-flowto", "aria-grabbed", "aria-haspopup", "aria-hidden", "aria-invalid", "aria-label", "aria-labelledby", "aria-level", "aria-live", "aria-multiline", "aria-multiselectable", "aria-orientation", "aria-owns", "aria-posinset", "aria-pressed", "aria-readonly", "aria-relevant", "aria-required", "aria-selected", "aria-setsize", "aria-sort", "aria-valuemax", "aria-valuemin", "aria-valuenow", "aria-valuetext", "class", "content", "contenteditable", "contextmenu", "datatype", "dir", "draggable", "dropzone", "hidden", "hreflang", "id", "inlist", "itemid", "itemref", "itemscope", "itemtype", "lang", "media", "ns1:type", "ns2:alphabet", "ns2:ph", "onabort", "onblur", "oncanplay", "oncanplaythrough", "onchange", "onclick", "oncontextmenu", "ondblclick", "ondrag", "ondragend", "ondragenter", "ondragleave", "ondragover", "ondragstart", "ondrop", "ondurationchange", "onemptied", "onended", "onerror", "onfocus", "oninput", "oninvalid", "onkeydown", "onkeypress", "onkeyup", "onload", "onloadeddata", "onloadedmetadata", "onloadstart", "onmousedown", "onmousemove", "onmouseout", "onmouseover", "onmouseup", "onmousewheel", "onpause", "onplay", "onplaying", "onprogress", "onratechange", "onreadystatechange", "onreset", "onscroll", "onseeked", "onseeking", "onselect", "onshow", "onstalled", "onsubmit", "onsuspend", "ontimeupdate", "onvolumechange", "onwaiting", "prefix", "property", "rel", "resource", "rev", "role", "spellcheck", "style", "tabindex", "target", "title", "type", "typeof", "vocab", "xml:base", "xml:lang", "xml:space", "colspan", "rowspan", "epub:type", "epub:prefix"];
allowedXhtml11Tags = ["div", "p", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "li", "dl", "dt", "dd", "address", "hr", "pre", "blockquote", "center", "ins", "del", "a", "span", "bdo", "br", "em", "strong", "dfn", "code", "samp", "kbd", "bar", "cite", "abbr", "acronym", "q", "sub", "sup", "tt", "i", "b", "big", "small", "u", "s", "strike", "basefont", "font", "object", "param", "img", "table", "caption", "colgroup", "col", "thead", "tfoot", "tbody", "tr", "th", "td", "embed", "applet", "iframe", "img", "map", "noscript", "ns:svg", "object", "script", "table", "tt", "var"];
$ = cheerio.load(content.data, {
lowerCaseTags: true,
recognizeSelfClosing: true
});
// Only body innerHTML is allowed
if ($("body").length) {
$ = cheerio.load($("body").html(), {
lowerCaseTags: true,
recognizeSelfClosing: true
});
}
$($("*").get().reverse()).each(function(elemIndex, elem) {
var attrs, child, k, ref, ref1, that, v;
attrs = elem.attribs;
that = this;
if ((ref = that.name) === "img" || ref === "br" || ref === "hr") {
if (that.name === "img") {
$(that).attr("alt", $(that).attr("alt") || "image-placeholder");
}
}
for (k in attrs) {
v = attrs[k];
if (indexOf.call(allowedAttributes, k) >= 0) {
if (k === "type") {
if (that.name !== "script") {
$(that).removeAttr(k);
}
}
} else {
$(that).removeAttr(k);
}
}
if (self.options.version === 2) {
if (ref1 = that.name, indexOf.call(allowedXhtml11Tags, ref1) >= 0) {
} else {
console.log("Warning (content[" + index + "]):", that.name, "tag isn't allowed on EPUB 2/XHTML 1.1 DTD.");
child = $(that).html();
return $(that).replaceWith($("<div>" + child + "</div>"));
}
}
});
$("img").each(function(index, elem) {
var dir, extension, id, image, mediaType, url;
url = $(elem).attr("src");
if (image = self.options.images.find(function(element) {
return element.url === url;
})) {
id = image.id;
extension = image.extension;
} else {
id = uuid();
mediaType = mime.getType(url.replace(/\?.*/, ""));
extension = mime.getExtension(mediaType);
dir = content.dir;
self.options.images.push({id, url, dir, mediaType, extension});
}
return $(elem).attr("src", `images/${id}.${extension}`);
});
content.data = $.xml();
return content;
});
if (this.options.cover) {
this.options._coverMediaType = mime.getType(this.options.cover);
this.options._coverExtension = mime.getExtension(this.options._coverMediaType);
}
this.render();
this.promise = this.defer.promise;
}
render() {
var self;
self = this;
if (self.options.verbose) {
console.log("Generating Template Files.....");
}
return this.generateTempFile().then(function() {
if (self.options.verbose) {
console.log("Downloading Images...");
}
return self.downloadAllImage().fin(function() {
if (self.options.verbose) {
console.log("Making Cover...");
}
return self.makeCover().then(function() {
if (self.options.verbose) {
console.log("Generating Epub Files...");
}
return self.genEpub().then(function(result) {
if (self.options.verbose) {
console.log("About to finish...");
}
self.defer.resolve(result);
if (self.options.verbose) {
return console.log("Done.");
}
}, function(err) {
return self.defer.reject(err);
});
}, function(err) {
return self.defer.reject(err);
});
}, function(err) {
return self.defer.reject(err);
});
}, function(err) {
return self.defer.reject(err);
});
}
generateTempFile() {
var base, generateDefer, htmlTocPath, ncxTocPath, opfPath, self;
generateDefer = new Q.defer();
self = this;
if (!fs.existsSync(this.options.tempDir)) {
fs.mkdirSync(this.options.tempDir);
}
fs.mkdirSync(this.uuid);
fs.mkdirSync(path.resolve(this.uuid, "./OEBPS"));
(base = this.options).css || (base.css = fs.readFileSync(path.resolve(__dirname, "../templates/template.css")));
fs.writeFileSync(path.resolve(this.uuid, "./OEBPS/style.css"), this.options.css);
if (self.options.fonts.length) {
fs.mkdirSync(path.resolve(this.uuid, "./OEBPS/fonts"));
this.options.fonts = _.map(this.options.fonts, function(font) {
var filename;
if (!fs.existsSync(font)) {
generateDefer.reject(new Error('Custom font not found at ' + font + '.'));
return generateDefer.promise;
}
filename = path.basename(font);
fsextra.copySync(font, path.resolve(self.uuid, "./OEBPS/fonts/" + filename));
return filename;
});
}
_.each(this.options.content, function(content) {
var data;
data = `${self.options.docHeader}\n <head>\n <meta charset="UTF-8" />\n <title>${entities.encodeXML(content.title || '')}</title>\n <link rel="stylesheet" type="text/css" href="style.css" />\n </head>\n<body>`;
data += content.title && self.options.appendChapterTitles ? `<h1>${entities.encodeXML(content.title)}</h1>` : "";
data += content.title && content.author && content.author.length ? `<p class='epub-author'>${entities.encodeXML(content.author.join(", "))}</p>` : "";
data += content.title && content.url ? `<p class='epub-link'><a href='${content.url}'>${content.url}</a></p>` : "";
data += `${content.data}</body></html>`;
return fs.writeFileSync(content.filePath, data);
});
// write meta-inf/container.xml
fs.mkdirSync(this.uuid + "/META-INF");
fs.writeFileSync(`${this.uuid}/META-INF/container.xml`, "<?xml version=\"1.0\" encoding=\"UTF-8\" ?><container version=\"1.0\" xmlns=\"urn:oasis:names:tc:opendocument:xmlns:container\"><rootfiles><rootfile full-path=\"OEBPS/content.opf\" media-type=\"application/oebps-package+xml\"/></rootfiles></container>");
if (self.options.version === 2) {
// write meta-inf/com.apple.ibooks.display-options.xml [from pedrosanta:xhtml#6]
fs.writeFileSync(`${this.uuid}/META-INF/com.apple.ibooks.display-options.xml`, "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<display_options>\n <platform name=\"*\">\n <option name=\"specified-fonts\">true</option>\n </platform>\n</display_options>");
}
opfPath = self.options.customOpfTemplatePath || path.resolve(__dirname, `../templates/epub${self.options.version}/content.opf.ejs`);
if (!fs.existsSync(opfPath)) {
generateDefer.reject(new Error('Custom file to OPF template not found.'));
return generateDefer.promise;
}
ncxTocPath = self.options.customNcxTocTemplatePath || path.resolve(__dirname, "../templates/toc.ncx.ejs");
if (!fs.existsSync(ncxTocPath)) {
generateDefer.reject(new Error('Custom file the NCX toc template not found.'));
return generateDefer.promise;
}
htmlTocPath = self.options.customHtmlTocTemplatePath || path.resolve(__dirname, `../templates/epub${self.options.version}/toc.xhtml.ejs`);
if (!fs.existsSync(htmlTocPath)) {
generateDefer.reject(new Error('Custom file to HTML toc template not found.'));
return generateDefer.promise;
}
Q.all([Q.nfcall(ejs.renderFile, opfPath, self.options), Q.nfcall(ejs.renderFile, ncxTocPath, self.options), Q.nfcall(ejs.renderFile, htmlTocPath, self.options)]).spread(function(data1, data2, data3) {
fs.writeFileSync(path.resolve(self.uuid, "./OEBPS/content.opf"), data1);
fs.writeFileSync(path.resolve(self.uuid, "./OEBPS/toc.ncx"), data2);
fs.writeFileSync(path.resolve(self.uuid, "./OEBPS/toc.xhtml"), data3);
return generateDefer.resolve();
}, function(err) {
console.error(arguments);
return generateDefer.reject(err);
});
return generateDefer.promise;
}
makeCover() {
var coverDefer, destPath, userAgent, writeStream;
userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.116 Safari/537.36";
coverDefer = new Q.defer();
if (this.options.cover) {
destPath = path.resolve(this.uuid, "./OEBPS/cover." + this.options._coverExtension);
writeStream = null;
if (this.options.cover.slice(0, 4) === "http") {
writeStream = request.get(this.options.cover).set({
'User-Agent': userAgent
});
writeStream.pipe(fs.createWriteStream(destPath));
} else {
writeStream = fs.createReadStream(this.options.cover);
writeStream.pipe(fs.createWriteStream(destPath));
}
writeStream.on("end", function() {
console.log("[Success] cover image downloaded successfully!");
return coverDefer.resolve();
});
writeStream.on("error", function(err) {
console.error("Error", err);
return coverDefer.reject(err);
});
} else {
coverDefer.resolve();
}
return coverDefer.promise;
}
downloadImage(options) { //{id, url, mediaType}
var auxpath, downloadImageDefer, filename, requestAction, self, userAgent;
self = this;
userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.116 Safari/537.36";
if (!options.url && typeof options !== "string") {
return false;
}
downloadImageDefer = new Q.defer();
filename = path.resolve(self.uuid, "./OEBPS/images/" + options.id + "." + options.extension);
if (options.url.indexOf("file://") === 0) {
auxpath = options.url.substr(7);
fsextra.copySync(auxpath, filename);
return downloadImageDefer.resolve(options);
} else {
if (options.url.indexOf("http") === 0) {
requestAction = request.get(options.url).set({
'User-Agent': userAgent
});
requestAction.pipe(fs.createWriteStream(filename));
} else {
requestAction = fs.createReadStream(path.resolve(options.dir, options.url));
requestAction.pipe(fs.createWriteStream(filename));
}
requestAction.on('error', function(err) {
console.error('[Download Error]', 'Error while downloading', options.url, err);
fs.unlinkSync(filename);
return downloadImageDefer.reject(err);
});
requestAction.on('end', function() {
console.log("[Download Success]", options.url);
return downloadImageDefer.resolve(options);
});
return downloadImageDefer.promise;
}
}
downloadAllImage() {
var deferArray, imgDefer, self;
self = this;
imgDefer = new Q.defer();
if (!self.options.images.length) {
imgDefer.resolve();
} else {
fs.mkdirSync(path.resolve(this.uuid, "./OEBPS/images"));
deferArray = [];
_.each(self.options.images, function(image) {
return deferArray.push(self.downloadImage(image));
});
Q.all(deferArray).fin(function() {
return imgDefer.resolve();
});
}
return imgDefer.promise;
}
genEpub() {
var archive, cwd, genDefer, output, self;
// Thanks to Paul Bradley
// http://www.bradleymedia.org/gzip-markdown-epub/ (404 as of 28.07.2016)
// Web Archive URL:
// http://web.archive.org/web/20150521053611/http://www.bradleymedia.org/gzip-markdown-epub
// or Gist:
// https://gist.github.com/cyrilis/8d48eef37fbc108869ac32eb3ef97bca
genDefer = new Q.defer();
self = this;
cwd = this.uuid;
archive = archiver("zip", {
zlib: {
level: 9
}
});
output = fs.createWriteStream(self.options.output);
console.log("Zipping temp dir to", self.options.output);
archive.append("application/epub+zip", {
store: true,
name: "mimetype"
});
archive.directory(cwd + "/META-INF", "META-INF");
archive.directory(cwd + "/OEBPS", "OEBPS");
archive.pipe(output);
archive.on("end", function() {
console.log("Done zipping, clearing temp dir...");
return rimraf(cwd, function(err) {
if (err) {
return genDefer.reject(err);
} else {
return genDefer.resolve();
}
});
});
archive.on("error", function(err) {
return genDefer.reject(err);
});
archive.finalize();
return genDefer.promise;
}
};
module.exports = EPub;
}).call(this);