Using JSZip and make everything a promise

This commit is contained in:
Fabian Stamm 2018-09-17 22:29:03 +02:00
parent 446bf3d5d8
commit 1c21bb3534
14 changed files with 2169 additions and 879 deletions

67
epub.d.ts vendored
View File

@ -1,66 +1 @@
// Type definitions for epub export {};
// 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;
}

770
epub.js
View File

@ -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
View 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
View 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

File diff suppressed because one or more lines are too long

1
lib/test.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export {};

9
lib/test.js Normal file
View 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
View 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"}

View File

@ -19,8 +19,8 @@
"type": "git", "type": "git",
"url": "http://github.com/julien-c/epub.git" "url": "http://github.com/julien-c/epub.git"
}, },
"main": "./epub", "main": "./lib/epub",
"typings": "./epub.d.ts", "typings": "./lib/epub.d.ts",
"licenses": [ "licenses": [
{ {
"type": "MIT", "type": "MIT",
@ -36,9 +36,19 @@
], ],
"dependencies": { "dependencies": {
"adm-zip": "^0.4.11", "adm-zip": "^0.4.11",
"jszip": "^3.1.5",
"xml2js": "^0.4.19" "xml2js": "^0.4.19"
}, },
"optionalDependencies": { "optionalDependencies": {
"zipfile": "^0.5.11" "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
View 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
View 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
View 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
View 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"