Using JSZip and make everything a promise
This commit is contained in:
parent
446bf3d5d8
commit
1c21bb3534
67
epub.d.ts
vendored
67
epub.d.ts
vendored
@ -1,66 +1 @@
|
||||
// Type definitions for epub
|
||||
// Project: https://github.com/julien-c/epub
|
||||
// Definitions by: Julien Chaumond <https://github.com/julien-c>
|
||||
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
||||
|
||||
/// <reference path="../node/node.d.ts" />
|
||||
|
||||
/**
|
||||
* new EPub(fname[, imageroot][, linkroot])
|
||||
* - fname (String): filename for the ebook
|
||||
* - imageroot (String): URL prefix for images
|
||||
* - linkroot (String): URL prefix for links
|
||||
*
|
||||
* Creates an Event Emitter type object for parsing epub files
|
||||
*
|
||||
* var epub = new EPub("book.epub");
|
||||
* epub.on("end", function () {
|
||||
* console.log(epub.spine);
|
||||
* });
|
||||
* epub.on("error", function (error) { ... });
|
||||
* epub.parse();
|
||||
*
|
||||
* Image and link URL format is:
|
||||
*
|
||||
* imageroot + img_id + img_zip_path
|
||||
*
|
||||
* So an image "logo.jpg" which resides in "OPT/" in the zip archive
|
||||
* and is listed in the manifest with id "logo_img" will have the
|
||||
* following url (providing that imageroot is "/images/"):
|
||||
*
|
||||
* /images/logo_img/OPT/logo.jpg
|
||||
**/
|
||||
declare module "epub" {
|
||||
|
||||
import {EventEmitter} from "events";
|
||||
|
||||
interface TocElement {
|
||||
level: number;
|
||||
order: number;
|
||||
title: string;
|
||||
id: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
class EPub extends EventEmitter {
|
||||
constructor(epubfile: string, imagewebroot?: string, chapterwebroot?: string);
|
||||
|
||||
metadata: Object;
|
||||
manifest: Object;
|
||||
spine: Object;
|
||||
flow: Array<Object>;
|
||||
toc: Array<TocElement>;
|
||||
|
||||
parse(): void;
|
||||
|
||||
getChapter(chapterId: string, callback: (error: Error, text: string) => void): void;
|
||||
|
||||
getChapterRaw(chapterId: string, callback: (error: Error, text: string) => void): void;
|
||||
|
||||
getImage(id: string, callback: (error: Error, data: Buffer, mimeType: string) => void): void;
|
||||
|
||||
getFile(id: string, callback: (error: Error, data: Buffer, mimeType: string) => void): void;
|
||||
}
|
||||
|
||||
export = EPub;
|
||||
}
|
||||
export {};
|
||||
|
770
epub.js
770
epub.js
@ -1,770 +0,0 @@
|
||||
var xml2js = require('xml2js');
|
||||
var xml2jsOptions = xml2js.defaults['0.1'];
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
|
||||
try {
|
||||
// zipfile is an optional dependency:
|
||||
var ZipFile = require("zipfile").ZipFile;
|
||||
} catch (err) {
|
||||
// Mock zipfile using pure-JS adm-zip:
|
||||
var AdmZip = require('adm-zip');
|
||||
|
||||
var ZipFile = function(filename) {
|
||||
this.admZip = new AdmZip(filename);
|
||||
this.names = this.admZip.getEntries().map(function(zipEntry) {
|
||||
return zipEntry.entryName;
|
||||
});
|
||||
this.count = this.names.length;
|
||||
};
|
||||
ZipFile.prototype.readFile = function(name, cb) {
|
||||
this.admZip.readFileAsync(this.admZip.getEntry(name), function(buffer, error) {
|
||||
// `error` is bogus right now, so let's just drop it.
|
||||
// see https://github.com/cthackers/adm-zip/pull/88
|
||||
return cb(null, buffer);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
//TODO: Cache parsed data
|
||||
|
||||
/**
|
||||
* new EPub(fname[, imageroot][, linkroot])
|
||||
* - fname (String): filename for the ebook
|
||||
* - imageroot (String): URL prefix for images
|
||||
* - linkroot (String): URL prefix for links
|
||||
*
|
||||
* Creates an Event Emitter type object for parsing epub files
|
||||
*
|
||||
* var epub = new EPub("book.epub");
|
||||
* epub.on("end", function () {
|
||||
* console.log(epub.spine);
|
||||
* });
|
||||
* epub.on("error", function (error) { ... });
|
||||
* epub.parse();
|
||||
*
|
||||
* Image and link URL format is:
|
||||
*
|
||||
* imageroot + img_id + img_zip_path
|
||||
*
|
||||
* So an image "logo.jpg" which resides in "OPT/" in the zip archive
|
||||
* and is listed in the manifest with id "logo_img" will have the
|
||||
* following url (providing that imageroot is "/images/"):
|
||||
*
|
||||
* /images/logo_img/OPT/logo.jpg
|
||||
**/
|
||||
class EPub extends EventEmitter {
|
||||
constructor(fname, imageroot, linkroot) {
|
||||
super();
|
||||
|
||||
this.filename = fname;
|
||||
|
||||
this.imageroot = (imageroot || "/images/").trim();
|
||||
this.linkroot = (linkroot || "/links/").trim();
|
||||
|
||||
if (this.imageroot.substr(-1) != "/") {
|
||||
this.imageroot += "/";
|
||||
}
|
||||
if (this.linkroot.substr(-1) != "/") {
|
||||
this.linkroot += "/";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EPub#parse() -> undefined
|
||||
*
|
||||
* Starts the parser, needs to be called by the script
|
||||
**/
|
||||
parse() {
|
||||
this.containerFile = false;
|
||||
this.mimeFile = false;
|
||||
this.rootFile = false;
|
||||
|
||||
this.metadata = {};
|
||||
this.manifest = {};
|
||||
this.spine = {toc: false, contents: []};
|
||||
this.flow = [];
|
||||
this.toc = [];
|
||||
|
||||
this.open();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* EPub#open() -> undefined
|
||||
*
|
||||
* Opens the epub file with Zip unpacker, retrieves file listing
|
||||
* and runs mime type check
|
||||
**/
|
||||
open() {
|
||||
try {
|
||||
this.zip = new ZipFile(this.filename);
|
||||
} catch (E) {
|
||||
this.emit("error", new Error("Invalid/missing file"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.zip.names || !this.zip.names.length) {
|
||||
this.emit("error", new Error("No files in archive"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.checkMimeType();
|
||||
};
|
||||
|
||||
/**
|
||||
* EPub#checkMimeType() -> undefined
|
||||
*
|
||||
* Checks if there's a file called "mimetype" and that it's contents
|
||||
* are "application/epub+zip". On success runs root file check.
|
||||
**/
|
||||
checkMimeType() {
|
||||
var i, len;
|
||||
|
||||
for (i = 0, len = this.zip.names.length; i < len; i++) {
|
||||
if (this.zip.names[i].toLowerCase() == "mimetype") {
|
||||
this.mimeFile = this.zip.names[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!this.mimeFile) {
|
||||
this.emit("error", new Error("No mimetype file in archive"));
|
||||
return;
|
||||
}
|
||||
this.zip.readFile(this.mimeFile, (function (err, data) {
|
||||
if (err) {
|
||||
this.emit("error", new Error("Reading archive failed"));
|
||||
return;
|
||||
}
|
||||
var txt = data.toString("utf-8").toLowerCase().trim();
|
||||
|
||||
if (txt != "application/epub+zip") {
|
||||
this.emit("error", new Error("Unsupported mime type"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.getRootFiles();
|
||||
}).bind(this));
|
||||
};
|
||||
|
||||
/**
|
||||
* EPub#getRootFiles() -> undefined
|
||||
*
|
||||
* Looks for a "meta-inf/container.xml" file and searches for a
|
||||
* rootfile element with mime type "application/oebps-package+xml".
|
||||
* On success calls the rootfile parser
|
||||
**/
|
||||
getRootFiles() {
|
||||
var i, len;
|
||||
for (i = 0, len = this.zip.names.length; i < len; i++) {
|
||||
if (this.zip.names[i].toLowerCase() == "meta-inf/container.xml") {
|
||||
this.containerFile = this.zip.names[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!this.containerFile) {
|
||||
this.emit("error", new Error("No container file in archive"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.zip.readFile(this.containerFile, (function (err, data) {
|
||||
if (err) {
|
||||
this.emit("error", new Error("Reading archive failed"));
|
||||
return;
|
||||
}
|
||||
var xml = data.toString("utf-8").toLowerCase().trim(),
|
||||
xmlparser = new xml2js.Parser(xml2jsOptions);
|
||||
|
||||
xmlparser.on("end", (function (result) {
|
||||
|
||||
if (!result.rootfiles || !result.rootfiles.rootfile) {
|
||||
this.emit("error", new Error("No rootfiles found"));
|
||||
console.dir(result);
|
||||
return;
|
||||
}
|
||||
|
||||
var rootfile = result.rootfiles.rootfile,
|
||||
filename = false, i, len;
|
||||
|
||||
if (Array.isArray(rootfile)) {
|
||||
|
||||
for (i = 0, len = rootfile.length; i < len; i++) {
|
||||
if (rootfile[i]["@"]["media-type"] &&
|
||||
rootfile[i]["@"]["media-type"] == "application/oebps-package+xml" &&
|
||||
rootfile[i]["@"]["full-path"]) {
|
||||
filename = rootfile[i]["@"]["full-path"].toLowerCase().trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} else if (rootfile["@"]) {
|
||||
if (rootfile["@"]["media-type"] != "application/oebps-package+xml" || !rootfile["@"]["full-path"]) {
|
||||
this.emit("error", new Error("Rootfile in unknown format"));
|
||||
return;
|
||||
}
|
||||
filename = rootfile["@"]["full-path"].toLowerCase().trim();
|
||||
}
|
||||
|
||||
if (!filename) {
|
||||
this.emit("error", new Error("Empty rootfile"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
for (i = 0, len = this.zip.names.length; i < len; i++) {
|
||||
if (this.zip.names[i].toLowerCase() == filename) {
|
||||
this.rootFile = this.zip.names[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.rootFile) {
|
||||
this.emit("error", new Error("Rootfile not found from archive"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.handleRootFile();
|
||||
|
||||
}).bind(this));
|
||||
|
||||
xmlparser.on("error", (function (err) {
|
||||
this.emit("error", new Error("Parsing container XML failed"));
|
||||
return;
|
||||
}).bind(this));
|
||||
|
||||
xmlparser.parseString(xml);
|
||||
|
||||
|
||||
}).bind(this));
|
||||
};
|
||||
|
||||
/**
|
||||
* EPub#handleRootFile() -> undefined
|
||||
*
|
||||
* Parses the rootfile XML and calls rootfile parser
|
||||
**/
|
||||
handleRootFile() {
|
||||
|
||||
this.zip.readFile(this.rootFile, (function (err, data) {
|
||||
if (err) {
|
||||
this.emit("error", new Error("Reading archive failed"));
|
||||
return;
|
||||
}
|
||||
var xml = data.toString("utf-8"),
|
||||
xmlparser = new xml2js.Parser(xml2jsOptions);
|
||||
|
||||
xmlparser.on("end", this.parseRootFile.bind(this));
|
||||
|
||||
xmlparser.on("error", (function (err) {
|
||||
this.emit("error", new Error("Parsing container XML failed"));
|
||||
return;
|
||||
}).bind(this));
|
||||
|
||||
xmlparser.parseString(xml);
|
||||
|
||||
}).bind(this));
|
||||
};
|
||||
|
||||
/**
|
||||
* EPub#parseRootFile() -> undefined
|
||||
*
|
||||
* Parses elements "metadata," "manifest," "spine" and TOC.
|
||||
* Emits "end" if no TOC
|
||||
**/
|
||||
parseRootFile(rootfile) {
|
||||
|
||||
this.version = rootfile['@'].version || '2.0';
|
||||
|
||||
var i, len, keys, keyparts, key;
|
||||
keys = Object.keys(rootfile);
|
||||
for (i = 0, len = keys.length; i < len; i++) {
|
||||
keyparts = keys[i].split(":");
|
||||
key = (keyparts.pop() || "").toLowerCase().trim();
|
||||
switch (key) {
|
||||
case "metadata":
|
||||
this.parseMetadata(rootfile[keys[i]]);
|
||||
break;
|
||||
case "manifest":
|
||||
this.parseManifest(rootfile[keys[i]]);
|
||||
break;
|
||||
case "spine":
|
||||
this.parseSpine(rootfile[keys[i]]);
|
||||
break;
|
||||
case "guide":
|
||||
//this.parseGuide(rootfile[keys[i]]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.spine.toc) {
|
||||
this.parseTOC();
|
||||
} else {
|
||||
this.emit("end");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* EPub#parseMetadata() -> undefined
|
||||
*
|
||||
* Parses "metadata" block (book metadata, title, author etc.)
|
||||
**/
|
||||
parseMetadata(metadata) {
|
||||
var i, j, len, keys, keyparts, key;
|
||||
|
||||
keys = Object.keys(metadata);
|
||||
for (i = 0, len = keys.length; i < len; i++) {
|
||||
keyparts = keys[i].split(":");
|
||||
key = (keyparts.pop() || "").toLowerCase().trim();
|
||||
switch (key) {
|
||||
case "publisher":
|
||||
if (Array.isArray(metadata[keys[i]])) {
|
||||
this.metadata.publisher = String(metadata[keys[i]][0] && metadata[keys[i]][0]["#"] || metadata[keys[i]][0] || "").trim();
|
||||
} else {
|
||||
this.metadata.publisher = String(metadata[keys[i]]["#"] || metadata[keys[i]] || "").trim();
|
||||
}
|
||||
break;
|
||||
case "language":
|
||||
if (Array.isArray(metadata[keys[i]])) {
|
||||
this.metadata.language = String(metadata[keys[i]][0] && metadata[keys[i]][0]["#"] || metadata[keys[i]][0] || "").toLowerCase().trim();
|
||||
} else {
|
||||
this.metadata.language = String(metadata[keys[i]]["#"] || metadata[keys[i]] || "").toLowerCase().trim();
|
||||
}
|
||||
break;
|
||||
case "title":
|
||||
if (Array.isArray(metadata[keys[i]])) {
|
||||
this.metadata.title = String(metadata[keys[i]][0] && metadata[keys[i]][0]["#"] || metadata[keys[i]][0] || "").trim();
|
||||
} else {
|
||||
this.metadata.title = String(metadata[keys[i]]["#"] || metadata[keys[i]] || "").trim();
|
||||
}
|
||||
break;
|
||||
case "subject":
|
||||
if (Array.isArray(metadata[keys[i]])) {
|
||||
this.metadata.subject = String(metadata[keys[i]][0] && metadata[keys[i]][0]["#"] || metadata[keys[i]][0] || "").trim();
|
||||
} else {
|
||||
this.metadata.subject = String(metadata[keys[i]]["#"] || metadata[keys[i]] || "").trim();
|
||||
}
|
||||
break;
|
||||
case "description":
|
||||
if (Array.isArray(metadata[keys[i]])) {
|
||||
this.metadata.description = String(metadata[keys[i]][0] && metadata[keys[i]][0]["#"] || metadata[keys[i]][0] || "").trim();
|
||||
} else {
|
||||
this.metadata.description = String(metadata[keys[i]]["#"] || metadata[keys[i]] || "").trim();
|
||||
}
|
||||
break;
|
||||
case "creator":
|
||||
if (Array.isArray(metadata[keys[i]])) {
|
||||
this.metadata.creator = String(metadata[keys[i]][0] && metadata[keys[i]][0]["#"] || metadata[keys[i]][0] || "").trim();
|
||||
this.metadata.creatorFileAs = String(metadata[keys[i]][0] && metadata[keys[i]][0]['@'] && metadata[keys[i]][0]['@']["opf:file-as"] || this.metadata.creator).trim();
|
||||
} else {
|
||||
this.metadata.creator = String(metadata[keys[i]]["#"] || metadata[keys[i]] || "").trim();
|
||||
this.metadata.creatorFileAs = String(metadata[keys[i]]['@'] && metadata[keys[i]]['@']["opf:file-as"] || this.metadata.creator).trim();
|
||||
}
|
||||
break;
|
||||
case "date":
|
||||
if (Array.isArray(metadata[keys[i]])) {
|
||||
this.metadata.date = String(metadata[keys[i]][0] && metadata[keys[i]][0]["#"] || metadata[keys[i]][0] || "").trim();
|
||||
} else {
|
||||
this.metadata.date = String(metadata[keys[i]]["#"] || metadata[keys[i]] || "").trim();
|
||||
}
|
||||
break;
|
||||
case "identifier":
|
||||
if (metadata[keys[i]]["@"] && metadata[keys[i]]["@"]["opf:scheme"] == "ISBN") {
|
||||
this.metadata.ISBN = String(metadata[keys[i]]["#"] || "").trim();
|
||||
} else if (metadata[keys[i]]["@"] && metadata[keys[i]]["@"].id && metadata[keys[i]]["@"].id.match(/uuid/i)) {
|
||||
this.metadata.UUID = String(metadata[keys[i]]["#"] || "").replace('urn:uuid:', '').toUpperCase().trim();
|
||||
} else if (Array.isArray(metadata[keys[i]])) {
|
||||
for (j = 0; j < metadata[keys[i]].length; j++) {
|
||||
if (metadata[keys[i]][j]["@"]) {
|
||||
if (metadata[keys[i]][j]["@"]["opf:scheme"] == "ISBN") {
|
||||
this.metadata.ISBN = String(metadata[keys[i]][j]["#"] || "").trim();
|
||||
} else if (metadata[keys[i]][j]["@"].id && metadata[keys[i]][j]["@"].id.match(/uuid/i)) {
|
||||
this.metadata.UUID = String(metadata[keys[i]][j]["#"] || "").replace('urn:uuid:', '').toUpperCase().trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var metas = metadata['meta'] || {};
|
||||
Object.keys(metas).forEach(function(key) {
|
||||
var meta = metas[key];
|
||||
if (meta['@'] && meta['@'].name) {
|
||||
var name = meta['@'].name;
|
||||
this.metadata[name] = meta['@'].content;
|
||||
}
|
||||
if (meta['#'] && meta['@'].property) {
|
||||
this.metadata[meta['@'].property] = meta['#'];
|
||||
}
|
||||
|
||||
if(meta.name && meta.name =="cover"){
|
||||
this.metadata[meta.name] = meta.content;
|
||||
}
|
||||
}, this);
|
||||
};
|
||||
|
||||
/**
|
||||
* EPub#parseManifest() -> undefined
|
||||
*
|
||||
* Parses "manifest" block (all items included, html files, images, styles)
|
||||
**/
|
||||
parseManifest(manifest) {
|
||||
var i, len, path = this.rootFile.split("/"), element, path_str;
|
||||
path.pop();
|
||||
path_str = path.join("/");
|
||||
|
||||
if (manifest.item) {
|
||||
for (i = 0, len = manifest.item.length; i < len; i++) {
|
||||
if (manifest.item[i]['@']) {
|
||||
element = manifest.item[i]['@'];
|
||||
|
||||
if (element.href && element.href.substr(0, path_str.length) != path_str) {
|
||||
element.href = path.concat([element.href]).join("/");
|
||||
}
|
||||
|
||||
this.manifest[manifest.item[i]['@'].id] = element;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* EPub#parseSpine() -> undefined
|
||||
*
|
||||
* Parses "spine" block (all html elements that are shown to the reader)
|
||||
**/
|
||||
parseSpine(spine) {
|
||||
var i, len, path = this.rootFile.split("/"), element;
|
||||
path.pop();
|
||||
|
||||
if (spine['@'] && spine['@'].toc) {
|
||||
this.spine.toc = this.manifest[spine['@'].toc] || false;
|
||||
}
|
||||
|
||||
if (spine.itemref) {
|
||||
if(!Array.isArray(spine.itemref)){
|
||||
spine.itemref = [spine.itemref];
|
||||
}
|
||||
for (i = 0, len = spine.itemref.length; i < len; i++) {
|
||||
if (spine.itemref[i]['@']) {
|
||||
if (element = this.manifest[spine.itemref[i]['@'].idref]) {
|
||||
this.spine.contents.push(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.flow = this.spine.contents;
|
||||
};
|
||||
|
||||
/**
|
||||
* EPub#parseTOC() -> undefined
|
||||
*
|
||||
* Parses ncx file for table of contents (title, html file)
|
||||
**/
|
||||
parseTOC() {
|
||||
var i, len, path = this.spine.toc.href.split("/"), id_list = {}, keys;
|
||||
path.pop();
|
||||
|
||||
keys = Object.keys(this.manifest);
|
||||
for (i = 0, len = keys.length; i < len; i++) {
|
||||
id_list[this.manifest[keys[i]].href] = keys[i];
|
||||
}
|
||||
|
||||
this.zip.readFile(this.spine.toc.href, (function (err, data) {
|
||||
if (err) {
|
||||
this.emit("error", new Error("Reading archive failed"));
|
||||
return;
|
||||
}
|
||||
var xml = data.toString("utf-8"),
|
||||
xmlparser = new xml2js.Parser(xml2jsOptions);
|
||||
|
||||
xmlparser.on("end", (function (result) {
|
||||
if (result.navMap && result.navMap.navPoint) {
|
||||
this.toc = this.walkNavMap(result.navMap.navPoint, path, id_list);
|
||||
}
|
||||
|
||||
this.emit("end");
|
||||
}).bind(this));
|
||||
|
||||
xmlparser.on("error", (function (err) {
|
||||
this.emit("error", new Error("Parsing container XML failed"));
|
||||
return;
|
||||
}).bind(this));
|
||||
|
||||
xmlparser.parseString(xml);
|
||||
|
||||
}).bind(this));
|
||||
};
|
||||
|
||||
/**
|
||||
* EPub#walkNavMap(branch, path, id_list,[, level]) -> Array
|
||||
* - branch (Array | Object): NCX NavPoint object
|
||||
* - path (Array): Base path
|
||||
* - id_list (Object): map of file paths and id values
|
||||
* - level (Number): deepness
|
||||
*
|
||||
* Walks the NavMap object through all levels and finds elements
|
||||
* for TOC
|
||||
**/
|
||||
walkNavMap(branch, path, id_list, level) {
|
||||
level = level || 0;
|
||||
|
||||
// don't go too far
|
||||
if (level > 7) {
|
||||
return [];
|
||||
}
|
||||
|
||||
var output = [];
|
||||
|
||||
if (!Array.isArray(branch)) {
|
||||
branch = [branch];
|
||||
}
|
||||
|
||||
for (var i = 0; i < branch.length; i++) {
|
||||
if (branch[i].navLabel) {
|
||||
|
||||
var title = '';
|
||||
if (branch[i].navLabel && typeof branch[i].navLabel.text == 'string') {
|
||||
title = branch[i].navLabel && branch[i].navLabel.text || branch[i].navLabel===branch[i].navLabel ?
|
||||
(branch[i].navLabel && branch[i].navLabel.text || branch[i].navLabel || "").trim() : '';
|
||||
}
|
||||
var order = Number(branch[i]["@"] && branch[i]["@"].playOrder || 0);
|
||||
if (isNaN(order)) {
|
||||
order = 0;
|
||||
}
|
||||
var href = '';
|
||||
if (branch[i].content && branch[i].content["@"] && typeof branch[i].content["@"].src == 'string') {
|
||||
href = branch[i].content["@"].src.trim();
|
||||
}
|
||||
|
||||
var element = {
|
||||
level: level,
|
||||
order: order,
|
||||
title: title
|
||||
};
|
||||
|
||||
if (href) {
|
||||
href = path.concat([href]).join("/");
|
||||
element.href = href;
|
||||
|
||||
if (id_list[element.href]) {
|
||||
// link existing object
|
||||
element = this.manifest[id_list[element.href]];
|
||||
element.title = title;
|
||||
element.order = order;
|
||||
element.level = level;
|
||||
} else {
|
||||
// use new one
|
||||
element.href = href;
|
||||
element.id = (branch[i]["@"] && branch[i]["@"].id || "").trim();
|
||||
}
|
||||
|
||||
output.push(element);
|
||||
}
|
||||
}
|
||||
if (branch[i].navPoint) {
|
||||
output = output.concat(this.walkNavMap(branch[i].navPoint, path, id_list, level + 1));
|
||||
}
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
/**
|
||||
* EPub#getChapter(id, callback) -> undefined
|
||||
* - id (String): Manifest id value for a chapter
|
||||
* - callback (Function): callback function
|
||||
*
|
||||
* Finds a chapter text for an id. Replaces image and link URL's, removes
|
||||
* <head> etc. elements. Return only chapters with mime type application/xhtml+xml
|
||||
**/
|
||||
getChapter(id, callback) {
|
||||
this.getChapterRaw(id, (function (err, str) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
var i, len, path = this.rootFile.split("/"), keys = Object.keys(this.manifest);
|
||||
path.pop();
|
||||
|
||||
// remove linebreaks (no multi line matches in JS regex!)
|
||||
str = str.replace(/\r?\n/g, "\u0000");
|
||||
|
||||
// keep only <body> contents
|
||||
str.replace(/<body[^>]*?>(.*)<\/body[^>]*?>/i, function (o, d) {
|
||||
str = d.trim();
|
||||
});
|
||||
|
||||
// remove <script> blocks if any
|
||||
str = str.replace(/<script[^>]*?>(.*?)<\/script[^>]*?>/ig, function (o, s) {
|
||||
return "";
|
||||
});
|
||||
|
||||
// remove <style> blocks if any
|
||||
str = str.replace(/<style[^>]*?>(.*?)<\/style[^>]*?>/ig, function (o, s) {
|
||||
return "";
|
||||
});
|
||||
|
||||
// remove onEvent handlers
|
||||
str = str.replace(/(\s)(on\w+)(\s*=\s*["']?[^"'\s>]*?["'\s>])/g, function (o, a, b, c) {
|
||||
return a + "skip-" + b + c;
|
||||
});
|
||||
|
||||
// replace images
|
||||
str = str.replace(/(\ssrc\s*=\s*["']?)([^"'\s>]*?)(["'\s>])/g, (function (o, a, b, c) {
|
||||
var img = path.concat([b]).join("/").trim(),
|
||||
element;
|
||||
|
||||
for (i = 0, len = keys.length; i < len; i++) {
|
||||
if (this.manifest[keys[i]].href == img) {
|
||||
element = this.manifest[keys[i]];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// include only images from manifest
|
||||
if (element) {
|
||||
return a + this.imageroot + element.id + "/" + img + c;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
|
||||
}).bind(this));
|
||||
|
||||
// replace links
|
||||
str = str.replace(/(\shref\s*=\s*["']?)([^"'\s>]*?)(["'\s>])/g, (function (o, a, b, c) {
|
||||
var linkparts = b && b.split("#"),
|
||||
link = path.concat([(linkparts.shift() || "")]).join("/").trim(),
|
||||
element;
|
||||
|
||||
for (i = 0, len = keys.length; i < len; i++) {
|
||||
if (this.manifest[keys[i]].href.split("#")[0] == link) {
|
||||
element = this.manifest[keys[i]];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (linkparts.length) {
|
||||
link += "#" + linkparts.join("#");
|
||||
}
|
||||
|
||||
// include only images from manifest
|
||||
if (element) {
|
||||
return a + this.linkroot + element.id + "/" + link + c;
|
||||
} else {
|
||||
return a + b + c;
|
||||
}
|
||||
|
||||
}).bind(this));
|
||||
|
||||
// bring back linebreaks
|
||||
str = str.replace(/\u0000/g, "\n").trim();
|
||||
|
||||
callback(null, str);
|
||||
}).bind(this));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* EPub#getChapterRaw(id, callback) -> undefined
|
||||
* - id (String): Manifest id value for a chapter
|
||||
* - callback (Function): callback function
|
||||
*
|
||||
* Returns the raw chapter text for an id.
|
||||
**/
|
||||
getChapterRaw(id, callback) {
|
||||
if (this.manifest[id]) {
|
||||
|
||||
if (!(this.manifest[id]['media-type'] == "application/xhtml+xml" || this.manifest[id]['media-type'] == "image/svg+xml")) {
|
||||
return callback(new Error("Invalid mime type for chapter"));
|
||||
}
|
||||
|
||||
this.zip.readFile(this.manifest[id].href, (function (err, data) {
|
||||
if (err) {
|
||||
callback(new Error("Reading archive failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
var str = data.toString("utf-8");
|
||||
|
||||
callback(null, str);
|
||||
|
||||
}).bind(this));
|
||||
} else {
|
||||
callback(new Error("File not found"));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* EPub#getImage(id, callback) -> undefined
|
||||
* - id (String): Manifest id value for an image
|
||||
* - callback (Function): callback function
|
||||
*
|
||||
* Finds an image for an id. Returns the image as Buffer. Callback gets
|
||||
* an error object, image buffer and image content-type.
|
||||
* Return only images with mime type image
|
||||
**/
|
||||
getImage(id, callback) {
|
||||
if (this.manifest[id]) {
|
||||
|
||||
if ((this.manifest[id]['media-type'] || "").toLowerCase().trim().substr(0, 6) != "image/") {
|
||||
return callback(new Error("Invalid mime type for image"));
|
||||
}
|
||||
|
||||
this.getFile(id, callback);
|
||||
} else {
|
||||
callback(new Error("File not found"));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* EPub#getFile(id, callback) -> undefined
|
||||
* - id (String): Manifest id value for a file
|
||||
* - callback (Function): callback function
|
||||
*
|
||||
* Finds a file for an id. Returns the file as Buffer. Callback gets
|
||||
* an error object, file contents buffer and file content-type.
|
||||
**/
|
||||
getFile(id, callback) {
|
||||
if (this.manifest[id]) {
|
||||
|
||||
this.zip.readFile(this.manifest[id].href, (function (err, data) {
|
||||
if (err) {
|
||||
callback(new Error("Reading archive failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, data, this.manifest[id]['media-type']);
|
||||
}).bind(this));
|
||||
} else {
|
||||
callback(new Error("File not found"));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
readFile(filename, options, callback_) {
|
||||
var callback = arguments[arguments.length - 1];
|
||||
|
||||
if (typeof options === 'function' || !options) {
|
||||
this.zip.readFile(filename, callback);
|
||||
} else if (typeof options === 'string') {
|
||||
// options is an encoding
|
||||
this.zip.readFile(filename, function(err, data) {
|
||||
if (err) {
|
||||
callback(new Error('Reading archive failed'));
|
||||
return;
|
||||
}
|
||||
callback(null, data.toString(options));
|
||||
});
|
||||
} else {
|
||||
throw new TypeError('Bad arguments');
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
// Expose to the world
|
||||
module.exports = EPub;
|
168
lib/epub.d.ts
vendored
Normal file
168
lib/epub.d.ts
vendored
Normal file
@ -0,0 +1,168 @@
|
||||
import * as JSZip from "jszip";
|
||||
/**
|
||||
* new EPub(fname[, imageroot][, linkroot])
|
||||
* - fname (String): filename for the ebook
|
||||
* - imageroot (String): URL prefix for images
|
||||
* - linkroot (String): URL prefix for links
|
||||
*
|
||||
* Creates an Event Emitter type object for parsing epub files
|
||||
*
|
||||
* var epub = new EPub("book.epub");
|
||||
* epub.on("end", function () {
|
||||
* console.log(epub.spine);
|
||||
* });
|
||||
* epub.on("error", function (error) { ... });
|
||||
* epub.parse();
|
||||
*
|
||||
* Image and link URL format is:
|
||||
*
|
||||
* imageroot + img_id + img_zip_path
|
||||
*
|
||||
* So an image "logo.jpg" which resides in "OPT/" in the zip archive
|
||||
* and is listed in the manifest with id "logo_img" will have the
|
||||
* following url (providing that imageroot is "/images/"):
|
||||
*
|
||||
* /images/logo_img/OPT/logo.jpg
|
||||
**/
|
||||
export declare class EPub {
|
||||
zip: JSZip;
|
||||
filename: string;
|
||||
imageroot: string;
|
||||
linkroot: string;
|
||||
containerFile: string;
|
||||
mimeFile: string;
|
||||
rootFile: string;
|
||||
metadata: {
|
||||
publisher?: string;
|
||||
language?: string;
|
||||
title?: string;
|
||||
subject?: string;
|
||||
description?: string;
|
||||
creator?: string;
|
||||
creatorFileAs?: string;
|
||||
date?: string;
|
||||
ISBN?: string;
|
||||
UUID?: string;
|
||||
};
|
||||
manifest: {};
|
||||
spine: {
|
||||
toc: any;
|
||||
contents: any[];
|
||||
};
|
||||
flow: any[];
|
||||
toc: any[];
|
||||
version: string;
|
||||
constructor(fname: any, imageroot: any, linkroot: any);
|
||||
/**
|
||||
* EPub#parse() -> undefined
|
||||
*
|
||||
* Starts the parser, needs to be called by the script
|
||||
**/
|
||||
parse(): Promise<void>;
|
||||
/**
|
||||
* EPub#open() -> undefined
|
||||
*
|
||||
* Opens the epub file with Zip unpacker, retrieves file listing
|
||||
* and runs mime type check
|
||||
**/
|
||||
open(): Promise<void>;
|
||||
/**
|
||||
* EPub#checkMimeType() -> undefined
|
||||
*
|
||||
* Checks if there's a file called "mimetype" and that it's contents
|
||||
* are "application/epub+zip". On success runs root file check.
|
||||
**/
|
||||
checkMimeType(): Promise<void>;
|
||||
/**
|
||||
* EPub#getRootFiles() -> undefined
|
||||
*
|
||||
* Looks for a "meta-inf/container.xml" file and searches for a
|
||||
* rootfile element with mime type "application/oebps-package+xml".
|
||||
* On success calls the rootfile parser
|
||||
**/
|
||||
getRootFiles(): Promise<void>;
|
||||
/**
|
||||
* EPub#handleRootFile() -> undefined
|
||||
*
|
||||
* Parses the rootfile XML and calls rootfile parser
|
||||
**/
|
||||
handleRootFile(): Promise<void>;
|
||||
/**
|
||||
* EPub#parseRootFile() -> undefined
|
||||
*
|
||||
* Parses elements "metadata," "manifest," "spine" and TOC.
|
||||
* Emits "end" if no TOC
|
||||
**/
|
||||
parseRootFile(rootfile: any): Promise<void>;
|
||||
/**
|
||||
* EPub#parseMetadata() -> undefined
|
||||
*
|
||||
* Parses "metadata" block (book metadata, title, author etc.)
|
||||
**/
|
||||
parseMetadata(metadata: any): void;
|
||||
/**
|
||||
* EPub#parseManifest() -> undefined
|
||||
*
|
||||
* Parses "manifest" block (all items included, html files, images, styles)
|
||||
**/
|
||||
parseManifest(manifest: any): void;
|
||||
/**
|
||||
* EPub#parseSpine() -> undefined
|
||||
*
|
||||
* Parses "spine" block (all html elements that are shown to the reader)
|
||||
**/
|
||||
parseSpine(spine: any): void;
|
||||
/**
|
||||
* EPub#parseTOC() -> undefined
|
||||
*
|
||||
* Parses ncx file for table of contents (title, html file)
|
||||
**/
|
||||
parseTOC(): Promise<void>;
|
||||
/**
|
||||
* EPub#walkNavMap(branch, path, id_list,[, level]) -> Array
|
||||
* - branch (Array | Object): NCX NavPoint object
|
||||
* - path (Array): Base path
|
||||
* - id_list (Object): map of file paths and id values
|
||||
* - level (Number): deepness
|
||||
*
|
||||
* Walks the NavMap object through all levels and finds elements
|
||||
* for TOC
|
||||
**/
|
||||
walkNavMap(branch: any, path: any, id_list: any, level?: any): any[];
|
||||
/**
|
||||
* EPub#getChapter(id, callback) -> undefined
|
||||
* - id (String): Manifest id value for a chapter
|
||||
* - callback (Function): callback function
|
||||
*
|
||||
* Finds a chapter text for an id. Replaces image and link URL's, removes
|
||||
* <head> etc. elements. Return only chapters with mime type application/xhtml+xml
|
||||
**/
|
||||
getChapter(id: any): Promise<string>;
|
||||
/**
|
||||
* EPub#getChapterRaw(id, callback) -> undefined
|
||||
* - id (String): Manifest id value for a chapter
|
||||
* - callback (Function): callback function
|
||||
*
|
||||
* Returns the raw chapter text for an id.
|
||||
**/
|
||||
getChapterRaw(id: any): Promise<string>;
|
||||
/**
|
||||
* EPub#getImage(id, callback) -> undefined
|
||||
* - id (String): Manifest id value for an image
|
||||
* - callback (Function): callback function
|
||||
*
|
||||
* Finds an image for an id. Returns the image as Buffer. Callback gets
|
||||
* an error object, image buffer and image content-type.
|
||||
* Return only images with mime type image
|
||||
**/
|
||||
getImage(id: any): Promise<void>;
|
||||
/**
|
||||
* EPub#getFile(id, callback) -> undefined
|
||||
* - id (String): Manifest id value for a file
|
||||
* - callback (Function): callback function
|
||||
*
|
||||
* Finds a file for an id. Returns the file as Buffer. Callback gets
|
||||
* an error object, file contents buffer and file content-type.
|
||||
**/
|
||||
getFile(id: any): Promise<void>;
|
||||
}
|
651
lib/epub.js
Normal file
651
lib/epub.js
Normal file
@ -0,0 +1,651 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var xml2js = require('xml2js');
|
||||
var xml2jsOptions = xml2js.defaults['0.1'];
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
// try {
|
||||
// // zipfile is an optional dependency:
|
||||
// var ZipFile = require("zipfile").ZipFile;
|
||||
// } catch (err) {
|
||||
// // Mock zipfile using pure-JS adm-zip:
|
||||
// var AdmZip = require('adm-zip');
|
||||
// var ZipFile = function (filename) {
|
||||
// this.admZip = new AdmZip(filename);
|
||||
// this.names = this.admZip.getEntries().map(function (zipEntry) {
|
||||
// return zipEntry.entryName;
|
||||
// });
|
||||
// this.count = this.names.length;
|
||||
// };
|
||||
// ZipFile.prototype.readFile = function (name, cb) {
|
||||
// this.admZip.readFileAsync(this.admZip.getEntry(name), function (buffer, error) {
|
||||
// // `error` is bogus right now, so let's just drop it.
|
||||
// // see https://github.com/cthackers/adm-zip/pull/88
|
||||
// return cb(null, buffer);
|
||||
// });
|
||||
// };
|
||||
// }
|
||||
const fs = require("fs");
|
||||
const JSZip = require("jszip");
|
||||
//TODO: Cache parsed data
|
||||
/**
|
||||
* new EPub(fname[, imageroot][, linkroot])
|
||||
* - fname (String): filename for the ebook
|
||||
* - imageroot (String): URL prefix for images
|
||||
* - linkroot (String): URL prefix for links
|
||||
*
|
||||
* Creates an Event Emitter type object for parsing epub files
|
||||
*
|
||||
* var epub = new EPub("book.epub");
|
||||
* epub.on("end", function () {
|
||||
* console.log(epub.spine);
|
||||
* });
|
||||
* epub.on("error", function (error) { ... });
|
||||
* epub.parse();
|
||||
*
|
||||
* Image and link URL format is:
|
||||
*
|
||||
* imageroot + img_id + img_zip_path
|
||||
*
|
||||
* So an image "logo.jpg" which resides in "OPT/" in the zip archive
|
||||
* and is listed in the manifest with id "logo_img" will have the
|
||||
* following url (providing that imageroot is "/images/"):
|
||||
*
|
||||
* /images/logo_img/OPT/logo.jpg
|
||||
**/
|
||||
class EPub {
|
||||
constructor(fname, imageroot, linkroot) {
|
||||
this.metadata = {};
|
||||
this.manifest = {};
|
||||
this.spine = { toc: undefined, contents: [] };
|
||||
this.flow = [];
|
||||
this.toc = [];
|
||||
this.filename = fname;
|
||||
this.imageroot = (imageroot || "/images/").trim();
|
||||
this.linkroot = (linkroot || "/links/").trim();
|
||||
if (this.imageroot.substr(-1) != "/") {
|
||||
this.imageroot += "/";
|
||||
}
|
||||
if (this.linkroot.substr(-1) != "/") {
|
||||
this.linkroot += "/";
|
||||
}
|
||||
}
|
||||
/**
|
||||
* EPub#parse() -> undefined
|
||||
*
|
||||
* Starts the parser, needs to be called by the script
|
||||
**/
|
||||
async parse() {
|
||||
this.containerFile = undefined;
|
||||
this.mimeFile = undefined;
|
||||
this.rootFile = undefined;
|
||||
this.metadata = {};
|
||||
this.manifest = {};
|
||||
this.spine = { toc: undefined, contents: [] };
|
||||
this.flow = [];
|
||||
this.toc = [];
|
||||
await this.open();
|
||||
}
|
||||
/**
|
||||
* EPub#open() -> undefined
|
||||
*
|
||||
* Opens the epub file with Zip unpacker, retrieves file listing
|
||||
* and runs mime type check
|
||||
**/
|
||||
async open() {
|
||||
this.zip = await new Promise((resolve, reject) => {
|
||||
fs.readFile(this.filename, (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
}).then((data) => {
|
||||
return JSZip.loadAsync(data);
|
||||
});
|
||||
if (Object.keys(this.zip.files).length < 1) {
|
||||
throw new Error("No files in archive");
|
||||
}
|
||||
await this.checkMimeType();
|
||||
}
|
||||
;
|
||||
/**
|
||||
* EPub#checkMimeType() -> undefined
|
||||
*
|
||||
* Checks if there's a file called "mimetype" and that it's contents
|
||||
* are "application/epub+zip". On success runs root file check.
|
||||
**/
|
||||
async checkMimeType() {
|
||||
var i, len;
|
||||
for (let file in this.zip.files) {
|
||||
if (file.toLowerCase() == "mimetype") {
|
||||
this.mimeFile = file;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!this.mimeFile) {
|
||||
throw new Error("No mimetype file in archive");
|
||||
}
|
||||
let data = await this.zip.file(this.mimeFile).async("nodebuffer");
|
||||
let txt = data.toString("utf-8").toLowerCase().trim();
|
||||
if (txt != "application/epub+zip") {
|
||||
throw new Error("Unsupported mime type");
|
||||
}
|
||||
await this.getRootFiles();
|
||||
}
|
||||
;
|
||||
/**
|
||||
* EPub#getRootFiles() -> undefined
|
||||
*
|
||||
* Looks for a "meta-inf/container.xml" file and searches for a
|
||||
* rootfile element with mime type "application/oebps-package+xml".
|
||||
* On success calls the rootfile parser
|
||||
**/
|
||||
async getRootFiles() {
|
||||
for (let file in this.zip.files) {
|
||||
if (file.toLowerCase() == "meta-inf/container.xml") {
|
||||
this.containerFile = file;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!this.containerFile)
|
||||
throw new Error("No container file in archive");
|
||||
let data = await this.zip.files[this.containerFile].async("nodebuffer");
|
||||
var xml = data.toString("utf-8").toLowerCase().trim(), xmlparser = new xml2js.Parser(xml2jsOptions);
|
||||
let res = await new Promise((resolve, reject) => {
|
||||
xmlparser.on("end", (result) => {
|
||||
if (!result.rootfiles || !result.rootfiles.rootfile) {
|
||||
reject(new Error("No rootfiles found"));
|
||||
console.dir(result);
|
||||
return;
|
||||
}
|
||||
var rootfile = result.rootfiles.rootfile, filename = undefined, i, len;
|
||||
if (Array.isArray(rootfile)) {
|
||||
for (i = 0, len = rootfile.length; i < len; i++) {
|
||||
if (rootfile[i]["@"]["media-type"] &&
|
||||
rootfile[i]["@"]["media-type"] == "application/oebps-package+xml" &&
|
||||
rootfile[i]["@"]["full-path"]) {
|
||||
filename = rootfile[i]["@"]["full-path"].toLowerCase().trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (rootfile["@"]) {
|
||||
if (rootfile["@"]["media-type"] != "application/oebps-package+xml" || !rootfile["@"]["full-path"]) {
|
||||
reject(new Error("Rootfile in unknown format"));
|
||||
return;
|
||||
}
|
||||
filename = rootfile["@"]["full-path"].toLowerCase().trim();
|
||||
}
|
||||
if (!filename) {
|
||||
reject(new Error("Empty rootfile"));
|
||||
return;
|
||||
}
|
||||
for (let file in this.zip.files) {
|
||||
if (file == filename) {
|
||||
this.rootFile = file;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!this.rootFile) {
|
||||
reject(new Error("Rootfile not found from archive"));
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
xmlparser.on("error", (err) => {
|
||||
reject(new Error("Parsing container XML failed"));
|
||||
return;
|
||||
});
|
||||
xmlparser.parseString(xml);
|
||||
});
|
||||
await this.handleRootFile();
|
||||
}
|
||||
;
|
||||
/**
|
||||
* EPub#handleRootFile() -> undefined
|
||||
*
|
||||
* Parses the rootfile XML and calls rootfile parser
|
||||
**/
|
||||
async handleRootFile() {
|
||||
let data = await this.zip.files[this.rootFile].async("nodebuffer");
|
||||
var xml = data.toString("utf-8");
|
||||
let rf = await new Promise((resolve, reject) => {
|
||||
let xmlparser = new xml2js.Parser(xml2jsOptions);
|
||||
xmlparser.on("end", (data) => {
|
||||
resolve(data);
|
||||
});
|
||||
xmlparser.on("error", err => {
|
||||
reject(err);
|
||||
});
|
||||
xmlparser.parseString(xml);
|
||||
});
|
||||
await this.parseRootFile(rf);
|
||||
}
|
||||
;
|
||||
/**
|
||||
* EPub#parseRootFile() -> undefined
|
||||
*
|
||||
* Parses elements "metadata," "manifest," "spine" and TOC.
|
||||
* Emits "end" if no TOC
|
||||
**/
|
||||
async parseRootFile(rootfile) {
|
||||
this.version = rootfile['@'].version || '2.0';
|
||||
var i, len, keys, keyparts, key;
|
||||
keys = Object.keys(rootfile);
|
||||
for (i = 0, len = keys.length; i < len; i++) {
|
||||
keyparts = keys[i].split(":");
|
||||
key = (keyparts.pop() || "").toLowerCase().trim();
|
||||
switch (key) {
|
||||
case "metadata":
|
||||
this.parseMetadata(rootfile[keys[i]]);
|
||||
break;
|
||||
case "manifest":
|
||||
this.parseManifest(rootfile[keys[i]]);
|
||||
break;
|
||||
case "spine":
|
||||
this.parseSpine(rootfile[keys[i]]);
|
||||
break;
|
||||
case "guide":
|
||||
//this.parseGuide(rootfile[keys[i]]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (this.spine.toc) {
|
||||
await this.parseTOC();
|
||||
}
|
||||
}
|
||||
;
|
||||
/**
|
||||
* EPub#parseMetadata() -> undefined
|
||||
*
|
||||
* Parses "metadata" block (book metadata, title, author etc.)
|
||||
**/
|
||||
parseMetadata(metadata) {
|
||||
var i, j, len, keys, keyparts, key;
|
||||
keys = Object.keys(metadata);
|
||||
for (i = 0, len = keys.length; i < len; i++) {
|
||||
keyparts = keys[i].split(":");
|
||||
key = (keyparts.pop() || "").toLowerCase().trim();
|
||||
switch (key) {
|
||||
case "publisher":
|
||||
if (Array.isArray(metadata[keys[i]])) {
|
||||
this.metadata.publisher = String(metadata[keys[i]][0] && metadata[keys[i]][0]["#"] || metadata[keys[i]][0] || "").trim();
|
||||
}
|
||||
else {
|
||||
this.metadata.publisher = String(metadata[keys[i]]["#"] || metadata[keys[i]] || "").trim();
|
||||
}
|
||||
break;
|
||||
case "language":
|
||||
if (Array.isArray(metadata[keys[i]])) {
|
||||
this.metadata.language = String(metadata[keys[i]][0] && metadata[keys[i]][0]["#"] || metadata[keys[i]][0] || "").toLowerCase().trim();
|
||||
}
|
||||
else {
|
||||
this.metadata.language = String(metadata[keys[i]]["#"] || metadata[keys[i]] || "").toLowerCase().trim();
|
||||
}
|
||||
break;
|
||||
case "title":
|
||||
if (Array.isArray(metadata[keys[i]])) {
|
||||
this.metadata.title = String(metadata[keys[i]][0] && metadata[keys[i]][0]["#"] || metadata[keys[i]][0] || "").trim();
|
||||
}
|
||||
else {
|
||||
this.metadata.title = String(metadata[keys[i]]["#"] || metadata[keys[i]] || "").trim();
|
||||
}
|
||||
break;
|
||||
case "subject":
|
||||
if (Array.isArray(metadata[keys[i]])) {
|
||||
this.metadata.subject = String(metadata[keys[i]][0] && metadata[keys[i]][0]["#"] || metadata[keys[i]][0] || "").trim();
|
||||
}
|
||||
else {
|
||||
this.metadata.subject = String(metadata[keys[i]]["#"] || metadata[keys[i]] || "").trim();
|
||||
}
|
||||
break;
|
||||
case "description":
|
||||
if (Array.isArray(metadata[keys[i]])) {
|
||||
this.metadata.description = String(metadata[keys[i]][0] && metadata[keys[i]][0]["#"] || metadata[keys[i]][0] || "").trim();
|
||||
}
|
||||
else {
|
||||
this.metadata.description = String(metadata[keys[i]]["#"] || metadata[keys[i]] || "").trim();
|
||||
}
|
||||
break;
|
||||
case "creator":
|
||||
if (Array.isArray(metadata[keys[i]])) {
|
||||
this.metadata.creator = String(metadata[keys[i]][0] && metadata[keys[i]][0]["#"] || metadata[keys[i]][0] || "").trim();
|
||||
this.metadata.creatorFileAs = String(metadata[keys[i]][0] && metadata[keys[i]][0]['@'] && metadata[keys[i]][0]['@']["opf:file-as"] || this.metadata.creator).trim();
|
||||
}
|
||||
else {
|
||||
this.metadata.creator = String(metadata[keys[i]]["#"] || metadata[keys[i]] || "").trim();
|
||||
this.metadata.creatorFileAs = String(metadata[keys[i]]['@'] && metadata[keys[i]]['@']["opf:file-as"] || this.metadata.creator).trim();
|
||||
}
|
||||
break;
|
||||
case "date":
|
||||
if (Array.isArray(metadata[keys[i]])) {
|
||||
this.metadata.date = String(metadata[keys[i]][0] && metadata[keys[i]][0]["#"] || metadata[keys[i]][0] || "").trim();
|
||||
}
|
||||
else {
|
||||
this.metadata.date = String(metadata[keys[i]]["#"] || metadata[keys[i]] || "").trim();
|
||||
}
|
||||
break;
|
||||
case "identifier":
|
||||
if (metadata[keys[i]]["@"] && metadata[keys[i]]["@"]["opf:scheme"] == "ISBN") {
|
||||
this.metadata.ISBN = String(metadata[keys[i]]["#"] || "").trim();
|
||||
}
|
||||
else if (metadata[keys[i]]["@"] && metadata[keys[i]]["@"].id && metadata[keys[i]]["@"].id.match(/uuid/i)) {
|
||||
this.metadata.UUID = String(metadata[keys[i]]["#"] || "").replace('urn:uuid:', '').toUpperCase().trim();
|
||||
}
|
||||
else if (Array.isArray(metadata[keys[i]])) {
|
||||
for (j = 0; j < metadata[keys[i]].length; j++) {
|
||||
if (metadata[keys[i]][j]["@"]) {
|
||||
if (metadata[keys[i]][j]["@"]["opf:scheme"] == "ISBN") {
|
||||
this.metadata.ISBN = String(metadata[keys[i]][j]["#"] || "").trim();
|
||||
}
|
||||
else if (metadata[keys[i]][j]["@"].id && metadata[keys[i]][j]["@"].id.match(/uuid/i)) {
|
||||
this.metadata.UUID = String(metadata[keys[i]][j]["#"] || "").replace('urn:uuid:', '').toUpperCase().trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
var metas = metadata['meta'] || {};
|
||||
Object.keys(metas).forEach((key) => {
|
||||
var meta = metas[key];
|
||||
if (meta['@'] && meta['@'].name) {
|
||||
var name = meta['@'].name;
|
||||
this.metadata[name] = meta['@'].content;
|
||||
}
|
||||
if (meta['#'] && meta['@'].property) {
|
||||
this.metadata[meta['@'].property] = meta['#'];
|
||||
}
|
||||
if (meta.name && meta.name == "cover") {
|
||||
this.metadata[meta.name] = meta.content;
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
;
|
||||
/**
|
||||
* EPub#parseManifest() -> undefined
|
||||
*
|
||||
* Parses "manifest" block (all items included, html files, images, styles)
|
||||
**/
|
||||
parseManifest(manifest) {
|
||||
var i, len, path = this.rootFile.split("/"), element, path_str;
|
||||
path.pop();
|
||||
path_str = path.join("/");
|
||||
if (manifest.item) {
|
||||
for (i = 0, len = manifest.item.length; i < len; i++) {
|
||||
if (manifest.item[i]['@']) {
|
||||
element = manifest.item[i]['@'];
|
||||
if (element.href && element.href.substr(0, path_str.length) != path_str) {
|
||||
element.href = path.concat([element.href]).join("/");
|
||||
}
|
||||
this.manifest[manifest.item[i]['@'].id] = element;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
;
|
||||
/**
|
||||
* EPub#parseSpine() -> undefined
|
||||
*
|
||||
* Parses "spine" block (all html elements that are shown to the reader)
|
||||
**/
|
||||
parseSpine(spine) {
|
||||
var i, len, path = this.rootFile.split("/"), element;
|
||||
path.pop();
|
||||
if (spine['@'] && spine['@'].toc) {
|
||||
this.spine.toc = this.manifest[spine['@'].toc] || false;
|
||||
}
|
||||
if (spine.itemref) {
|
||||
if (!Array.isArray(spine.itemref)) {
|
||||
spine.itemref = [spine.itemref];
|
||||
}
|
||||
for (i = 0, len = spine.itemref.length; i < len; i++) {
|
||||
if (spine.itemref[i]['@']) {
|
||||
if (element = this.manifest[spine.itemref[i]['@'].idref]) {
|
||||
this.spine.contents.push(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.flow = this.spine.contents;
|
||||
}
|
||||
;
|
||||
/**
|
||||
* EPub#parseTOC() -> undefined
|
||||
*
|
||||
* Parses ncx file for table of contents (title, html file)
|
||||
**/
|
||||
async parseTOC() {
|
||||
var i, len, path = this.spine.toc.href.split("/"), id_list = {}, keys;
|
||||
path.pop();
|
||||
keys = Object.keys(this.manifest);
|
||||
for (i = 0, len = keys.length; i < len; i++) {
|
||||
id_list[this.manifest[keys[i]].href] = keys[i];
|
||||
}
|
||||
let data = await this.zip.files[this.spine.toc.href].async("nodebuffer");
|
||||
var xml = data.toString("utf-8");
|
||||
await new Promise((resolve, reject) => {
|
||||
let xmlparser = new xml2js.Parser(xml2jsOptions);
|
||||
xmlparser.on("end", result => {
|
||||
if (result.navMap && result.navMap.navPoint) {
|
||||
this.toc = this.walkNavMap(result.navMap.navPoint, path, id_list);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
xmlparser.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
xmlparser.parseString(xml);
|
||||
});
|
||||
}
|
||||
;
|
||||
/**
|
||||
* EPub#walkNavMap(branch, path, id_list,[, level]) -> Array
|
||||
* - branch (Array | Object): NCX NavPoint object
|
||||
* - path (Array): Base path
|
||||
* - id_list (Object): map of file paths and id values
|
||||
* - level (Number): deepness
|
||||
*
|
||||
* Walks the NavMap object through all levels and finds elements
|
||||
* for TOC
|
||||
**/
|
||||
walkNavMap(branch, path, id_list, level) {
|
||||
level = level || 0;
|
||||
// don't go too far
|
||||
if (level > 7) {
|
||||
return [];
|
||||
}
|
||||
var output = [];
|
||||
if (!Array.isArray(branch)) {
|
||||
branch = [branch];
|
||||
}
|
||||
for (var i = 0; i < branch.length; i++) {
|
||||
if (branch[i].navLabel) {
|
||||
var title = '';
|
||||
if (branch[i].navLabel && typeof branch[i].navLabel.text == 'string') {
|
||||
title = branch[i].navLabel && branch[i].navLabel.text || branch[i].navLabel === branch[i].navLabel ?
|
||||
(branch[i].navLabel && branch[i].navLabel.text || branch[i].navLabel || "").trim() : '';
|
||||
}
|
||||
var order = Number(branch[i]["@"] && branch[i]["@"].playOrder || 0);
|
||||
if (isNaN(order)) {
|
||||
order = 0;
|
||||
}
|
||||
var href = '';
|
||||
if (branch[i].content && branch[i].content["@"] && typeof branch[i].content["@"].src == 'string') {
|
||||
href = branch[i].content["@"].src.trim();
|
||||
}
|
||||
var element = {
|
||||
level: level,
|
||||
order: order,
|
||||
title: title,
|
||||
href: undefined,
|
||||
id: undefined
|
||||
};
|
||||
if (href) {
|
||||
href = path.concat([href]).join("/");
|
||||
element.href = href;
|
||||
if (id_list[element.href]) {
|
||||
// link existing object
|
||||
element = this.manifest[id_list[element.href]];
|
||||
element.title = title;
|
||||
element.order = order;
|
||||
element.level = level;
|
||||
}
|
||||
else {
|
||||
// use new one
|
||||
element.href = href;
|
||||
element.id = (branch[i]["@"] && branch[i]["@"].id || "").trim();
|
||||
}
|
||||
output.push(element);
|
||||
}
|
||||
}
|
||||
if (branch[i].navPoint) {
|
||||
output = output.concat(this.walkNavMap(branch[i].navPoint, path, id_list, level + 1));
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
;
|
||||
/**
|
||||
* EPub#getChapter(id, callback) -> undefined
|
||||
* - id (String): Manifest id value for a chapter
|
||||
* - callback (Function): callback function
|
||||
*
|
||||
* Finds a chapter text for an id. Replaces image and link URL's, removes
|
||||
* <head> etc. elements. Return only chapters with mime type application/xhtml+xml
|
||||
**/
|
||||
async getChapter(id) {
|
||||
let str = await this.getChapterRaw(id);
|
||||
var i, len, path = this.rootFile.split("/"), keys = Object.keys(this.manifest);
|
||||
path.pop();
|
||||
// remove linebreaks (no multi line matches in JS regex!)
|
||||
str = str.replace(/\r?\n/g, "\u0000");
|
||||
// keep only <body> contents
|
||||
str.replace(/<body[^>]*?>(.*)<\/body[^>]*?>/i, function (o, d) {
|
||||
str = d.trim();
|
||||
return "";
|
||||
});
|
||||
// remove <script> blocks if any
|
||||
str = str.replace(/<script[^>]*?>(.*?)<\/script[^>]*?>/ig, function (o, s) {
|
||||
return "";
|
||||
});
|
||||
// remove <style> blocks if any
|
||||
str = str.replace(/<style[^>]*?>(.*?)<\/style[^>]*?>/ig, function (o, s) {
|
||||
return "";
|
||||
});
|
||||
// remove onEvent handlers
|
||||
str = str.replace(/(\s)(on\w+)(\s*=\s*["']?[^"'\s>]*?["'\s>])/g, function (o, a, b, c) {
|
||||
return a + "skip-" + b + c;
|
||||
});
|
||||
// replace images
|
||||
str = str.replace(/(\ssrc\s*=\s*["']?)([^"'\s>]*?)(["'\s>])/g, (o, a, b, c) => {
|
||||
var img = path.concat([b]).join("/").trim(), element;
|
||||
for (i = 0, len = keys.length; i < len; i++) {
|
||||
if (this.manifest[keys[i]].href == img) {
|
||||
element = this.manifest[keys[i]];
|
||||
break;
|
||||
}
|
||||
}
|
||||
// include only images from manifest
|
||||
if (element) {
|
||||
return a + this.imageroot + element.id + "/" + img + c;
|
||||
}
|
||||
else {
|
||||
return "";
|
||||
}
|
||||
});
|
||||
// replace links
|
||||
str = str.replace(/(\shref\s*=\s*["']?)([^"'\s>]*?)(["'\s>])/g, (o, a, b, c) => {
|
||||
var linkparts = b && b.split("#"), link = path.concat([(linkparts.shift() || "")]).join("/").trim(), element;
|
||||
for (i = 0, len = keys.length; i < len; i++) {
|
||||
if (this.manifest[keys[i]].href.split("#")[0] == link) {
|
||||
element = this.manifest[keys[i]];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (linkparts.length) {
|
||||
link += "#" + linkparts.join("#");
|
||||
}
|
||||
// include only images from manifest
|
||||
if (element) {
|
||||
return a + this.linkroot + element.id + "/" + link + c;
|
||||
}
|
||||
else {
|
||||
return a + b + c;
|
||||
}
|
||||
});
|
||||
// bring back linebreaks
|
||||
str = str.replace(/\u0000/g, "\n").trim();
|
||||
return str;
|
||||
}
|
||||
;
|
||||
/**
|
||||
* EPub#getChapterRaw(id, callback) -> undefined
|
||||
* - id (String): Manifest id value for a chapter
|
||||
* - callback (Function): callback function
|
||||
*
|
||||
* Returns the raw chapter text for an id.
|
||||
**/
|
||||
async getChapterRaw(id) {
|
||||
if (this.manifest[id]) {
|
||||
if (!(this.manifest[id]['media-type'] == "application/xhtml+xml" || this.manifest[id]['media-type'] == "image/svg+xml")) {
|
||||
throw new Error("Invalid mime type for chapter");
|
||||
}
|
||||
return this.zip.files[this.manifest[id].href].async("nodebuffer").then(b => b.toString("utf8"));
|
||||
}
|
||||
else {
|
||||
throw new Error("File not found");
|
||||
}
|
||||
}
|
||||
;
|
||||
/**
|
||||
* EPub#getImage(id, callback) -> undefined
|
||||
* - id (String): Manifest id value for an image
|
||||
* - callback (Function): callback function
|
||||
*
|
||||
* Finds an image for an id. Returns the image as Buffer. Callback gets
|
||||
* an error object, image buffer and image content-type.
|
||||
* Return only images with mime type image
|
||||
**/
|
||||
async getImage(id) {
|
||||
if (this.manifest[id]) {
|
||||
if ((this.manifest[id]['media-type'] || "").toLowerCase().trim().substr(0, 6) != "image/") {
|
||||
throw new Error("Invalid mime type for image");
|
||||
}
|
||||
return this.getFile(id);
|
||||
}
|
||||
else {
|
||||
throw new Error("File not found");
|
||||
}
|
||||
}
|
||||
;
|
||||
/**
|
||||
* EPub#getFile(id, callback) -> undefined
|
||||
* - id (String): Manifest id value for a file
|
||||
* - callback (Function): callback function
|
||||
*
|
||||
* Finds a file for an id. Returns the file as Buffer. Callback gets
|
||||
* an error object, file contents buffer and file content-type.
|
||||
**/
|
||||
async getFile(id) {
|
||||
if (this.manifest[id]) {
|
||||
let data = await this.zip.files[this.manifest[id].href].async("nodebuffer");
|
||||
// this.zip.files(this.manifest[id].href, (function (err, data) {
|
||||
// if (err) {
|
||||
// callback(new Error("Reading archive failed"));
|
||||
// return;
|
||||
// }
|
||||
// callback(null, data, this.manifest[id]['media-type']);
|
||||
// }).bind(this));
|
||||
}
|
||||
else {
|
||||
throw new Error("File not found");
|
||||
}
|
||||
}
|
||||
;
|
||||
}
|
||||
exports.EPub = EPub;
|
||||
//# sourceMappingURL=epub.js.map
|
1
lib/epub.js.map
Normal file
1
lib/epub.js.map
Normal file
File diff suppressed because one or more lines are too long
1
lib/test.d.ts
vendored
Normal file
1
lib/test.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export {};
|
9
lib/test.js
Normal file
9
lib/test.js
Normal file
@ -0,0 +1,9 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const epub_1 = require("./epub");
|
||||
var epub = new epub_1.EPub("alice.epub", "/imagewebroot/", "/articlewebroot/");
|
||||
epub.parse().then(async () => {
|
||||
console.log(epub);
|
||||
console.log(await epub.getChapter("item32"));
|
||||
}).catch(err => console.error(err));
|
||||
//# sourceMappingURL=test.js.map
|
1
lib/test.js.map
Normal file
1
lib/test.js.map
Normal file
@ -0,0 +1 @@
|
||||
{"version":3,"file":"test.js","sourceRoot":"","sources":["../src/test.ts"],"names":[],"mappings":";;AAAA,iCAA8B;AAE9B,IAAI,IAAI,GAAG,IAAI,WAAI,CAAC,YAAY,EAAE,gBAAgB,EAAE,kBAAkB,CAAC,CAAC;AACxE,IAAI,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE;IAC1B,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAClB,OAAO,CAAC,GAAG,CAAC,MAAM,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAA;AAC/C,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC"}
|
14
package.json
14
package.json
@ -19,8 +19,8 @@
|
||||
"type": "git",
|
||||
"url": "http://github.com/julien-c/epub.git"
|
||||
},
|
||||
"main": "./epub",
|
||||
"typings": "./epub.d.ts",
|
||||
"main": "./lib/epub",
|
||||
"typings": "./lib/epub.d.ts",
|
||||
"licenses": [
|
||||
{
|
||||
"type": "MIT",
|
||||
@ -36,9 +36,19 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.4.11",
|
||||
"jszip": "^3.1.5",
|
||||
"xml2js": "^0.4.19"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"zipfile": "^0.5.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jszip": "^3.1.4",
|
||||
"@types/node": "^10.10.1",
|
||||
"@types/xml2js": "^0.4.3",
|
||||
"typescript": "^3.0.3"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc"
|
||||
}
|
||||
}
|
765
src/epub.ts
Normal file
765
src/epub.ts
Normal file
@ -0,0 +1,765 @@
|
||||
var xml2js = require('xml2js');
|
||||
var xml2jsOptions = xml2js.defaults['0.1'];
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
|
||||
// try {
|
||||
// // zipfile is an optional dependency:
|
||||
// var ZipFile = require("zipfile").ZipFile;
|
||||
// } catch (err) {
|
||||
// // Mock zipfile using pure-JS adm-zip:
|
||||
// var AdmZip = require('adm-zip');
|
||||
|
||||
// var ZipFile = function (filename) {
|
||||
// this.admZip = new AdmZip(filename);
|
||||
// this.names = this.admZip.getEntries().map(function (zipEntry) {
|
||||
// return zipEntry.entryName;
|
||||
// });
|
||||
// this.count = this.names.length;
|
||||
// };
|
||||
// ZipFile.prototype.readFile = function (name, cb) {
|
||||
// this.admZip.readFileAsync(this.admZip.getEntry(name), function (buffer, error) {
|
||||
// // `error` is bogus right now, so let's just drop it.
|
||||
// // see https://github.com/cthackers/adm-zip/pull/88
|
||||
// return cb(null, buffer);
|
||||
// });
|
||||
// };
|
||||
// }
|
||||
|
||||
import * as fs from "fs";
|
||||
import * as JSZip from "jszip";
|
||||
|
||||
//TODO: Cache parsed data
|
||||
|
||||
/**
|
||||
* new EPub(fname[, imageroot][, linkroot])
|
||||
* - fname (String): filename for the ebook
|
||||
* - imageroot (String): URL prefix for images
|
||||
* - linkroot (String): URL prefix for links
|
||||
*
|
||||
* Creates an Event Emitter type object for parsing epub files
|
||||
*
|
||||
* var epub = new EPub("book.epub");
|
||||
* epub.on("end", function () {
|
||||
* console.log(epub.spine);
|
||||
* });
|
||||
* epub.on("error", function (error) { ... });
|
||||
* epub.parse();
|
||||
*
|
||||
* Image and link URL format is:
|
||||
*
|
||||
* imageroot + img_id + img_zip_path
|
||||
*
|
||||
* So an image "logo.jpg" which resides in "OPT/" in the zip archive
|
||||
* and is listed in the manifest with id "logo_img" will have the
|
||||
* following url (providing that imageroot is "/images/"):
|
||||
*
|
||||
* /images/logo_img/OPT/logo.jpg
|
||||
**/
|
||||
export class EPub {
|
||||
zip: JSZip;
|
||||
filename: string;
|
||||
imageroot: string;
|
||||
linkroot: string;
|
||||
containerFile: string;
|
||||
mimeFile: string;
|
||||
rootFile: string;
|
||||
metadata: {
|
||||
publisher?: string,
|
||||
language?: string,
|
||||
title?: string,
|
||||
subject?: string,
|
||||
description?: string,
|
||||
creator?: string,
|
||||
creatorFileAs?: string,
|
||||
date?: string,
|
||||
ISBN?: string,
|
||||
UUID?: string
|
||||
} = <any>{};
|
||||
manifest = {};
|
||||
spine = { toc: undefined, contents: [] };
|
||||
flow = [];
|
||||
toc = [];
|
||||
|
||||
version: string;
|
||||
constructor(fname, imageroot, linkroot) {
|
||||
|
||||
this.filename = fname;
|
||||
|
||||
this.imageroot = (imageroot || "/images/").trim();
|
||||
this.linkroot = (linkroot || "/links/").trim();
|
||||
|
||||
if (this.imageroot.substr(-1) != "/") {
|
||||
this.imageroot += "/";
|
||||
}
|
||||
if (this.linkroot.substr(-1) != "/") {
|
||||
this.linkroot += "/";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EPub#parse() -> undefined
|
||||
*
|
||||
* Starts the parser, needs to be called by the script
|
||||
**/
|
||||
async parse() {
|
||||
this.containerFile = undefined;
|
||||
this.mimeFile = undefined;
|
||||
this.rootFile = undefined;
|
||||
|
||||
this.metadata = {};
|
||||
this.manifest = {};
|
||||
this.spine = { toc: undefined, contents: [] };
|
||||
this.flow = [];
|
||||
this.toc = [];
|
||||
|
||||
await this.open();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* EPub#open() -> undefined
|
||||
*
|
||||
* Opens the epub file with Zip unpacker, retrieves file listing
|
||||
* and runs mime type check
|
||||
**/
|
||||
async open() {
|
||||
this.zip = await new Promise<Buffer>((resolve, reject) => {
|
||||
fs.readFile(this.filename, (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
}).then((data) => {
|
||||
return JSZip.loadAsync(data);
|
||||
})
|
||||
|
||||
if (Object.keys(this.zip.files).length < 1) {
|
||||
throw new Error("No files in archive");
|
||||
}
|
||||
|
||||
await this.checkMimeType();
|
||||
};
|
||||
|
||||
/**
|
||||
* EPub#checkMimeType() -> undefined
|
||||
*
|
||||
* Checks if there's a file called "mimetype" and that it's contents
|
||||
* are "application/epub+zip". On success runs root file check.
|
||||
**/
|
||||
async checkMimeType() {
|
||||
var i, len;
|
||||
|
||||
for (let file in this.zip.files) {
|
||||
if (file.toLowerCase() == "mimetype") {
|
||||
this.mimeFile = file;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.mimeFile) {
|
||||
throw new Error("No mimetype file in archive");
|
||||
}
|
||||
|
||||
let data: Buffer = await this.zip.file(this.mimeFile).async("nodebuffer");
|
||||
let txt = data.toString("utf-8").toLowerCase().trim();
|
||||
|
||||
if (txt != "application/epub+zip") {
|
||||
throw new Error("Unsupported mime type");
|
||||
|
||||
}
|
||||
|
||||
await this.getRootFiles();
|
||||
};
|
||||
|
||||
/**
|
||||
* EPub#getRootFiles() -> undefined
|
||||
*
|
||||
* Looks for a "meta-inf/container.xml" file and searches for a
|
||||
* rootfile element with mime type "application/oebps-package+xml".
|
||||
* On success calls the rootfile parser
|
||||
**/
|
||||
async getRootFiles() {
|
||||
for (let file in this.zip.files) {
|
||||
if (file.toLowerCase() == "meta-inf/container.xml") {
|
||||
this.containerFile = file;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.containerFile)
|
||||
throw new Error("No container file in archive")
|
||||
|
||||
|
||||
let data: Buffer = await this.zip.files[this.containerFile].async("nodebuffer")
|
||||
var xml = data.toString("utf-8").toLowerCase().trim(),
|
||||
xmlparser = new xml2js.Parser(xml2jsOptions);
|
||||
let res = await new Promise((resolve, reject) => {
|
||||
xmlparser.on("end", (result) => {
|
||||
if (!result.rootfiles || !result.rootfiles.rootfile) {
|
||||
reject(new Error("No rootfiles found"));
|
||||
console.dir(result);
|
||||
return;
|
||||
}
|
||||
|
||||
var rootfile = result.rootfiles.rootfile,
|
||||
filename = undefined, i, len;
|
||||
|
||||
if (Array.isArray(rootfile)) {
|
||||
|
||||
for (i = 0, len = rootfile.length; i < len; i++) {
|
||||
if (rootfile[i]["@"]["media-type"] &&
|
||||
rootfile[i]["@"]["media-type"] == "application/oebps-package+xml" &&
|
||||
rootfile[i]["@"]["full-path"]) {
|
||||
filename = rootfile[i]["@"]["full-path"].toLowerCase().trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} else if (rootfile["@"]) {
|
||||
if (rootfile["@"]["media-type"] != "application/oebps-package+xml" || !rootfile["@"]["full-path"]) {
|
||||
reject(new Error("Rootfile in unknown format"));
|
||||
return;
|
||||
}
|
||||
filename = rootfile["@"]["full-path"].toLowerCase().trim();
|
||||
}
|
||||
|
||||
if (!filename) {
|
||||
reject(new Error("Empty rootfile"));
|
||||
return;
|
||||
}
|
||||
|
||||
for (let file in this.zip.files) {
|
||||
if (file == filename) {
|
||||
this.rootFile = file;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.rootFile) {
|
||||
reject(new Error("Rootfile not found from archive"));
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
|
||||
xmlparser.on("error", (err) => {
|
||||
reject(new Error("Parsing container XML failed"));
|
||||
return;
|
||||
})
|
||||
|
||||
xmlparser.parseString(xml);
|
||||
})
|
||||
|
||||
await this.handleRootFile();
|
||||
};
|
||||
|
||||
/**
|
||||
* EPub#handleRootFile() -> undefined
|
||||
*
|
||||
* Parses the rootfile XML and calls rootfile parser
|
||||
**/
|
||||
async handleRootFile() {
|
||||
let data = await this.zip.files[this.rootFile].async("nodebuffer");
|
||||
var xml = data.toString("utf-8")
|
||||
|
||||
let rf = await new Promise((resolve, reject) => {
|
||||
let xmlparser = new xml2js.Parser(xml2jsOptions);
|
||||
xmlparser.on("end", (data) => {
|
||||
resolve(data);
|
||||
});
|
||||
|
||||
xmlparser.on("error", err => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
xmlparser.parseString(xml);
|
||||
})
|
||||
|
||||
await this.parseRootFile(rf);
|
||||
};
|
||||
|
||||
/**
|
||||
* EPub#parseRootFile() -> undefined
|
||||
*
|
||||
* Parses elements "metadata," "manifest," "spine" and TOC.
|
||||
* Emits "end" if no TOC
|
||||
**/
|
||||
async parseRootFile(rootfile) {
|
||||
|
||||
this.version = rootfile['@'].version || '2.0';
|
||||
|
||||
var i, len, keys, keyparts, key;
|
||||
keys = Object.keys(rootfile);
|
||||
for (i = 0, len = keys.length; i < len; i++) {
|
||||
keyparts = keys[i].split(":");
|
||||
key = (keyparts.pop() || "").toLowerCase().trim();
|
||||
switch (key) {
|
||||
case "metadata":
|
||||
this.parseMetadata(rootfile[keys[i]]);
|
||||
break;
|
||||
case "manifest":
|
||||
this.parseManifest(rootfile[keys[i]]);
|
||||
break;
|
||||
case "spine":
|
||||
this.parseSpine(rootfile[keys[i]]);
|
||||
break;
|
||||
case "guide":
|
||||
//this.parseGuide(rootfile[keys[i]]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.spine.toc) {
|
||||
await this.parseTOC();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* EPub#parseMetadata() -> undefined
|
||||
*
|
||||
* Parses "metadata" block (book metadata, title, author etc.)
|
||||
**/
|
||||
parseMetadata(metadata) {
|
||||
var i, j, len, keys, keyparts, key;
|
||||
|
||||
keys = Object.keys(metadata);
|
||||
for (i = 0, len = keys.length; i < len; i++) {
|
||||
keyparts = keys[i].split(":");
|
||||
key = (keyparts.pop() || "").toLowerCase().trim();
|
||||
switch (key) {
|
||||
case "publisher":
|
||||
if (Array.isArray(metadata[keys[i]])) {
|
||||
this.metadata.publisher = String(metadata[keys[i]][0] && metadata[keys[i]][0]["#"] || metadata[keys[i]][0] || "").trim();
|
||||
} else {
|
||||
this.metadata.publisher = String(metadata[keys[i]]["#"] || metadata[keys[i]] || "").trim();
|
||||
}
|
||||
break;
|
||||
case "language":
|
||||
if (Array.isArray(metadata[keys[i]])) {
|
||||
this.metadata.language = String(metadata[keys[i]][0] && metadata[keys[i]][0]["#"] || metadata[keys[i]][0] || "").toLowerCase().trim();
|
||||
} else {
|
||||
this.metadata.language = String(metadata[keys[i]]["#"] || metadata[keys[i]] || "").toLowerCase().trim();
|
||||
}
|
||||
break;
|
||||
case "title":
|
||||
if (Array.isArray(metadata[keys[i]])) {
|
||||
this.metadata.title = String(metadata[keys[i]][0] && metadata[keys[i]][0]["#"] || metadata[keys[i]][0] || "").trim();
|
||||
} else {
|
||||
this.metadata.title = String(metadata[keys[i]]["#"] || metadata[keys[i]] || "").trim();
|
||||
}
|
||||
break;
|
||||
case "subject":
|
||||
if (Array.isArray(metadata[keys[i]])) {
|
||||
this.metadata.subject = String(metadata[keys[i]][0] && metadata[keys[i]][0]["#"] || metadata[keys[i]][0] || "").trim();
|
||||
} else {
|
||||
this.metadata.subject = String(metadata[keys[i]]["#"] || metadata[keys[i]] || "").trim();
|
||||
}
|
||||
break;
|
||||
case "description":
|
||||
if (Array.isArray(metadata[keys[i]])) {
|
||||
this.metadata.description = String(metadata[keys[i]][0] && metadata[keys[i]][0]["#"] || metadata[keys[i]][0] || "").trim();
|
||||
} else {
|
||||
this.metadata.description = String(metadata[keys[i]]["#"] || metadata[keys[i]] || "").trim();
|
||||
}
|
||||
break;
|
||||
case "creator":
|
||||
if (Array.isArray(metadata[keys[i]])) {
|
||||
this.metadata.creator = String(metadata[keys[i]][0] && metadata[keys[i]][0]["#"] || metadata[keys[i]][0] || "").trim();
|
||||
this.metadata.creatorFileAs = String(metadata[keys[i]][0] && metadata[keys[i]][0]['@'] && metadata[keys[i]][0]['@']["opf:file-as"] || this.metadata.creator).trim();
|
||||
} else {
|
||||
this.metadata.creator = String(metadata[keys[i]]["#"] || metadata[keys[i]] || "").trim();
|
||||
this.metadata.creatorFileAs = String(metadata[keys[i]]['@'] && metadata[keys[i]]['@']["opf:file-as"] || this.metadata.creator).trim();
|
||||
}
|
||||
break;
|
||||
case "date":
|
||||
if (Array.isArray(metadata[keys[i]])) {
|
||||
this.metadata.date = String(metadata[keys[i]][0] && metadata[keys[i]][0]["#"] || metadata[keys[i]][0] || "").trim();
|
||||
} else {
|
||||
this.metadata.date = String(metadata[keys[i]]["#"] || metadata[keys[i]] || "").trim();
|
||||
}
|
||||
break;
|
||||
case "identifier":
|
||||
if (metadata[keys[i]]["@"] && metadata[keys[i]]["@"]["opf:scheme"] == "ISBN") {
|
||||
this.metadata.ISBN = String(metadata[keys[i]]["#"] || "").trim();
|
||||
} else if (metadata[keys[i]]["@"] && metadata[keys[i]]["@"].id && metadata[keys[i]]["@"].id.match(/uuid/i)) {
|
||||
this.metadata.UUID = String(metadata[keys[i]]["#"] || "").replace('urn:uuid:', '').toUpperCase().trim();
|
||||
} else if (Array.isArray(metadata[keys[i]])) {
|
||||
for (j = 0; j < metadata[keys[i]].length; j++) {
|
||||
if (metadata[keys[i]][j]["@"]) {
|
||||
if (metadata[keys[i]][j]["@"]["opf:scheme"] == "ISBN") {
|
||||
this.metadata.ISBN = String(metadata[keys[i]][j]["#"] || "").trim();
|
||||
} else if (metadata[keys[i]][j]["@"].id && metadata[keys[i]][j]["@"].id.match(/uuid/i)) {
|
||||
this.metadata.UUID = String(metadata[keys[i]][j]["#"] || "").replace('urn:uuid:', '').toUpperCase().trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var metas = metadata['meta'] || {};
|
||||
Object.keys(metas).forEach((key) => {
|
||||
var meta = metas[key];
|
||||
if (meta['@'] && meta['@'].name) {
|
||||
var name = meta['@'].name;
|
||||
this.metadata[name] = meta['@'].content;
|
||||
}
|
||||
if (meta['#'] && meta['@'].property) {
|
||||
this.metadata[meta['@'].property] = meta['#'];
|
||||
}
|
||||
|
||||
if (meta.name && meta.name == "cover") {
|
||||
this.metadata[meta.name] = meta.content;
|
||||
}
|
||||
}, this);
|
||||
};
|
||||
|
||||
/**
|
||||
* EPub#parseManifest() -> undefined
|
||||
*
|
||||
* Parses "manifest" block (all items included, html files, images, styles)
|
||||
**/
|
||||
parseManifest(manifest) {
|
||||
var i, len, path = this.rootFile.split("/"), element, path_str;
|
||||
path.pop();
|
||||
path_str = path.join("/");
|
||||
|
||||
if (manifest.item) {
|
||||
for (i = 0, len = manifest.item.length; i < len; i++) {
|
||||
if (manifest.item[i]['@']) {
|
||||
element = manifest.item[i]['@'];
|
||||
|
||||
if (element.href && element.href.substr(0, path_str.length) != path_str) {
|
||||
element.href = path.concat([element.href]).join("/");
|
||||
}
|
||||
|
||||
this.manifest[manifest.item[i]['@'].id] = element;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* EPub#parseSpine() -> undefined
|
||||
*
|
||||
* Parses "spine" block (all html elements that are shown to the reader)
|
||||
**/
|
||||
parseSpine(spine) {
|
||||
var i, len, path = this.rootFile.split("/"), element;
|
||||
path.pop();
|
||||
|
||||
if (spine['@'] && spine['@'].toc) {
|
||||
this.spine.toc = this.manifest[spine['@'].toc] || false;
|
||||
}
|
||||
|
||||
if (spine.itemref) {
|
||||
if (!Array.isArray(spine.itemref)) {
|
||||
spine.itemref = [spine.itemref];
|
||||
}
|
||||
for (i = 0, len = spine.itemref.length; i < len; i++) {
|
||||
if (spine.itemref[i]['@']) {
|
||||
if (element = this.manifest[spine.itemref[i]['@'].idref]) {
|
||||
this.spine.contents.push(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.flow = this.spine.contents;
|
||||
};
|
||||
|
||||
/**
|
||||
* EPub#parseTOC() -> undefined
|
||||
*
|
||||
* Parses ncx file for table of contents (title, html file)
|
||||
**/
|
||||
async parseTOC() {
|
||||
var i, len, path = this.spine.toc.href.split("/"), id_list = {}, keys;
|
||||
path.pop();
|
||||
|
||||
keys = Object.keys(this.manifest);
|
||||
for (i = 0, len = keys.length; i < len; i++) {
|
||||
id_list[this.manifest[keys[i]].href] = keys[i];
|
||||
}
|
||||
|
||||
let data = await this.zip.files[this.spine.toc.href].async("nodebuffer")
|
||||
var xml = data.toString("utf-8")
|
||||
await new Promise((resolve, reject) => {
|
||||
|
||||
|
||||
let xmlparser = new xml2js.Parser(xml2jsOptions);
|
||||
|
||||
xmlparser.on("end", result => {
|
||||
if (result.navMap && result.navMap.navPoint) {
|
||||
this.toc = this.walkNavMap(result.navMap.navPoint, path, id_list);
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
|
||||
xmlparser.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
xmlparser.parseString(xml);
|
||||
})
|
||||
};
|
||||
|
||||
/**
|
||||
* EPub#walkNavMap(branch, path, id_list,[, level]) -> Array
|
||||
* - branch (Array | Object): NCX NavPoint object
|
||||
* - path (Array): Base path
|
||||
* - id_list (Object): map of file paths and id values
|
||||
* - level (Number): deepness
|
||||
*
|
||||
* Walks the NavMap object through all levels and finds elements
|
||||
* for TOC
|
||||
**/
|
||||
walkNavMap(branch, path, id_list, level?) {
|
||||
level = level || 0;
|
||||
|
||||
// don't go too far
|
||||
if (level > 7) {
|
||||
return [];
|
||||
}
|
||||
|
||||
var output = [];
|
||||
|
||||
if (!Array.isArray(branch)) {
|
||||
branch = [branch];
|
||||
}
|
||||
|
||||
for (var i = 0; i < branch.length; i++) {
|
||||
if (branch[i].navLabel) {
|
||||
|
||||
var title = '';
|
||||
if (branch[i].navLabel && typeof branch[i].navLabel.text == 'string') {
|
||||
title = branch[i].navLabel && branch[i].navLabel.text || branch[i].navLabel === branch[i].navLabel ?
|
||||
(branch[i].navLabel && branch[i].navLabel.text || branch[i].navLabel || "").trim() : '';
|
||||
}
|
||||
var order = Number(branch[i]["@"] && branch[i]["@"].playOrder || 0);
|
||||
if (isNaN(order)) {
|
||||
order = 0;
|
||||
}
|
||||
var href = '';
|
||||
if (branch[i].content && branch[i].content["@"] && typeof branch[i].content["@"].src == 'string') {
|
||||
href = branch[i].content["@"].src.trim();
|
||||
}
|
||||
|
||||
var element = {
|
||||
level: level,
|
||||
order: order,
|
||||
title: title,
|
||||
href: undefined,
|
||||
id: undefined
|
||||
};
|
||||
|
||||
if (href) {
|
||||
href = path.concat([href]).join("/");
|
||||
element.href = href;
|
||||
|
||||
if (id_list[element.href]) {
|
||||
// link existing object
|
||||
element = this.manifest[id_list[element.href]];
|
||||
element.title = title;
|
||||
element.order = order;
|
||||
element.level = level;
|
||||
} else {
|
||||
// use new one
|
||||
element.href = href;
|
||||
element.id = (branch[i]["@"] && branch[i]["@"].id || "").trim();
|
||||
}
|
||||
|
||||
output.push(element);
|
||||
}
|
||||
}
|
||||
if (branch[i].navPoint) {
|
||||
output = output.concat(this.walkNavMap(branch[i].navPoint, path, id_list, level + 1));
|
||||
}
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
/**
|
||||
* EPub#getChapter(id, callback) -> undefined
|
||||
* - id (String): Manifest id value for a chapter
|
||||
* - callback (Function): callback function
|
||||
*
|
||||
* Finds a chapter text for an id. Replaces image and link URL's, removes
|
||||
* <head> etc. elements. Return only chapters with mime type application/xhtml+xml
|
||||
**/
|
||||
async getChapter(id) {
|
||||
let str = await this.getChapterRaw(id)
|
||||
|
||||
var i, len, path = this.rootFile.split("/"), keys = Object.keys(this.manifest);
|
||||
path.pop();
|
||||
|
||||
// remove linebreaks (no multi line matches in JS regex!)
|
||||
str = str.replace(/\r?\n/g, "\u0000");
|
||||
|
||||
// keep only <body> contents
|
||||
str.replace(/<body[^>]*?>(.*)<\/body[^>]*?>/i, function (o, d) {
|
||||
str = d.trim();
|
||||
return "";
|
||||
});
|
||||
|
||||
// remove <script> blocks if any
|
||||
str = str.replace(/<script[^>]*?>(.*?)<\/script[^>]*?>/ig, function (o, s) {
|
||||
return "";
|
||||
});
|
||||
|
||||
// remove <style> blocks if any
|
||||
str = str.replace(/<style[^>]*?>(.*?)<\/style[^>]*?>/ig, function (o, s) {
|
||||
return "";
|
||||
});
|
||||
|
||||
// remove onEvent handlers
|
||||
str = str.replace(/(\s)(on\w+)(\s*=\s*["']?[^"'\s>]*?["'\s>])/g, function (o, a, b, c) {
|
||||
return a + "skip-" + b + c;
|
||||
});
|
||||
|
||||
// replace images
|
||||
str = str.replace(/(\ssrc\s*=\s*["']?)([^"'\s>]*?)(["'\s>])/g, (o, a, b, c) => {
|
||||
var img = path.concat([b]).join("/").trim(),
|
||||
element;
|
||||
|
||||
for (i = 0, len = keys.length; i < len; i++) {
|
||||
if (this.manifest[keys[i]].href == img) {
|
||||
element = this.manifest[keys[i]];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// include only images from manifest
|
||||
if (element) {
|
||||
return a + this.imageroot + element.id + "/" + img + c;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// replace links
|
||||
str = str.replace(/(\shref\s*=\s*["']?)([^"'\s>]*?)(["'\s>])/g, (o, a, b, c) => {
|
||||
var linkparts = b && b.split("#"),
|
||||
link = path.concat([(linkparts.shift() || "")]).join("/").trim(),
|
||||
element;
|
||||
|
||||
for (i = 0, len = keys.length; i < len; i++) {
|
||||
if (this.manifest[keys[i]].href.split("#")[0] == link) {
|
||||
element = this.manifest[keys[i]];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (linkparts.length) {
|
||||
link += "#" + linkparts.join("#");
|
||||
}
|
||||
|
||||
// include only images from manifest
|
||||
if (element) {
|
||||
return a + this.linkroot + element.id + "/" + link + c;
|
||||
} else {
|
||||
return a + b + c;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// bring back linebreaks
|
||||
str = str.replace(/\u0000/g, "\n").trim();
|
||||
|
||||
return str;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* EPub#getChapterRaw(id, callback) -> undefined
|
||||
* - id (String): Manifest id value for a chapter
|
||||
* - callback (Function): callback function
|
||||
*
|
||||
* Returns the raw chapter text for an id.
|
||||
**/
|
||||
async getChapterRaw(id) {
|
||||
|
||||
if (this.manifest[id]) {
|
||||
if (!(this.manifest[id]['media-type'] == "application/xhtml+xml" || this.manifest[id]['media-type'] == "image/svg+xml")) {
|
||||
throw new Error("Invalid mime type for chapter");
|
||||
}
|
||||
return this.zip.files[this.manifest[id].href].async("nodebuffer").then(b => b.toString("utf8"));
|
||||
} else {
|
||||
throw new Error("File not found");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* EPub#getImage(id, callback) -> undefined
|
||||
* - id (String): Manifest id value for an image
|
||||
* - callback (Function): callback function
|
||||
*
|
||||
* Finds an image for an id. Returns the image as Buffer. Callback gets
|
||||
* an error object, image buffer and image content-type.
|
||||
* Return only images with mime type image
|
||||
**/
|
||||
async getImage(id) {
|
||||
if (this.manifest[id]) {
|
||||
|
||||
if ((this.manifest[id]['media-type'] || "").toLowerCase().trim().substr(0, 6) != "image/") {
|
||||
throw new Error("Invalid mime type for image");
|
||||
}
|
||||
|
||||
return this.getFile(id);
|
||||
} else {
|
||||
throw new Error("File not found");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* EPub#getFile(id, callback) -> undefined
|
||||
* - id (String): Manifest id value for a file
|
||||
* - callback (Function): callback function
|
||||
*
|
||||
* Finds a file for an id. Returns the file as Buffer. Callback gets
|
||||
* an error object, file contents buffer and file content-type.
|
||||
**/
|
||||
async getFile(id) {
|
||||
if (this.manifest[id]) {
|
||||
let data = await this.zip.files[this.manifest[id].href].async("nodebuffer");
|
||||
// this.zip.files(this.manifest[id].href, (function (err, data) {
|
||||
// if (err) {
|
||||
// callback(new Error("Reading archive failed"));
|
||||
// return;
|
||||
// }
|
||||
|
||||
// callback(null, data, this.manifest[id]['media-type']);
|
||||
// }).bind(this));
|
||||
} else {
|
||||
throw new Error("File not found");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// readFile(filename, options, callback_) {
|
||||
// var callback = arguments[arguments.length - 1];
|
||||
|
||||
// if (typeof options === 'function' || !options) {
|
||||
// this.zip.readFile(filename, callback);
|
||||
// } else if (typeof options === 'string') {
|
||||
// // options is an encoding
|
||||
// this.zip.readFile(filename, function (err, data) {
|
||||
// if (err) {
|
||||
// callback(new Error('Reading archive failed'));
|
||||
// return;
|
||||
// }
|
||||
// callback(null, data.toString(options));
|
||||
// });
|
||||
// } else {
|
||||
// throw new TypeError('Bad arguments');
|
||||
// }
|
||||
// };
|
||||
|
||||
}
|
7
src/test.ts
Normal file
7
src/test.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { EPub } from "./epub";
|
||||
|
||||
var epub = new EPub("alice.epub", "/imagewebroot/", "/articlewebroot/");
|
||||
epub.parse().then(async () => {
|
||||
console.log(epub);
|
||||
console.log(await epub.getChapter("item32"))
|
||||
}).catch(err => console.error(err));
|
14
tsconfig.json
Normal file
14
tsconfig.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Basic Options */
|
||||
"target": "es2017",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"declaration": true,
|
||||
"outDir": "./lib",
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": [
|
||||
"./src"
|
||||
]
|
||||
}
|
498
yarn.lock
Normal file
498
yarn.lock
Normal file
@ -0,0 +1,498 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@types/events@*":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86"
|
||||
|
||||
"@types/jszip@^3.1.4":
|
||||
version "3.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/jszip/-/jszip-3.1.4.tgz#9b81e3901a6988e9459ac27abf483e6b892251af"
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/node@*", "@types/node@^10.10.1":
|
||||
version "10.10.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.10.1.tgz#d5c96ca246a418404914d180b7fdd625ad18eca6"
|
||||
|
||||
"@types/xml2js@^0.4.3":
|
||||
version "0.4.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/xml2js/-/xml2js-0.4.3.tgz#2f41bfc74d5a4022511721f872ed395a210ad3b7"
|
||||
dependencies:
|
||||
"@types/events" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
abbrev@1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
|
||||
|
||||
adm-zip@^0.4.11:
|
||||
version "0.4.11"
|
||||
resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.11.tgz#2aa54c84c4b01a9d0fb89bb11982a51f13e3d62a"
|
||||
|
||||
ansi-regex@^2.0.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
|
||||
|
||||
ansi-regex@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
|
||||
|
||||
aproba@^1.0.3:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
|
||||
|
||||
are-we-there-yet@~1.1.2:
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21"
|
||||
dependencies:
|
||||
delegates "^1.0.0"
|
||||
readable-stream "^2.0.6"
|
||||
|
||||
balanced-match@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.11"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
||||
dependencies:
|
||||
balanced-match "^1.0.0"
|
||||
concat-map "0.0.1"
|
||||
|
||||
chownr@^1.0.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494"
|
||||
|
||||
code-point-at@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
|
||||
|
||||
concat-map@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
|
||||
console-control-strings@^1.0.0, console-control-strings@~1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
|
||||
|
||||
core-js@~2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.3.0.tgz#fab83fbb0b2d8dc85fa636c4b9d34c75420c6d65"
|
||||
|
||||
core-util-is@~1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
|
||||
|
||||
debug@^2.1.2:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
deep-extend@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
|
||||
|
||||
delegates@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
|
||||
|
||||
detect-libc@^1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
|
||||
|
||||
es6-promise@~3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "http://registry.npmjs.org/es6-promise/-/es6-promise-3.0.2.tgz#010d5858423a5f118979665f46486a95c6ee2bb6"
|
||||
|
||||
fs-minipass@^1.2.5:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d"
|
||||
dependencies:
|
||||
minipass "^2.2.1"
|
||||
|
||||
fs.realpath@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||
|
||||
gauge@~2.7.3:
|
||||
version "2.7.4"
|
||||
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
|
||||
dependencies:
|
||||
aproba "^1.0.3"
|
||||
console-control-strings "^1.0.0"
|
||||
has-unicode "^2.0.0"
|
||||
object-assign "^4.1.0"
|
||||
signal-exit "^3.0.0"
|
||||
string-width "^1.0.1"
|
||||
strip-ansi "^3.0.1"
|
||||
wide-align "^1.1.0"
|
||||
|
||||
glob@^7.0.5:
|
||||
version "7.1.3"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "^3.0.4"
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
has-unicode@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
|
||||
|
||||
iconv-lite@^0.4.4:
|
||||
version "0.4.24"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3"
|
||||
|
||||
ignore-walk@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8"
|
||||
dependencies:
|
||||
minimatch "^3.0.4"
|
||||
|
||||
immediate@~3.0.5:
|
||||
version "3.0.6"
|
||||
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
|
||||
|
||||
inflight@^1.0.4:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
|
||||
dependencies:
|
||||
once "^1.3.0"
|
||||
wrappy "1"
|
||||
|
||||
inherits@2, inherits@~2.0.1, inherits@~2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
|
||||
|
||||
ini@~1.3.0:
|
||||
version "1.3.5"
|
||||
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
|
||||
|
||||
is-fullwidth-code-point@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
|
||||
dependencies:
|
||||
number-is-nan "^1.0.0"
|
||||
|
||||
is-fullwidth-code-point@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
|
||||
|
||||
isarray@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
|
||||
|
||||
jszip@^3.1.5:
|
||||
version "3.1.5"
|
||||
resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.1.5.tgz#e3c2a6c6d706ac6e603314036d43cd40beefdf37"
|
||||
dependencies:
|
||||
core-js "~2.3.0"
|
||||
es6-promise "~3.0.2"
|
||||
lie "~3.1.0"
|
||||
pako "~1.0.2"
|
||||
readable-stream "~2.0.6"
|
||||
|
||||
lie@~3.1.0:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e"
|
||||
dependencies:
|
||||
immediate "~3.0.5"
|
||||
|
||||
minimatch@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimist@0.0.8:
|
||||
version "0.0.8"
|
||||
resolved "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
|
||||
|
||||
minimist@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
|
||||
|
||||
minipass@^2.2.1, minipass@^2.3.3:
|
||||
version "2.3.4"
|
||||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.4.tgz#4768d7605ed6194d6d576169b9e12ef71e9d9957"
|
||||
dependencies:
|
||||
safe-buffer "^5.1.2"
|
||||
yallist "^3.0.0"
|
||||
|
||||
minizlib@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.1.0.tgz#11e13658ce46bc3a70a267aac58359d1e0c29ceb"
|
||||
dependencies:
|
||||
minipass "^2.2.1"
|
||||
|
||||
mkdirp@^0.5.0, mkdirp@^0.5.1:
|
||||
version "0.5.1"
|
||||
resolved "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
|
||||
dependencies:
|
||||
minimist "0.0.8"
|
||||
|
||||
ms@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
|
||||
nan@~2.10.0:
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f"
|
||||
|
||||
needle@^2.2.1:
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.3.tgz#c1b04da378cd634d8befe2de965dc2cfb0fd65ca"
|
||||
dependencies:
|
||||
debug "^2.1.2"
|
||||
iconv-lite "^0.4.4"
|
||||
sax "^1.2.4"
|
||||
|
||||
node-pre-gyp@~0.10.2:
|
||||
version "0.10.3"
|
||||
resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc"
|
||||
dependencies:
|
||||
detect-libc "^1.0.2"
|
||||
mkdirp "^0.5.1"
|
||||
needle "^2.2.1"
|
||||
nopt "^4.0.1"
|
||||
npm-packlist "^1.1.6"
|
||||
npmlog "^4.0.2"
|
||||
rc "^1.2.7"
|
||||
rimraf "^2.6.1"
|
||||
semver "^5.3.0"
|
||||
tar "^4"
|
||||
|
||||
nopt@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
|
||||
dependencies:
|
||||
abbrev "1"
|
||||
osenv "^0.1.4"
|
||||
|
||||
npm-bundled@^1.0.1:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.5.tgz#3c1732b7ba936b3a10325aef616467c0ccbcc979"
|
||||
|
||||
npm-packlist@^1.1.6:
|
||||
version "1.1.11"
|
||||
resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.11.tgz#84e8c683cbe7867d34b1d357d893ce29e28a02de"
|
||||
dependencies:
|
||||
ignore-walk "^3.0.1"
|
||||
npm-bundled "^1.0.1"
|
||||
|
||||
npmlog@^4.0.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
|
||||
dependencies:
|
||||
are-we-there-yet "~1.1.2"
|
||||
console-control-strings "~1.1.0"
|
||||
gauge "~2.7.3"
|
||||
set-blocking "~2.0.0"
|
||||
|
||||
number-is-nan@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
|
||||
|
||||
object-assign@^4.1.0:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||
|
||||
once@^1.3.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
os-homedir@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
|
||||
|
||||
os-tmpdir@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
|
||||
|
||||
osenv@^0.1.4:
|
||||
version "0.1.5"
|
||||
resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410"
|
||||
dependencies:
|
||||
os-homedir "^1.0.0"
|
||||
os-tmpdir "^1.0.0"
|
||||
|
||||
pako@~1.0.2:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258"
|
||||
|
||||
path-is-absolute@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
|
||||
|
||||
process-nextick-args@~1.0.6:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3"
|
||||
|
||||
process-nextick-args@~2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
|
||||
|
||||
rc@^1.2.7:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
|
||||
dependencies:
|
||||
deep-extend "^0.6.0"
|
||||
ini "~1.3.0"
|
||||
minimist "^1.2.0"
|
||||
strip-json-comments "~2.0.1"
|
||||
|
||||
readable-stream@^2.0.6:
|
||||
version "2.3.6"
|
||||
resolved "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
|
||||
dependencies:
|
||||
core-util-is "~1.0.0"
|
||||
inherits "~2.0.3"
|
||||
isarray "~1.0.0"
|
||||
process-nextick-args "~2.0.0"
|
||||
safe-buffer "~5.1.1"
|
||||
string_decoder "~1.1.1"
|
||||
util-deprecate "~1.0.1"
|
||||
|
||||
readable-stream@~2.0.6:
|
||||
version "2.0.6"
|
||||
resolved "http://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e"
|
||||
dependencies:
|
||||
core-util-is "~1.0.0"
|
||||
inherits "~2.0.1"
|
||||
isarray "~1.0.0"
|
||||
process-nextick-args "~1.0.6"
|
||||
string_decoder "~0.10.x"
|
||||
util-deprecate "~1.0.1"
|
||||
|
||||
rimraf@^2.6.1:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
|
||||
dependencies:
|
||||
glob "^7.0.5"
|
||||
|
||||
safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||
|
||||
"safer-buffer@>= 2.1.2 < 3":
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
|
||||
sax@>=0.6.0, sax@^1.2.4:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
||||
|
||||
semver@^5.3.0:
|
||||
version "5.5.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477"
|
||||
|
||||
set-blocking@~2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
|
||||
|
||||
signal-exit@^3.0.0:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
|
||||
|
||||
string-width@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
|
||||
dependencies:
|
||||
code-point-at "^1.0.0"
|
||||
is-fullwidth-code-point "^1.0.0"
|
||||
strip-ansi "^3.0.0"
|
||||
|
||||
"string-width@^1.0.2 || 2":
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
|
||||
dependencies:
|
||||
is-fullwidth-code-point "^2.0.0"
|
||||
strip-ansi "^4.0.0"
|
||||
|
||||
string_decoder@~0.10.x:
|
||||
version "0.10.31"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
|
||||
|
||||
string_decoder@~1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
|
||||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
strip-ansi@^3.0.0, strip-ansi@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
|
||||
dependencies:
|
||||
ansi-regex "^2.0.0"
|
||||
|
||||
strip-ansi@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
|
||||
dependencies:
|
||||
ansi-regex "^3.0.0"
|
||||
|
||||
strip-json-comments@~2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
||||
|
||||
tar@^4:
|
||||
version "4.4.6"
|
||||
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.6.tgz#63110f09c00b4e60ac8bcfe1bf3c8660235fbc9b"
|
||||
dependencies:
|
||||
chownr "^1.0.1"
|
||||
fs-minipass "^1.2.5"
|
||||
minipass "^2.3.3"
|
||||
minizlib "^1.1.0"
|
||||
mkdirp "^0.5.0"
|
||||
safe-buffer "^5.1.2"
|
||||
yallist "^3.0.2"
|
||||
|
||||
typescript@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.0.3.tgz#4853b3e275ecdaa27f78fda46dc273a7eb7fc1c8"
|
||||
|
||||
util-deprecate@~1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
|
||||
wide-align@^1.1.0:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"
|
||||
dependencies:
|
||||
string-width "^1.0.2 || 2"
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
|
||||
xml2js@^0.4.19:
|
||||
version "0.4.19"
|
||||
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7"
|
||||
dependencies:
|
||||
sax ">=0.6.0"
|
||||
xmlbuilder "~9.0.1"
|
||||
|
||||
xmlbuilder@~9.0.1:
|
||||
version "9.0.7"
|
||||
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d"
|
||||
|
||||
yallist@^3.0.0, yallist@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.2.tgz#8452b4bb7e83c7c188d8041c1a837c773d6d8bb9"
|
||||
|
||||
zipfile@^0.5.11:
|
||||
version "0.5.12"
|
||||
resolved "https://registry.yarnpkg.com/zipfile/-/zipfile-0.5.12.tgz#4488ff93dfb8d9089033ba34d8b2cf05358b08b2"
|
||||
dependencies:
|
||||
nan "~2.10.0"
|
||||
node-pre-gyp "~0.10.2"
|
Reference in New Issue
Block a user