From 4fd9b8f2b5a98bfcde57970b48fed2488a80f356 Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Fri, 17 Sep 2021 21:53:37 +0300 Subject: add in master snapshot of epubjs --- lib/epub.js/src/book.js | 768 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 768 insertions(+) create mode 100644 lib/epub.js/src/book.js (limited to 'lib/epub.js/src/book.js') diff --git a/lib/epub.js/src/book.js b/lib/epub.js/src/book.js new file mode 100644 index 0000000..4243bd1 --- /dev/null +++ b/lib/epub.js/src/book.js @@ -0,0 +1,768 @@ +import EventEmitter from "event-emitter"; +import {extend, defer} from "./utils/core"; +import Url from "./utils/url"; +import Path from "./utils/path"; +import Spine from "./spine"; +import Locations from "./locations"; +import Container from "./container"; +import Packaging from "./packaging"; +import Navigation from "./navigation"; +import Resources from "./resources"; +import PageList from "./pagelist"; +import Rendition from "./rendition"; +import Archive from "./archive"; +import request from "./utils/request"; +import EpubCFI from "./epubcfi"; +import Store from "./store"; +import DisplayOptions from "./displayoptions"; +import { EPUBJS_VERSION, EVENTS } from "./utils/constants"; + +const CONTAINER_PATH = "META-INF/container.xml"; +const IBOOKS_DISPLAY_OPTIONS_PATH = "META-INF/com.apple.ibooks.display-options.xml"; + +const INPUT_TYPE = { + BINARY: "binary", + BASE64: "base64", + EPUB: "epub", + OPF: "opf", + MANIFEST: "json", + DIRECTORY: "directory" +}; + +/** + * An Epub representation with methods for the loading, parsing and manipulation + * of its contents. + * @class + * @param {string} [url] + * @param {object} [options] + * @param {method} [options.requestMethod] a request function to use instead of the default + * @param {boolean} [options.requestCredentials=undefined] send the xhr request withCredentials + * @param {object} [options.requestHeaders=undefined] send the xhr request headers + * @param {string} [options.encoding=binary] optional to pass 'binary' or base64' for archived Epubs + * @param {string} [options.replacements=none] use base64, blobUrl, or none for replacing assets in archived Epubs + * @param {method} [options.canonical] optional function to determine canonical urls for a path + * @param {string} [options.openAs] optional string to determine the input type + * @param {string} [options.store=false] cache the contents in local storage, value should be the name of the reader + * @returns {Book} + * @example new Book("/path/to/book.epub", {}) + * @example new Book({ replacements: "blobUrl" }) + */ +class Book { + constructor(url, options) { + // Allow passing just options to the Book + if (typeof(options) === "undefined" && + typeof(url) !== "string" && + url instanceof Blob === false && + url instanceof ArrayBuffer === false) { + options = url; + url = undefined; + } + + this.settings = extend(this.settings || {}, { + requestMethod: undefined, + requestCredentials: undefined, + requestHeaders: undefined, + encoding: undefined, + replacements: undefined, + canonical: undefined, + openAs: undefined, + store: undefined + }); + + extend(this.settings, options); + + + // Promises + this.opening = new defer(); + /** + * @member {promise} opened returns after the book is loaded + * @memberof Book + */ + this.opened = this.opening.promise; + this.isOpen = false; + + this.loading = { + manifest: new defer(), + spine: new defer(), + metadata: new defer(), + cover: new defer(), + navigation: new defer(), + pageList: new defer(), + resources: new defer(), + displayOptions: new defer() + }; + + this.loaded = { + manifest: this.loading.manifest.promise, + spine: this.loading.spine.promise, + metadata: this.loading.metadata.promise, + cover: this.loading.cover.promise, + navigation: this.loading.navigation.promise, + pageList: this.loading.pageList.promise, + resources: this.loading.resources.promise, + displayOptions: this.loading.displayOptions.promise + }; + + /** + * @member {promise} ready returns after the book is loaded and parsed + * @memberof Book + * @private + */ + this.ready = Promise.all([ + this.loaded.manifest, + this.loaded.spine, + this.loaded.metadata, + this.loaded.cover, + this.loaded.navigation, + this.loaded.resources, + this.loaded.displayOptions + ]); + + + // Queue for methods used before opening + this.isRendered = false; + // this._q = queue(this); + + /** + * @member {method} request + * @memberof Book + * @private + */ + this.request = this.settings.requestMethod || request; + + /** + * @member {Spine} spine + * @memberof Book + */ + this.spine = new Spine(); + + /** + * @member {Locations} locations + * @memberof Book + */ + this.locations = new Locations(this.spine, this.load.bind(this)); + + /** + * @member {Navigation} navigation + * @memberof Book + */ + this.navigation = undefined; + + /** + * @member {PageList} pagelist + * @memberof Book + */ + this.pageList = undefined; + + /** + * @member {Url} url + * @memberof Book + * @private + */ + this.url = undefined; + + /** + * @member {Path} path + * @memberof Book + * @private + */ + this.path = undefined; + + /** + * @member {boolean} archived + * @memberof Book + * @private + */ + this.archived = false; + + /** + * @member {Archive} archive + * @memberof Book + * @private + */ + this.archive = undefined; + + /** + * @member {Store} storage + * @memberof Book + * @private + */ + this.storage = undefined; + + /** + * @member {Resources} resources + * @memberof Book + * @private + */ + this.resources = undefined; + + /** + * @member {Rendition} rendition + * @memberof Book + * @private + */ + this.rendition = undefined; + + /** + * @member {Container} container + * @memberof Book + * @private + */ + this.container = undefined; + + /** + * @member {Packaging} packaging + * @memberof Book + * @private + */ + this.packaging = undefined; + + /** + * @member {DisplayOptions} displayOptions + * @memberof DisplayOptions + * @private + */ + this.displayOptions = undefined; + + // this.toc = undefined; + if (this.settings.store) { + this.store(this.settings.store); + } + + if(url) { + this.open(url, this.settings.openAs).catch((error) => { + var err = new Error("Cannot load book at "+ url ); + this.emit(EVENTS.BOOK.OPEN_FAILED, err); + }); + } + } + + /** + * Open a epub or url + * @param {string | ArrayBuffer} input Url, Path or ArrayBuffer + * @param {string} [what="binary", "base64", "epub", "opf", "json", "directory"] force opening as a certain type + * @returns {Promise} of when the book has been loaded + * @example book.open("/path/to/book.epub") + */ + open(input, what) { + var opening; + var type = what || this.determineType(input); + + if (type === INPUT_TYPE.BINARY) { + this.archived = true; + this.url = new Url("/", ""); + opening = this.openEpub(input); + } else if (type === INPUT_TYPE.BASE64) { + this.archived = true; + this.url = new Url("/", ""); + opening = this.openEpub(input, type); + } else if (type === INPUT_TYPE.EPUB) { + this.archived = true; + this.url = new Url("/", ""); + opening = this.request(input, "binary", this.settings.requestCredentials, this.settings.requestHeaders) + .then(this.openEpub.bind(this)); + } else if(type == INPUT_TYPE.OPF) { + this.url = new Url(input); + opening = this.openPackaging(this.url.Path.toString()); + } else if(type == INPUT_TYPE.MANIFEST) { + this.url = new Url(input); + opening = this.openManifest(this.url.Path.toString()); + } else { + this.url = new Url(input); + opening = this.openContainer(CONTAINER_PATH) + .then(this.openPackaging.bind(this)); + } + + return opening; + } + + /** + * Open an archived epub + * @private + * @param {binary} data + * @param {string} [encoding] + * @return {Promise} + */ + openEpub(data, encoding) { + return this.unarchive(data, encoding || this.settings.encoding) + .then(() => { + return this.openContainer(CONTAINER_PATH); + }) + .then((packagePath) => { + return this.openPackaging(packagePath); + }); + } + + /** + * Open the epub container + * @private + * @param {string} url + * @return {string} packagePath + */ + openContainer(url) { + return this.load(url) + .then((xml) => { + this.container = new Container(xml); + return this.resolve(this.container.packagePath); + }); + } + + /** + * Open the Open Packaging Format Xml + * @private + * @param {string} url + * @return {Promise} + */ + openPackaging(url) { + this.path = new Path(url); + return this.load(url) + .then((xml) => { + this.packaging = new Packaging(xml); + return this.unpack(this.packaging); + }); + } + + /** + * Open the manifest JSON + * @private + * @param {string} url + * @return {Promise} + */ + openManifest(url) { + this.path = new Path(url); + return this.load(url) + .then((json) => { + this.packaging = new Packaging(); + this.packaging.load(json); + return this.unpack(this.packaging); + }); + } + + /** + * Load a resource from the Book + * @param {string} path path to the resource to load + * @return {Promise} returns a promise with the requested resource + */ + load(path) { + var resolved = this.resolve(path); + if(this.archived) { + return this.archive.request(resolved); + } else { + return this.request(resolved, null, this.settings.requestCredentials, this.settings.requestHeaders); + } + } + + /** + * Resolve a path to it's absolute position in the Book + * @param {string} path + * @param {boolean} [absolute] force resolving the full URL + * @return {string} the resolved path string + */ + resolve(path, absolute) { + if (!path) { + return; + } + var resolved = path; + var isAbsolute = (path.indexOf("://") > -1); + + if (isAbsolute) { + return path; + } + + if (this.path) { + resolved = this.path.resolve(path); + } + + if(absolute != false && this.url) { + resolved = this.url.resolve(resolved); + } + + return resolved; + } + + /** + * Get a canonical link to a path + * @param {string} path + * @return {string} the canonical path string + */ + canonical(path) { + var url = path; + + if (!path) { + return ""; + } + + if (this.settings.canonical) { + url = this.settings.canonical(path); + } else { + url = this.resolve(path, true); + } + + return url; + } + + /** + * Determine the type of they input passed to open + * @private + * @param {string} input + * @return {string} binary | directory | epub | opf + */ + determineType(input) { + var url; + var path; + var extension; + + if (this.settings.encoding === "base64") { + return INPUT_TYPE.BASE64; + } + + if(typeof(input) != "string") { + return INPUT_TYPE.BINARY; + } + + url = new Url(input); + path = url.path(); + extension = path.extension; + + // If there's a search string, remove it before determining type + if (extension) { + extension = extension.replace(/\?.*$/, ""); + } + + if (!extension) { + return INPUT_TYPE.DIRECTORY; + } + + if(extension === "epub"){ + return INPUT_TYPE.EPUB; + } + + if(extension === "opf"){ + return INPUT_TYPE.OPF; + } + + if(extension === "json"){ + return INPUT_TYPE.MANIFEST; + } + } + + + /** + * unpack the contents of the Books packaging + * @private + * @param {Packaging} packaging object + */ + unpack(packaging) { + this.package = packaging; //TODO: deprecated this + + if (this.packaging.metadata.layout === "") { + // rendition:layout not set - check display options if book is pre-paginated + this.load(this.url.resolve(IBOOKS_DISPLAY_OPTIONS_PATH)).then((xml) => { + this.displayOptions = new DisplayOptions(xml); + this.loading.displayOptions.resolve(this.displayOptions); + }).catch((err) => { + this.displayOptions = new DisplayOptions(); + this.loading.displayOptions.resolve(this.displayOptions); + }); + } else { + this.displayOptions = new DisplayOptions(); + this.loading.displayOptions.resolve(this.displayOptions); + } + + this.spine.unpack(this.packaging, this.resolve.bind(this), this.canonical.bind(this)); + + this.resources = new Resources(this.packaging.manifest, { + archive: this.archive, + resolver: this.resolve.bind(this), + request: this.request.bind(this), + replacements: this.settings.replacements || (this.archived ? "blobUrl" : "base64") + }); + + this.loadNavigation(this.packaging).then(() => { + // this.toc = this.navigation.toc; + this.loading.navigation.resolve(this.navigation); + }); + + if (this.packaging.coverPath) { + this.cover = this.resolve(this.packaging.coverPath); + } + // Resolve promises + this.loading.manifest.resolve(this.packaging.manifest); + this.loading.metadata.resolve(this.packaging.metadata); + this.loading.spine.resolve(this.spine); + this.loading.cover.resolve(this.cover); + this.loading.resources.resolve(this.resources); + this.loading.pageList.resolve(this.pageList); + + this.isOpen = true; + + if(this.archived || this.settings.replacements && this.settings.replacements != "none") { + this.replacements().then(() => { + this.loaded.displayOptions.then(() => { + this.opening.resolve(this); + }); + }) + .catch((err) => { + console.error(err); + }); + } else { + // Resolve book opened promise + this.loaded.displayOptions.then(() => { + this.opening.resolve(this); + }); + } + + } + + /** + * Load Navigation and PageList from package + * @private + * @param {Packaging} packaging + */ + loadNavigation(packaging) { + let navPath = packaging.navPath || packaging.ncxPath; + let toc = packaging.toc; + + // From json manifest + if (toc) { + return new Promise((resolve, reject) => { + this.navigation = new Navigation(toc); + + if (packaging.pageList) { + this.pageList = new PageList(packaging.pageList); // TODO: handle page lists from Manifest + } + + resolve(this.navigation); + }); + } + + if (!navPath) { + return new Promise((resolve, reject) => { + this.navigation = new Navigation(); + this.pageList = new PageList(); + + resolve(this.navigation); + }); + } + + return this.load(navPath, "xml") + .then((xml) => { + this.navigation = new Navigation(xml); + this.pageList = new PageList(xml); + return this.navigation; + }); + } + + /** + * Gets a Section of the Book from the Spine + * Alias for `book.spine.get` + * @param {string} target + * @return {Section} + */ + section(target) { + return this.spine.get(target); + } + + /** + * Sugar to render a book to an element + * @param {element | string} element element or string to add a rendition to + * @param {object} [options] + * @return {Rendition} + */ + renderTo(element, options) { + this.rendition = new Rendition(this, options); + this.rendition.attachTo(element); + + return this.rendition; + } + + /** + * Set if request should use withCredentials + * @param {boolean} credentials + */ + setRequestCredentials(credentials) { + this.settings.requestCredentials = credentials; + } + + /** + * Set headers request should use + * @param {object} headers + */ + setRequestHeaders(headers) { + this.settings.requestHeaders = headers; + } + + /** + * Unarchive a zipped epub + * @private + * @param {binary} input epub data + * @param {string} [encoding] + * @return {Archive} + */ + unarchive(input, encoding) { + this.archive = new Archive(); + return this.archive.open(input, encoding); + } + + /** + * Store the epubs contents + * @private + * @param {binary} input epub data + * @param {string} [encoding] + * @return {Store} + */ + store(name) { + // Use "blobUrl" or "base64" for replacements + let replacementsSetting = this.settings.replacements && this.settings.replacements !== "none"; + // Save original url + let originalUrl = this.url; + // Save original request method + let requester = this.settings.requestMethod || request.bind(this); + // Create new Store + this.storage = new Store(name, requester, this.resolve.bind(this)); + // Replace request method to go through store + this.request = this.storage.request.bind(this.storage); + + this.opened.then(() => { + if (this.archived) { + this.storage.requester = this.archive.request.bind(this.archive); + } + // Substitute hook + let substituteResources = (output, section) => { + section.output = this.resources.substitute(output, section.url); + }; + + // Set to use replacements + this.resources.settings.replacements = replacementsSetting || "blobUrl"; + // Create replacement urls + this.resources.replacements(). + then(() => { + return this.resources.replaceCss(); + }); + + this.storage.on("offline", () => { + // Remove url to use relative resolving for hrefs + this.url = new Url("/", ""); + // Add hook to replace resources in contents + this.spine.hooks.serialize.register(substituteResources); + }); + + this.storage.on("online", () => { + // Restore original url + this.url = originalUrl; + // Remove hook + this.spine.hooks.serialize.deregister(substituteResources); + }); + + }); + + return this.storage; + } + + /** + * Get the cover url + * @return {Promise} coverUrl + */ + coverUrl() { + return this.loaded.cover.then(() => { + if (!this.cover) { + return null; + } + + if (this.archived) { + return this.archive.createUrl(this.cover); + } else { + return this.cover; + } + }); + } + + /** + * Load replacement urls + * @private + * @return {Promise} completed loading urls + */ + replacements() { + this.spine.hooks.serialize.register((output, section) => { + section.output = this.resources.substitute(output, section.url); + }); + + return this.resources.replacements(). + then(() => { + return this.resources.replaceCss(); + }); + } + + /** + * Find a DOM Range for a given CFI Range + * @param {EpubCFI} cfiRange a epub cfi range + * @return {Promise} + */ + getRange(cfiRange) { + var cfi = new EpubCFI(cfiRange); + var item = this.spine.get(cfi.spinePos); + var _request = this.load.bind(this); + if (!item) { + return new Promise((resolve, reject) => { + reject("CFI could not be found"); + }); + } + return item.load(_request).then(function (contents) { + var range = cfi.toRange(item.document); + return range; + }); + } + + /** + * Generates the Book Key using the identifer in the manifest or other string provided + * @param {string} [identifier] to use instead of metadata identifier + * @return {string} key + */ + key(identifier) { + var ident = identifier || this.packaging.metadata.identifier || this.url.filename; + return `epubjs:${EPUBJS_VERSION}:${ident}`; + } + + /** + * Destroy the Book and all associated objects + */ + destroy() { + this.opened = undefined; + this.loading = undefined; + this.loaded = undefined; + this.ready = undefined; + + this.isOpen = false; + this.isRendered = false; + + this.spine && this.spine.destroy(); + this.locations && this.locations.destroy(); + this.pageList && this.pageList.destroy(); + this.archive && this.archive.destroy(); + this.resources && this.resources.destroy(); + this.container && this.container.destroy(); + this.packaging && this.packaging.destroy(); + this.rendition && this.rendition.destroy(); + this.displayOptions && this.displayOptions.destroy(); + + this.spine = undefined; + this.locations = undefined; + this.pageList = undefined; + this.archive = undefined; + this.resources = undefined; + this.container = undefined; + this.packaging = undefined; + this.rendition = undefined; + + this.navigation = undefined; + this.url = undefined; + this.path = undefined; + this.archived = false; + } + +} + +//-- Enable binding events to book +EventEmitter(Book.prototype); + +export default Book; -- cgit v1.2.3