summaryrefslogtreecommitdiff
path: root/lib/epub.js/src
diff options
context:
space:
mode:
Diffstat (limited to 'lib/epub.js/src')
-rw-r--r--lib/epub.js/src/annotations.js301
-rw-r--r--lib/epub.js/src/archive.js255
-rw-r--r--lib/epub.js/src/book.js768
-rw-r--r--lib/epub.js/src/container.js50
-rw-r--r--lib/epub.js/src/contents.js1264
-rw-r--r--lib/epub.js/src/displayoptions.js70
-rw-r--r--lib/epub.js/src/epub.js35
-rw-r--r--lib/epub.js/src/epubcfi.js1048
-rw-r--r--lib/epub.js/src/index.js15
-rw-r--r--lib/epub.js/src/layout.js260
-rw-r--r--lib/epub.js/src/locations.js501
-rw-r--r--lib/epub.js/src/managers/continuous/index.js588
-rw-r--r--lib/epub.js/src/managers/default/index.js1073
-rw-r--r--lib/epub.js/src/managers/helpers/snap.js338
-rw-r--r--lib/epub.js/src/managers/helpers/stage.js363
-rw-r--r--lib/epub.js/src/managers/helpers/views.js167
-rw-r--r--lib/epub.js/src/managers/views/iframe.js835
-rw-r--r--lib/epub.js/src/managers/views/inline.js432
-rw-r--r--lib/epub.js/src/mapping.js511
-rw-r--r--lib/epub.js/src/navigation.js356
-rw-r--r--lib/epub.js/src/packaging.js372
-rw-r--r--lib/epub.js/src/pagelist.js274
-rw-r--r--lib/epub.js/src/rendition.js1064
-rw-r--r--lib/epub.js/src/resources.js320
-rw-r--r--lib/epub.js/src/section.js323
-rw-r--r--lib/epub.js/src/spine.js274
-rw-r--r--lib/epub.js/src/store.js384
-rw-r--r--lib/epub.js/src/themes.js268
-rw-r--r--lib/epub.js/src/utils/constants.js62
-rw-r--r--lib/epub.js/src/utils/core.js876
-rw-r--r--lib/epub.js/src/utils/hook.js82
-rw-r--r--lib/epub.js/src/utils/mime.js169
-rw-r--r--lib/epub.js/src/utils/path.js102
-rw-r--r--lib/epub.js/src/utils/queue.js246
-rw-r--r--lib/epub.js/src/utils/replacements.js138
-rw-r--r--lib/epub.js/src/utils/request.js150
-rw-r--r--lib/epub.js/src/utils/scrolltype.js55
-rw-r--r--lib/epub.js/src/utils/url.js108
38 files changed, 14497 insertions, 0 deletions
diff --git a/lib/epub.js/src/annotations.js b/lib/epub.js/src/annotations.js
new file mode 100644
index 0000000..ecbcb56
--- /dev/null
+++ b/lib/epub.js/src/annotations.js
@@ -0,0 +1,301 @@
+import EventEmitter from "event-emitter";
+import EpubCFI from "./epubcfi";
+import { EVENTS } from "./utils/constants";
+
+/**
+ * Handles managing adding & removing Annotations
+ * @param {Rendition} rendition
+ * @class
+ */
+class Annotations {
+
+ constructor (rendition) {
+ this.rendition = rendition;
+ this.highlights = [];
+ this.underlines = [];
+ this.marks = [];
+ this._annotations = {};
+ this._annotationsBySectionIndex = {};
+
+ this.rendition.hooks.render.register(this.inject.bind(this));
+ this.rendition.hooks.unloaded.register(this.clear.bind(this));
+ }
+
+ /**
+ * Add an annotation to store
+ * @param {string} type Type of annotation to add: "highlight", "underline", "mark"
+ * @param {EpubCFI} cfiRange EpubCFI range to attach annotation to
+ * @param {object} data Data to assign to annotation
+ * @param {function} [cb] Callback after annotation is added
+ * @param {string} className CSS class to assign to annotation
+ * @param {object} styles CSS styles to assign to annotation
+ * @returns {Annotation} annotation
+ */
+ add (type, cfiRange, data, cb, className, styles) {
+ let hash = encodeURI(cfiRange + type);
+ let cfi = new EpubCFI(cfiRange);
+ let sectionIndex = cfi.spinePos;
+ let annotation = new Annotation({
+ type,
+ cfiRange,
+ data,
+ sectionIndex,
+ cb,
+ className,
+ styles
+ });
+
+ this._annotations[hash] = annotation;
+
+ if (sectionIndex in this._annotationsBySectionIndex) {
+ this._annotationsBySectionIndex[sectionIndex].push(hash);
+ } else {
+ this._annotationsBySectionIndex[sectionIndex] = [hash];
+ }
+
+ let views = this.rendition.views();
+
+ views.forEach( (view) => {
+ if (annotation.sectionIndex === view.index) {
+ annotation.attach(view);
+ }
+ });
+
+ return annotation;
+ }
+
+ /**
+ * Remove an annotation from store
+ * @param {EpubCFI} cfiRange EpubCFI range the annotation is attached to
+ * @param {string} type Type of annotation to add: "highlight", "underline", "mark"
+ */
+ remove (cfiRange, type) {
+ let hash = encodeURI(cfiRange + type);
+
+ if (hash in this._annotations) {
+ let annotation = this._annotations[hash];
+
+ if (type && annotation.type !== type) {
+ return;
+ }
+
+ let views = this.rendition.views();
+ views.forEach( (view) => {
+ this._removeFromAnnotationBySectionIndex(annotation.sectionIndex, hash);
+ if (annotation.sectionIndex === view.index) {
+ annotation.detach(view);
+ }
+ });
+
+ delete this._annotations[hash];
+ }
+ }
+
+ /**
+ * Remove an annotations by Section Index
+ * @private
+ */
+ _removeFromAnnotationBySectionIndex (sectionIndex, hash) {
+ this._annotationsBySectionIndex[sectionIndex] = this._annotationsAt(sectionIndex).filter(h => h !== hash);
+ }
+
+ /**
+ * Get annotations by Section Index
+ * @private
+ */
+ _annotationsAt (index) {
+ return this._annotationsBySectionIndex[index];
+ }
+
+
+ /**
+ * Add a highlight to the store
+ * @param {EpubCFI} cfiRange EpubCFI range to attach annotation to
+ * @param {object} data Data to assign to annotation
+ * @param {function} cb Callback after annotation is clicked
+ * @param {string} className CSS class to assign to annotation
+ * @param {object} styles CSS styles to assign to annotation
+ */
+ highlight (cfiRange, data, cb, className, styles) {
+ return this.add("highlight", cfiRange, data, cb, className, styles);
+ }
+
+ /**
+ * Add a underline to the store
+ * @param {EpubCFI} cfiRange EpubCFI range to attach annotation to
+ * @param {object} data Data to assign to annotation
+ * @param {function} cb Callback after annotation is clicked
+ * @param {string} className CSS class to assign to annotation
+ * @param {object} styles CSS styles to assign to annotation
+ */
+ underline (cfiRange, data, cb, className, styles) {
+ return this.add("underline", cfiRange, data, cb, className, styles);
+ }
+
+ /**
+ * Add a mark to the store
+ * @param {EpubCFI} cfiRange EpubCFI range to attach annotation to
+ * @param {object} data Data to assign to annotation
+ * @param {function} cb Callback after annotation is clicked
+ */
+ mark (cfiRange, data, cb) {
+ return this.add("mark", cfiRange, data, cb);
+ }
+
+ /**
+ * iterate over annotations in the store
+ */
+ each () {
+ return this._annotations.forEach.apply(this._annotations, arguments);
+ }
+
+ /**
+ * Hook for injecting annotation into a view
+ * @param {View} view
+ * @private
+ */
+ inject (view) {
+ let sectionIndex = view.index;
+ if (sectionIndex in this._annotationsBySectionIndex) {
+ let annotations = this._annotationsBySectionIndex[sectionIndex];
+ annotations.forEach((hash) => {
+ let annotation = this._annotations[hash];
+ annotation.attach(view);
+ });
+ }
+ }
+
+ /**
+ * Hook for removing annotation from a view
+ * @param {View} view
+ * @private
+ */
+ clear (view) {
+ let sectionIndex = view.index;
+ if (sectionIndex in this._annotationsBySectionIndex) {
+ let annotations = this._annotationsBySectionIndex[sectionIndex];
+ annotations.forEach((hash) => {
+ let annotation = this._annotations[hash];
+ annotation.detach(view);
+ });
+ }
+ }
+
+ /**
+ * [Not Implemented] Show annotations
+ * @TODO: needs implementation in View
+ */
+ show () {
+
+ }
+
+ /**
+ * [Not Implemented] Hide annotations
+ * @TODO: needs implementation in View
+ */
+ hide () {
+
+ }
+
+}
+
+/**
+ * Annotation object
+ * @class
+ * @param {object} options
+ * @param {string} options.type Type of annotation to add: "highlight", "underline", "mark"
+ * @param {EpubCFI} options.cfiRange EpubCFI range to attach annotation to
+ * @param {object} options.data Data to assign to annotation
+ * @param {int} options.sectionIndex Index in the Spine of the Section annotation belongs to
+ * @param {function} [options.cb] Callback after annotation is clicked
+ * @param {string} className CSS class to assign to annotation
+ * @param {object} styles CSS styles to assign to annotation
+ * @returns {Annotation} annotation
+ */
+class Annotation {
+
+ constructor ({
+ type,
+ cfiRange,
+ data,
+ sectionIndex,
+ cb,
+ className,
+ styles
+ }) {
+ this.type = type;
+ this.cfiRange = cfiRange;
+ this.data = data;
+ this.sectionIndex = sectionIndex;
+ this.mark = undefined;
+ this.cb = cb;
+ this.className = className;
+ this.styles = styles;
+ }
+
+ /**
+ * Update stored data
+ * @param {object} data
+ */
+ update (data) {
+ this.data = data;
+ }
+
+ /**
+ * Add to a view
+ * @param {View} view
+ */
+ attach (view) {
+ let {cfiRange, data, type, mark, cb, className, styles} = this;
+ let result;
+
+ if (type === "highlight") {
+ result = view.highlight(cfiRange, data, cb, className, styles);
+ } else if (type === "underline") {
+ result = view.underline(cfiRange, data, cb, className, styles);
+ } else if (type === "mark") {
+ result = view.mark(cfiRange, data, cb);
+ }
+
+ this.mark = result;
+ this.emit(EVENTS.ANNOTATION.ATTACH, result);
+ return result;
+ }
+
+ /**
+ * Remove from a view
+ * @param {View} view
+ */
+ detach (view) {
+ let {cfiRange, type} = this;
+ let result;
+
+ if (view) {
+ if (type === "highlight") {
+ result = view.unhighlight(cfiRange);
+ } else if (type === "underline") {
+ result = view.ununderline(cfiRange);
+ } else if (type === "mark") {
+ result = view.unmark(cfiRange);
+ }
+ }
+
+ this.mark = undefined;
+ this.emit(EVENTS.ANNOTATION.DETACH, result);
+ return result;
+ }
+
+ /**
+ * [Not Implemented] Get text of an annotation
+ * @TODO: needs implementation in contents
+ */
+ text () {
+
+ }
+
+}
+
+EventEmitter(Annotation.prototype);
+
+
+export default Annotations
diff --git a/lib/epub.js/src/archive.js b/lib/epub.js/src/archive.js
new file mode 100644
index 0000000..ea22a81
--- /dev/null
+++ b/lib/epub.js/src/archive.js
@@ -0,0 +1,255 @@
+import {defer, isXml, parse} from "./utils/core";
+import request from "./utils/request";
+import mime from "./utils/mime";
+import Path from "./utils/path";
+import JSZip from "jszip/dist/jszip";
+
+/**
+ * Handles Unzipping a requesting files from an Epub Archive
+ * @class
+ */
+class Archive {
+
+ constructor() {
+ this.zip = undefined;
+ this.urlCache = {};
+
+ this.checkRequirements();
+
+ }
+
+ /**
+ * Checks to see if JSZip exists in global namspace,
+ * Requires JSZip if it isn't there
+ * @private
+ */
+ checkRequirements(){
+ try {
+ this.zip = new JSZip();
+ } catch (e) {
+ throw new Error("JSZip lib not loaded");
+ }
+ }
+
+ /**
+ * Open an archive
+ * @param {binary} input
+ * @param {boolean} [isBase64] tells JSZip if the input data is base64 encoded
+ * @return {Promise} zipfile
+ */
+ open(input, isBase64){
+ return this.zip.loadAsync(input, {"base64": isBase64});
+ }
+
+ /**
+ * Load and Open an archive
+ * @param {string} zipUrl
+ * @param {boolean} [isBase64] tells JSZip if the input data is base64 encoded
+ * @return {Promise} zipfile
+ */
+ openUrl(zipUrl, isBase64){
+ return request(zipUrl, "binary")
+ .then(function(data){
+ return this.zip.loadAsync(data, {"base64": isBase64});
+ }.bind(this));
+ }
+
+ /**
+ * Request a url from the archive
+ * @param {string} url a url to request from the archive
+ * @param {string} [type] specify the type of the returned result
+ * @return {Promise<Blob | string | JSON | Document | XMLDocument>}
+ */
+ request(url, type){
+ var deferred = new defer();
+ var response;
+ var path = new Path(url);
+
+ // If type isn't set, determine it from the file extension
+ if(!type) {
+ type = path.extension;
+ }
+
+ if(type == "blob"){
+ response = this.getBlob(url);
+ } else {
+ response = this.getText(url);
+ }
+
+ if (response) {
+ response.then(function (r) {
+ let result = this.handleResponse(r, type);
+ deferred.resolve(result);
+ }.bind(this));
+ } else {
+ deferred.reject({
+ message : "File not found in the epub: " + url,
+ stack : new Error().stack
+ });
+ }
+ return deferred.promise;
+ }
+
+ /**
+ * Handle the response from request
+ * @private
+ * @param {any} response
+ * @param {string} [type]
+ * @return {any} the parsed result
+ */
+ handleResponse(response, type){
+ var r;
+
+ if(type == "json") {
+ r = JSON.parse(response);
+ }
+ else
+ if(isXml(type)) {
+ r = parse(response, "text/xml");
+ }
+ else
+ if(type == "xhtml") {
+ r = parse(response, "application/xhtml+xml");
+ }
+ else
+ if(type == "html" || type == "htm") {
+ r = parse(response, "text/html");
+ } else {
+ r = response;
+ }
+
+ return r;
+ }
+
+ /**
+ * Get a Blob from Archive by Url
+ * @param {string} url
+ * @param {string} [mimeType]
+ * @return {Blob}
+ */
+ getBlob(url, mimeType){
+ var decodededUrl = window.decodeURIComponent(url.substr(1)); // Remove first slash
+ var entry = this.zip.file(decodededUrl);
+
+ if(entry) {
+ mimeType = mimeType || mime.lookup(entry.name);
+ return entry.async("uint8array").then(function(uint8array) {
+ return new Blob([uint8array], {type : mimeType});
+ });
+ }
+ }
+
+ /**
+ * Get Text from Archive by Url
+ * @param {string} url
+ * @param {string} [encoding]
+ * @return {string}
+ */
+ getText(url, encoding){
+ var decodededUrl = window.decodeURIComponent(url.substr(1)); // Remove first slash
+ var entry = this.zip.file(decodededUrl);
+
+ if(entry) {
+ return entry.async("string").then(function(text) {
+ return text;
+ });
+ }
+ }
+
+ /**
+ * Get a base64 encoded result from Archive by Url
+ * @param {string} url
+ * @param {string} [mimeType]
+ * @return {string} base64 encoded
+ */
+ getBase64(url, mimeType){
+ var decodededUrl = window.decodeURIComponent(url.substr(1)); // Remove first slash
+ var entry = this.zip.file(decodededUrl);
+
+ if(entry) {
+ mimeType = mimeType || mime.lookup(entry.name);
+ return entry.async("base64").then(function(data) {
+ return "data:" + mimeType + ";base64," + data;
+ });
+ }
+ }
+
+ /**
+ * Create a Url from an unarchived item
+ * @param {string} url
+ * @param {object} [options.base64] use base64 encoding or blob url
+ * @return {Promise} url promise with Url string
+ */
+ createUrl(url, options){
+ var deferred = new defer();
+ var _URL = window.URL || window.webkitURL || window.mozURL;
+ var tempUrl;
+ var response;
+ var useBase64 = options && options.base64;
+
+ if(url in this.urlCache) {
+ deferred.resolve(this.urlCache[url]);
+ return deferred.promise;
+ }
+
+ if (useBase64) {
+ response = this.getBase64(url);
+
+ if (response) {
+ response.then(function(tempUrl) {
+
+ this.urlCache[url] = tempUrl;
+ deferred.resolve(tempUrl);
+
+ }.bind(this));
+
+ }
+
+ } else {
+
+ response = this.getBlob(url);
+
+ if (response) {
+ response.then(function(blob) {
+
+ tempUrl = _URL.createObjectURL(blob);
+ this.urlCache[url] = tempUrl;
+ deferred.resolve(tempUrl);
+
+ }.bind(this));
+
+ }
+ }
+
+
+ if (!response) {
+ deferred.reject({
+ message : "File not found in the epub: " + url,
+ stack : new Error().stack
+ });
+ }
+
+ return deferred.promise;
+ }
+
+ /**
+ * Revoke Temp Url for a achive item
+ * @param {string} url url of the item in the archive
+ */
+ revokeUrl(url){
+ var _URL = window.URL || window.webkitURL || window.mozURL;
+ var fromCache = this.urlCache[url];
+ if(fromCache) _URL.revokeObjectURL(fromCache);
+ }
+
+ destroy() {
+ var _URL = window.URL || window.webkitURL || window.mozURL;
+ for (let fromCache in this.urlCache) {
+ _URL.revokeObjectURL(fromCache);
+ }
+ this.zip = undefined;
+ this.urlCache = {};
+ }
+}
+
+export default Archive;
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<?string>} 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;
diff --git a/lib/epub.js/src/container.js b/lib/epub.js/src/container.js
new file mode 100644
index 0000000..f3a214f
--- /dev/null
+++ b/lib/epub.js/src/container.js
@@ -0,0 +1,50 @@
+import path from "path-webpack";
+import {qs} from "./utils/core";
+
+/**
+ * Handles Parsing and Accessing an Epub Container
+ * @class
+ * @param {document} [containerDocument] xml document
+ */
+class Container {
+ constructor(containerDocument) {
+ this.packagePath = '';
+ this.directory = '';
+ this.encoding = '';
+
+ if (containerDocument) {
+ this.parse(containerDocument);
+ }
+ }
+
+ /**
+ * Parse the Container XML
+ * @param {document} containerDocument
+ */
+ parse(containerDocument){
+ //-- <rootfile full-path="OPS/package.opf" media-type="application/oebps-package+xml"/>
+ var rootfile;
+
+ if(!containerDocument) {
+ throw new Error("Container File Not Found");
+ }
+
+ rootfile = qs(containerDocument, "rootfile");
+
+ if(!rootfile) {
+ throw new Error("No RootFile Found");
+ }
+
+ this.packagePath = rootfile.getAttribute("full-path");
+ this.directory = path.dirname(this.packagePath);
+ this.encoding = containerDocument.xmlEncoding;
+ }
+
+ destroy() {
+ this.packagePath = undefined;
+ this.directory = undefined;
+ this.encoding = undefined;
+ }
+}
+
+export default Container;
diff --git a/lib/epub.js/src/contents.js b/lib/epub.js/src/contents.js
new file mode 100644
index 0000000..3effe72
--- /dev/null
+++ b/lib/epub.js/src/contents.js
@@ -0,0 +1,1264 @@
+import EventEmitter from "event-emitter";
+import {isNumber, prefixed, borders, defaults} from "./utils/core";
+import EpubCFI from "./epubcfi";
+import Mapping from "./mapping";
+import {replaceLinks} from "./utils/replacements";
+import { EPUBJS_VERSION, EVENTS, DOM_EVENTS } from "./utils/constants";
+
+const hasNavigator = typeof (navigator) !== "undefined";
+
+const isChrome = hasNavigator && /Chrome/.test(navigator.userAgent);
+const isWebkit = hasNavigator && !isChrome && /AppleWebKit/.test(navigator.userAgent);
+
+const ELEMENT_NODE = 1;
+const TEXT_NODE = 3;
+
+/**
+ * Handles DOM manipulation, queries and events for View contents
+ * @class
+ * @param {document} doc Document
+ * @param {element} content Parent Element (typically Body)
+ * @param {string} cfiBase Section component of CFIs
+ * @param {number} sectionIndex Index in Spine of Conntent's Section
+ */
+class Contents {
+ constructor(doc, content, cfiBase, sectionIndex) {
+ // Blank Cfi for Parsing
+ this.epubcfi = new EpubCFI();
+
+ this.document = doc;
+ this.documentElement = this.document.documentElement;
+ this.content = content || this.document.body;
+ this.window = this.document.defaultView;
+
+ this._size = {
+ width: 0,
+ height: 0
+ };
+
+ this.sectionIndex = sectionIndex || 0;
+ this.cfiBase = cfiBase || "";
+
+ this.epubReadingSystem("epub.js", EPUBJS_VERSION);
+ this.called = 0;
+ this.active = true;
+ this.listeners();
+ }
+
+ /**
+ * Get DOM events that are listened for and passed along
+ */
+ static get listenedEvents() {
+ return DOM_EVENTS;
+ }
+
+ /**
+ * Get or Set width
+ * @param {number} [w]
+ * @returns {number} width
+ */
+ width(w) {
+ // var frame = this.documentElement;
+ var frame = this.content;
+
+ if (w && isNumber(w)) {
+ w = w + "px";
+ }
+
+ if (w) {
+ frame.style.width = w;
+ // this.content.style.width = w;
+ }
+
+ return parseInt(this.window.getComputedStyle(frame)["width"]);
+
+
+ }
+
+ /**
+ * Get or Set height
+ * @param {number} [h]
+ * @returns {number} height
+ */
+ height(h) {
+ // var frame = this.documentElement;
+ var frame = this.content;
+
+ if (h && isNumber(h)) {
+ h = h + "px";
+ }
+
+ if (h) {
+ frame.style.height = h;
+ // this.content.style.height = h;
+ }
+
+ return parseInt(this.window.getComputedStyle(frame)["height"]);
+
+ }
+
+ /**
+ * Get or Set width of the contents
+ * @param {number} [w]
+ * @returns {number} width
+ */
+ contentWidth(w) {
+
+ var content = this.content || this.document.body;
+
+ if (w && isNumber(w)) {
+ w = w + "px";
+ }
+
+ if (w) {
+ content.style.width = w;
+ }
+
+ return parseInt(this.window.getComputedStyle(content)["width"]);
+
+
+ }
+
+ /**
+ * Get or Set height of the contents
+ * @param {number} [h]
+ * @returns {number} height
+ */
+ contentHeight(h) {
+
+ var content = this.content || this.document.body;
+
+ if (h && isNumber(h)) {
+ h = h + "px";
+ }
+
+ if (h) {
+ content.style.height = h;
+ }
+
+ return parseInt(this.window.getComputedStyle(content)["height"]);
+
+ }
+
+ /**
+ * Get the width of the text using Range
+ * @returns {number} width
+ */
+ textWidth() {
+ let rect;
+ let width;
+ let range = this.document.createRange();
+ let content = this.content || this.document.body;
+ let border = borders(content);
+
+ // Select the contents of frame
+ range.selectNodeContents(content);
+
+ // get the width of the text content
+ rect = range.getBoundingClientRect();
+ width = rect.width;
+
+ if (border && border.width) {
+ width += border.width;
+ }
+
+ return Math.round(width);
+ }
+
+ /**
+ * Get the height of the text using Range
+ * @returns {number} height
+ */
+ textHeight() {
+ let rect;
+ let height;
+ let range = this.document.createRange();
+ let content = this.content || this.document.body;
+
+ range.selectNodeContents(content);
+
+ rect = range.getBoundingClientRect();
+ height = rect.bottom;
+
+ return Math.round(height);
+ }
+
+ /**
+ * Get documentElement scrollWidth
+ * @returns {number} width
+ */
+ scrollWidth() {
+ var width = this.documentElement.scrollWidth;
+
+ return width;
+ }
+
+ /**
+ * Get documentElement scrollHeight
+ * @returns {number} height
+ */
+ scrollHeight() {
+ var height = this.documentElement.scrollHeight;
+
+ return height;
+ }
+
+ /**
+ * Set overflow css style of the contents
+ * @param {string} [overflow]
+ */
+ overflow(overflow) {
+
+ if (overflow) {
+ this.documentElement.style.overflow = overflow;
+ }
+
+ return this.window.getComputedStyle(this.documentElement)["overflow"];
+ }
+
+ /**
+ * Set overflowX css style of the documentElement
+ * @param {string} [overflow]
+ */
+ overflowX(overflow) {
+
+ if (overflow) {
+ this.documentElement.style.overflowX = overflow;
+ }
+
+ return this.window.getComputedStyle(this.documentElement)["overflowX"];
+ }
+
+ /**
+ * Set overflowY css style of the documentElement
+ * @param {string} [overflow]
+ */
+ overflowY(overflow) {
+
+ if (overflow) {
+ this.documentElement.style.overflowY = overflow;
+ }
+
+ return this.window.getComputedStyle(this.documentElement)["overflowY"];
+ }
+
+ /**
+ * Set Css styles on the contents element (typically Body)
+ * @param {string} property
+ * @param {string} value
+ * @param {boolean} [priority] set as "important"
+ */
+ css(property, value, priority) {
+ var content = this.content || this.document.body;
+
+ if (value) {
+ content.style.setProperty(property, value, priority ? "important" : "");
+ } else {
+ content.style.removeProperty(property);
+ }
+
+ return this.window.getComputedStyle(content)[property];
+ }
+
+ /**
+ * Get or Set the viewport element
+ * @param {object} [options]
+ * @param {string} [options.width]
+ * @param {string} [options.height]
+ * @param {string} [options.scale]
+ * @param {string} [options.minimum]
+ * @param {string} [options.maximum]
+ * @param {string} [options.scalable]
+ */
+ viewport(options) {
+ var _width, _height, _scale, _minimum, _maximum, _scalable;
+ // var width, height, scale, minimum, maximum, scalable;
+ var $viewport = this.document.querySelector("meta[name='viewport']");
+ var parsed = {
+ "width": undefined,
+ "height": undefined,
+ "scale": undefined,
+ "minimum": undefined,
+ "maximum": undefined,
+ "scalable": undefined
+ };
+ var newContent = [];
+ var settings = {};
+
+ /*
+ * check for the viewport size
+ * <meta name="viewport" content="width=1024,height=697" />
+ */
+ if($viewport && $viewport.hasAttribute("content")) {
+ let content = $viewport.getAttribute("content");
+ let _width = content.match(/width\s*=\s*([^,]*)/);
+ let _height = content.match(/height\s*=\s*([^,]*)/);
+ let _scale = content.match(/initial-scale\s*=\s*([^,]*)/);
+ let _minimum = content.match(/minimum-scale\s*=\s*([^,]*)/);
+ let _maximum = content.match(/maximum-scale\s*=\s*([^,]*)/);
+ let _scalable = content.match(/user-scalable\s*=\s*([^,]*)/);
+
+ if(_width && _width.length && typeof _width[1] !== "undefined"){
+ parsed.width = _width[1];
+ }
+ if(_height && _height.length && typeof _height[1] !== "undefined"){
+ parsed.height = _height[1];
+ }
+ if(_scale && _scale.length && typeof _scale[1] !== "undefined"){
+ parsed.scale = _scale[1];
+ }
+ if(_minimum && _minimum.length && typeof _minimum[1] !== "undefined"){
+ parsed.minimum = _minimum[1];
+ }
+ if(_maximum && _maximum.length && typeof _maximum[1] !== "undefined"){
+ parsed.maximum = _maximum[1];
+ }
+ if(_scalable && _scalable.length && typeof _scalable[1] !== "undefined"){
+ parsed.scalable = _scalable[1];
+ }
+ }
+
+ settings = defaults(options || {}, parsed);
+
+ if (options) {
+ if (settings.width) {
+ newContent.push("width=" + settings.width);
+ }
+
+ if (settings.height) {
+ newContent.push("height=" + settings.height);
+ }
+
+ if (settings.scale) {
+ newContent.push("initial-scale=" + settings.scale);
+ }
+
+ if (settings.scalable === "no") {
+ newContent.push("minimum-scale=" + settings.scale);
+ newContent.push("maximum-scale=" + settings.scale);
+ newContent.push("user-scalable=" + settings.scalable);
+ } else {
+
+ if (settings.scalable) {
+ newContent.push("user-scalable=" + settings.scalable);
+ }
+
+ if (settings.minimum) {
+ newContent.push("minimum-scale=" + settings.minimum);
+ }
+
+ if (settings.maximum) {
+ newContent.push("minimum-scale=" + settings.maximum);
+ }
+ }
+
+ if (!$viewport) {
+ $viewport = this.document.createElement("meta");
+ $viewport.setAttribute("name", "viewport");
+ this.document.querySelector("head").appendChild($viewport);
+ }
+
+ $viewport.setAttribute("content", newContent.join(", "));
+
+ this.window.scrollTo(0, 0);
+ }
+
+
+ return settings;
+ }
+
+ /**
+ * Event emitter for when the contents has expanded
+ * @private
+ */
+ expand() {
+ this.emit(EVENTS.CONTENTS.EXPAND);
+ }
+
+ /**
+ * Add DOM listeners
+ * @private
+ */
+ listeners() {
+ this.imageLoadListeners();
+
+ this.mediaQueryListeners();
+
+ // this.fontLoadListeners();
+
+ this.addEventListeners();
+
+ this.addSelectionListeners();
+
+ // this.transitionListeners();
+
+ if (typeof ResizeObserver === "undefined") {
+ this.resizeListeners();
+ this.visibilityListeners();
+ } else {
+ this.resizeObservers();
+ }
+
+ // this.mutationObservers();
+
+ this.linksHandler();
+ }
+
+ /**
+ * Remove DOM listeners
+ * @private
+ */
+ removeListeners() {
+
+ this.removeEventListeners();
+
+ this.removeSelectionListeners();
+
+ if (this.observer) {
+ this.observer.disconnect();
+ }
+
+ clearTimeout(this.expanding);
+ }
+
+ /**
+ * Check if size of contents has changed and
+ * emit 'resize' event if it has.
+ * @private
+ */
+ resizeCheck() {
+ let width = this.textWidth();
+ let height = this.textHeight();
+
+ if (width != this._size.width || height != this._size.height) {
+
+ this._size = {
+ width: width,
+ height: height
+ };
+
+ this.onResize && this.onResize(this._size);
+ this.emit(EVENTS.CONTENTS.RESIZE, this._size);
+ }
+ }
+
+ /**
+ * Poll for resize detection
+ * @private
+ */
+ resizeListeners() {
+ var width, height;
+ // Test size again
+ clearTimeout(this.expanding);
+ requestAnimationFrame(this.resizeCheck.bind(this));
+ this.expanding = setTimeout(this.resizeListeners.bind(this), 350);
+ }
+
+ /**
+ * Listen for visibility of tab to change
+ * @private
+ */
+ visibilityListeners() {
+ document.addEventListener("visibilitychange", () => {
+ if (document.visibilityState === "visible" && this.active === false) {
+ this.active = true;
+ this.resizeListeners();
+ } else {
+ this.active = false;
+ clearTimeout(this.expanding);
+ }
+ });
+ }
+
+ /**
+ * Use css transitions to detect resize
+ * @private
+ */
+ transitionListeners() {
+ let body = this.content;
+
+ body.style['transitionProperty'] = "font, font-size, font-size-adjust, font-stretch, font-variation-settings, font-weight, width, height";
+ body.style['transitionDuration'] = "0.001ms";
+ body.style['transitionTimingFunction'] = "linear";
+ body.style['transitionDelay'] = "0";
+
+ this._resizeCheck = this.resizeCheck.bind(this);
+ this.document.addEventListener('transitionend', this._resizeCheck);
+ }
+
+ /**
+ * Listen for media query changes and emit 'expand' event
+ * Adapted from: https://github.com/tylergaw/media-query-events/blob/master/js/mq-events.js
+ * @private
+ */
+ mediaQueryListeners() {
+ var sheets = this.document.styleSheets;
+ var mediaChangeHandler = function(m){
+ if(m.matches && !this._expanding) {
+ setTimeout(this.expand.bind(this), 1);
+ }
+ }.bind(this);
+
+ for (var i = 0; i < sheets.length; i += 1) {
+ var rules;
+ // Firefox errors if we access cssRules cross-domain
+ try {
+ rules = sheets[i].cssRules;
+ } catch (e) {
+ return;
+ }
+ if(!rules) return; // Stylesheets changed
+ for (var j = 0; j < rules.length; j += 1) {
+ //if (rules[j].constructor === CSSMediaRule) {
+ if(rules[j].media){
+ var mql = this.window.matchMedia(rules[j].media.mediaText);
+ mql.addListener(mediaChangeHandler);
+ //mql.onchange = mediaChangeHandler;
+ }
+ }
+ }
+ }
+
+ /**
+ * Use ResizeObserver to listen for changes in the DOM and check for resize
+ * @private
+ */
+ resizeObservers() {
+ // create an observer instance
+ this.observer = new ResizeObserver((e) => {
+ requestAnimationFrame(this.resizeCheck.bind(this));
+ });
+
+ // pass in the target node
+ this.observer.observe(this.document.documentElement);
+ }
+
+ /**
+ * Use MutationObserver to listen for changes in the DOM and check for resize
+ * @private
+ */
+ mutationObservers() {
+ // create an observer instance
+ this.observer = new MutationObserver((mutations) => {
+ this.resizeCheck();
+ });
+
+ // configuration of the observer:
+ let config = { attributes: true, childList: true, characterData: true, subtree: true };
+
+ // pass in the target node, as well as the observer options
+ this.observer.observe(this.document, config);
+ }
+
+ /**
+ * Test if images are loaded or add listener for when they load
+ * @private
+ */
+ imageLoadListeners() {
+ var images = this.document.querySelectorAll("img");
+ var img;
+ for (var i = 0; i < images.length; i++) {
+ img = images[i];
+
+ if (typeof img.naturalWidth !== "undefined" &&
+ img.naturalWidth === 0) {
+ img.onload = this.expand.bind(this);
+ }
+ }
+ }
+
+ /**
+ * Listen for font load and check for resize when loaded
+ * @private
+ */
+ fontLoadListeners() {
+ if (!this.document || !this.document.fonts) {
+ return;
+ }
+
+ this.document.fonts.ready.then(function () {
+ this.resizeCheck();
+ }.bind(this));
+
+ }
+
+ /**
+ * Get the documentElement
+ * @returns {element} documentElement
+ */
+ root() {
+ if(!this.document) return null;
+ return this.document.documentElement;
+ }
+
+ /**
+ * Get the location offset of a EpubCFI or an #id
+ * @param {string | EpubCFI} target
+ * @param {string} [ignoreClass] for the cfi
+ * @returns { {left: Number, top: Number }
+ */
+ locationOf(target, ignoreClass) {
+ var position;
+ var targetPos = {"left": 0, "top": 0};
+
+ if(!this.document) return targetPos;
+
+ if(this.epubcfi.isCfiString(target)) {
+ let range = new EpubCFI(target).toRange(this.document, ignoreClass);
+
+ if(range) {
+ try {
+ if (!range.endContainer ||
+ (range.startContainer == range.endContainer
+ && range.startOffset == range.endOffset)) {
+ // If the end for the range is not set, it results in collapsed becoming
+ // true. This in turn leads to inconsistent behaviour when calling
+ // getBoundingRect. Wrong bounds lead to the wrong page being displayed.
+ // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/15684911/
+ let pos = range.startContainer.textContent.indexOf(" ", range.startOffset);
+ if (pos == -1) {
+ pos = range.startContainer.textContent.length;
+ }
+ range.setEnd(range.startContainer, pos);
+ }
+ } catch (e) {
+ console.error("setting end offset to start container length failed", e);
+ }
+
+ if (range.startContainer.nodeType === Node.ELEMENT_NODE) {
+ position = range.startContainer.getBoundingClientRect();
+ targetPos.left = position.left;
+ targetPos.top = position.top;
+ } else {
+ // Webkit does not handle collapsed range bounds correctly
+ // https://bugs.webkit.org/show_bug.cgi?id=138949
+
+ // Construct a new non-collapsed range
+ if (isWebkit) {
+ let container = range.startContainer;
+ let newRange = new Range();
+ try {
+ if (container.nodeType === ELEMENT_NODE) {
+ position = container.getBoundingClientRect();
+ } else if (range.startOffset + 2 < container.length) {
+ newRange.setStart(container, range.startOffset);
+ newRange.setEnd(container, range.startOffset + 2);
+ position = newRange.getBoundingClientRect();
+ } else if (range.startOffset - 2 > 0) {
+ newRange.setStart(container, range.startOffset - 2);
+ newRange.setEnd(container, range.startOffset);
+ position = newRange.getBoundingClientRect();
+ } else { // empty, return the parent element
+ position = container.parentNode.getBoundingClientRect();
+ }
+ } catch (e) {
+ console.error(e, e.stack);
+ }
+ } else {
+ position = range.getBoundingClientRect();
+ }
+ }
+ }
+
+ } else if(typeof target === "string" &&
+ target.indexOf("#") > -1) {
+
+ let id = target.substring(target.indexOf("#")+1);
+ let el = this.document.getElementById(id);
+ if(el) {
+ if (isWebkit) {
+ // Webkit reports incorrect bounding rects in Columns
+ let newRange = new Range();
+ newRange.selectNode(el);
+ position = newRange.getBoundingClientRect();
+ } else {
+ position = el.getBoundingClientRect();
+ }
+ }
+ }
+
+ if (position) {
+ targetPos.left = position.left;
+ targetPos.top = position.top;
+ }
+
+ return targetPos;
+ }
+
+ /**
+ * Append a stylesheet link to the document head
+ * @param {string} src url
+ */
+ addStylesheet(src) {
+ return new Promise(function(resolve, reject){
+ var $stylesheet;
+ var ready = false;
+
+ if(!this.document) {
+ resolve(false);
+ return;
+ }
+
+ // Check if link already exists
+ $stylesheet = this.document.querySelector("link[href='"+src+"']");
+ if ($stylesheet) {
+ resolve(true);
+ return; // already present
+ }
+
+ $stylesheet = this.document.createElement("link");
+ $stylesheet.type = "text/css";
+ $stylesheet.rel = "stylesheet";
+ $stylesheet.href = src;
+ $stylesheet.onload = $stylesheet.onreadystatechange = function() {
+ if ( !ready && (!this.readyState || this.readyState == "complete") ) {
+ ready = true;
+ // Let apply
+ setTimeout(() => {
+ resolve(true);
+ }, 1);
+ }
+ };
+
+ this.document.head.appendChild($stylesheet);
+
+ }.bind(this));
+ }
+
+ _getStylesheetNode(key) {
+ var styleEl;
+ key = "epubjs-inserted-css-" + (key || '');
+
+ if(!this.document) return false;
+
+ // Check if link already exists
+ styleEl = this.document.getElementById(key);
+ if (!styleEl) {
+ styleEl = this.document.createElement("style");
+ styleEl.id = key;
+ // Append style element to head
+ this.document.head.appendChild(styleEl);
+ }
+ return styleEl;
+ }
+
+ /**
+ * Append stylesheet css
+ * @param {string} serializedCss
+ * @param {string} key If the key is the same, the CSS will be replaced instead of inserted
+ */
+ addStylesheetCss(serializedCss, key) {
+ if(!this.document || !serializedCss) return false;
+
+ var styleEl;
+ styleEl = this._getStylesheetNode(key);
+ styleEl.innerHTML = serializedCss;
+
+ return true;
+ }
+
+ /**
+ * Append stylesheet rules to a generate stylesheet
+ * Array: https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet/insertRule
+ * Object: https://github.com/desirable-objects/json-to-css
+ * @param {array | object} rules
+ * @param {string} key If the key is the same, the CSS will be replaced instead of inserted
+ */
+ addStylesheetRules(rules, key) {
+ var styleSheet;
+
+ if(!this.document || !rules || rules.length === 0) return;
+
+ // Grab style sheet
+ styleSheet = this._getStylesheetNode(key).sheet;
+
+ if (Object.prototype.toString.call(rules) === "[object Array]") {
+ for (var i = 0, rl = rules.length; i < rl; i++) {
+ var j = 1, rule = rules[i], selector = rules[i][0], propStr = "";
+ // If the second argument of a rule is an array of arrays, correct our variables.
+ if (Object.prototype.toString.call(rule[1][0]) === "[object Array]") {
+ rule = rule[1];
+ j = 0;
+ }
+
+ for (var pl = rule.length; j < pl; j++) {
+ var prop = rule[j];
+ propStr += prop[0] + ":" + prop[1] + (prop[2] ? " !important" : "") + ";\n";
+ }
+
+ // Insert CSS Rule
+ styleSheet.insertRule(selector + "{" + propStr + "}", styleSheet.cssRules.length);
+ }
+ } else {
+ const selectors = Object.keys(rules);
+ selectors.forEach((selector) => {
+ const definition = rules[selector];
+ if (Array.isArray(definition)) {
+ definition.forEach((item) => {
+ const _rules = Object.keys(item);
+ const result = _rules.map((rule) => {
+ return `${rule}:${item[rule]}`;
+ }).join(';');
+ styleSheet.insertRule(`${selector}{${result}}`, styleSheet.cssRules.length);
+ });
+ } else {
+ const _rules = Object.keys(definition);
+ const result = _rules.map((rule) => {
+ return `${rule}:${definition[rule]}`;
+ }).join(';');
+ styleSheet.insertRule(`${selector}{${result}}`, styleSheet.cssRules.length);
+ }
+ });
+ }
+ }
+
+ /**
+ * Append a script tag to the document head
+ * @param {string} src url
+ * @returns {Promise} loaded
+ */
+ addScript(src) {
+
+ return new Promise(function(resolve, reject){
+ var $script;
+ var ready = false;
+
+ if(!this.document) {
+ resolve(false);
+ return;
+ }
+
+ $script = this.document.createElement("script");
+ $script.type = "text/javascript";
+ $script.async = true;
+ $script.src = src;
+ $script.onload = $script.onreadystatechange = function() {
+ if ( !ready && (!this.readyState || this.readyState == "complete") ) {
+ ready = true;
+ setTimeout(function(){
+ resolve(true);
+ }, 1);
+ }
+ };
+
+ this.document.head.appendChild($script);
+
+ }.bind(this));
+ }
+
+ /**
+ * Add a class to the contents container
+ * @param {string} className
+ */
+ addClass(className) {
+ var content;
+
+ if(!this.document) return;
+
+ content = this.content || this.document.body;
+
+ if (content) {
+ content.classList.add(className);
+ }
+
+ }
+
+ /**
+ * Remove a class from the contents container
+ * @param {string} removeClass
+ */
+ removeClass(className) {
+ var content;
+
+ if(!this.document) return;
+
+ content = this.content || this.document.body;
+
+ if (content) {
+ content.classList.remove(className);
+ }
+
+ }
+
+ /**
+ * Add DOM event listeners
+ * @private
+ */
+ addEventListeners(){
+ if(!this.document) {
+ return;
+ }
+
+ this._triggerEvent = this.triggerEvent.bind(this);
+
+ DOM_EVENTS.forEach(function(eventName){
+ this.document.addEventListener(eventName, this._triggerEvent, { passive: true });
+ }, this);
+
+ }
+
+ /**
+ * Remove DOM event listeners
+ * @private
+ */
+ removeEventListeners(){
+ if(!this.document) {
+ return;
+ }
+ DOM_EVENTS.forEach(function(eventName){
+ this.document.removeEventListener(eventName, this._triggerEvent, { passive: true });
+ }, this);
+ this._triggerEvent = undefined;
+ }
+
+ /**
+ * Emit passed browser events
+ * @private
+ */
+ triggerEvent(e){
+ this.emit(e.type, e);
+ }
+
+ /**
+ * Add listener for text selection
+ * @private
+ */
+ addSelectionListeners(){
+ if(!this.document) {
+ return;
+ }
+ this._onSelectionChange = this.onSelectionChange.bind(this);
+ this.document.addEventListener("selectionchange", this._onSelectionChange, { passive: true });
+ }
+
+ /**
+ * Remove listener for text selection
+ * @private
+ */
+ removeSelectionListeners(){
+ if(!this.document) {
+ return;
+ }
+ this.document.removeEventListener("selectionchange", this._onSelectionChange, { passive: true });
+ this._onSelectionChange = undefined;
+ }
+
+ /**
+ * Handle getting text on selection
+ * @private
+ */
+ onSelectionChange(e){
+ if (this.selectionEndTimeout) {
+ clearTimeout(this.selectionEndTimeout);
+ }
+ this.selectionEndTimeout = setTimeout(function() {
+ var selection = this.window.getSelection();
+ this.triggerSelectedEvent(selection);
+ }.bind(this), 250);
+ }
+
+ /**
+ * Emit event on text selection
+ * @private
+ */
+ triggerSelectedEvent(selection){
+ var range, cfirange;
+
+ if (selection && selection.rangeCount > 0) {
+ range = selection.getRangeAt(0);
+ if(!range.collapsed) {
+ // cfirange = this.section.cfiFromRange(range);
+ cfirange = new EpubCFI(range, this.cfiBase).toString();
+ this.emit(EVENTS.CONTENTS.SELECTED, cfirange);
+ this.emit(EVENTS.CONTENTS.SELECTED_RANGE, range);
+ }
+ }
+ }
+
+ /**
+ * Get a Dom Range from EpubCFI
+ * @param {EpubCFI} _cfi
+ * @param {string} [ignoreClass]
+ * @returns {Range} range
+ */
+ range(_cfi, ignoreClass){
+ var cfi = new EpubCFI(_cfi);
+ return cfi.toRange(this.document, ignoreClass);
+ }
+
+ /**
+ * Get an EpubCFI from a Dom Range
+ * @param {Range} range
+ * @param {string} [ignoreClass]
+ * @returns {EpubCFI} cfi
+ */
+ cfiFromRange(range, ignoreClass){
+ return new EpubCFI(range, this.cfiBase, ignoreClass).toString();
+ }
+
+ /**
+ * Get an EpubCFI from a Dom node
+ * @param {node} node
+ * @param {string} [ignoreClass]
+ * @returns {EpubCFI} cfi
+ */
+ cfiFromNode(node, ignoreClass){
+ return new EpubCFI(node, this.cfiBase, ignoreClass).toString();
+ }
+
+ // TODO: find where this is used - remove?
+ map(layout){
+ var map = new Mapping(layout);
+ return map.section();
+ }
+
+ /**
+ * Size the contents to a given width and height
+ * @param {number} [width]
+ * @param {number} [height]
+ */
+ size(width, height){
+ var viewport = { scale: 1.0, scalable: "no" };
+
+ this.layoutStyle("scrolling");
+
+ if (width >= 0) {
+ this.width(width);
+ viewport.width = width;
+ this.css("padding", "0 "+(width/12)+"px");
+ }
+
+ if (height >= 0) {
+ this.height(height);
+ viewport.height = height;
+ }
+
+ this.css("margin", "0");
+ this.css("box-sizing", "border-box");
+
+
+ this.viewport(viewport);
+ }
+
+ /**
+ * Apply columns to the contents for pagination
+ * @param {number} width
+ * @param {number} height
+ * @param {number} columnWidth
+ * @param {number} gap
+ */
+ columns(width, height, columnWidth, gap, dir){
+ let COLUMN_AXIS = prefixed("column-axis");
+ let COLUMN_GAP = prefixed("column-gap");
+ let COLUMN_WIDTH = prefixed("column-width");
+ let COLUMN_FILL = prefixed("column-fill");
+
+ let writingMode = this.writingMode();
+ let axis = (writingMode.indexOf("vertical") === 0) ? "vertical" : "horizontal";
+
+ this.layoutStyle("paginated");
+
+ if (dir === "rtl" && axis === "horizontal") {
+ this.direction(dir);
+ }
+
+ this.width(width);
+ this.height(height);
+
+ // Deal with Mobile trying to scale to viewport
+ this.viewport({ width: width, height: height, scale: 1.0, scalable: "no" });
+
+ // TODO: inline-block needs more testing
+ // Fixes Safari column cut offs, but causes RTL issues
+ // this.css("display", "inline-block");
+
+ this.css("overflow-y", "hidden");
+ this.css("margin", "0", true);
+
+ if (axis === "vertical") {
+ this.css("padding-top", (gap / 2) + "px", true);
+ this.css("padding-bottom", (gap / 2) + "px", true);
+ this.css("padding-left", "20px");
+ this.css("padding-right", "20px");
+ this.css(COLUMN_AXIS, "vertical");
+ } else {
+ this.css("padding-top", "20px");
+ this.css("padding-bottom", "20px");
+ this.css("padding-left", (gap / 2) + "px", true);
+ this.css("padding-right", (gap / 2) + "px", true);
+ this.css(COLUMN_AXIS, "horizontal");
+ }
+
+ this.css("box-sizing", "border-box");
+ this.css("max-width", "inherit");
+
+ this.css(COLUMN_FILL, "auto");
+
+ this.css(COLUMN_GAP, gap+"px");
+ this.css(COLUMN_WIDTH, columnWidth+"px");
+
+ // Fix glyph clipping in WebKit
+ // https://github.com/futurepress/epub.js/issues/983
+ this.css("-webkit-line-box-contain", "block glyphs replaced");
+ }
+
+ /**
+ * Scale contents from center
+ * @param {number} scale
+ * @param {number} offsetX
+ * @param {number} offsetY
+ */
+ scaler(scale, offsetX, offsetY){
+ var scaleStr = "scale(" + scale + ")";
+ var translateStr = "";
+ // this.css("position", "absolute"));
+ this.css("transform-origin", "top left");
+
+ if (offsetX >= 0 || offsetY >= 0) {
+ translateStr = " translate(" + (offsetX || 0 )+ "px, " + (offsetY || 0 )+ "px )";
+ }
+
+ this.css("transform", scaleStr + translateStr);
+ }
+
+ /**
+ * Fit contents into a fixed width and height
+ * @param {number} width
+ * @param {number} height
+ */
+ fit(width, height, section){
+ var viewport = this.viewport();
+ var viewportWidth = parseInt(viewport.width);
+ var viewportHeight = parseInt(viewport.height);
+ var widthScale = width / viewportWidth;
+ var heightScale = height / viewportHeight;
+ var scale = widthScale < heightScale ? widthScale : heightScale;
+
+ // the translate does not work as intended, elements can end up unaligned
+ // var offsetY = (height - (viewportHeight * scale)) / 2;
+ // var offsetX = 0;
+ // if (this.sectionIndex % 2 === 1) {
+ // offsetX = width - (viewportWidth * scale);
+ // }
+
+ this.layoutStyle("paginated");
+
+ // scale needs width and height to be set
+ this.width(viewportWidth);
+ this.height(viewportHeight);
+ this.overflow("hidden");
+
+ // Scale to the correct size
+ this.scaler(scale, 0, 0);
+ // this.scaler(scale, offsetX > 0 ? offsetX : 0, offsetY);
+
+ // background images are not scaled by transform
+ this.css("background-size", viewportWidth * scale + "px " + viewportHeight * scale + "px");
+
+ this.css("background-color", "transparent");
+ if (section && section.properties.includes("page-spread-left")) {
+ // set margin since scale is weird
+ var marginLeft = width - (viewportWidth * scale);
+ this.css("margin-left", marginLeft + "px");
+ }
+ }
+
+ /**
+ * Set the direction of the text
+ * @param {string} [dir="ltr"] "rtl" | "ltr"
+ */
+ direction(dir) {
+ if (this.documentElement) {
+ this.documentElement.style["direction"] = dir;
+ }
+ }
+
+ mapPage(cfiBase, layout, start, end, dev) {
+ var mapping = new Mapping(layout, dev);
+
+ return mapping.page(this, cfiBase, start, end);
+ }
+
+ /**
+ * Emit event when link in content is clicked
+ * @private
+ */
+ linksHandler() {
+ replaceLinks(this.content, (href) => {
+ this.emit(EVENTS.CONTENTS.LINK_CLICKED, href);
+ });
+ }
+
+ /**
+ * Set the writingMode of the text
+ * @param {string} [mode="horizontal-tb"] "horizontal-tb" | "vertical-rl" | "vertical-lr"
+ */
+ writingMode(mode) {
+ let WRITING_MODE = prefixed("writing-mode");
+
+ if (mode && this.documentElement) {
+ this.documentElement.style[WRITING_MODE] = mode;
+ }
+
+ return this.window.getComputedStyle(this.documentElement)[WRITING_MODE] || '';
+ }
+
+ /**
+ * Set the layoutStyle of the content
+ * @param {string} [style="paginated"] "scrolling" | "paginated"
+ * @private
+ */
+ layoutStyle(style) {
+
+ if (style) {
+ this._layoutStyle = style;
+ navigator.epubReadingSystem.layoutStyle = this._layoutStyle;
+ }
+
+ return this._layoutStyle || "paginated";
+ }
+
+ /**
+ * Add the epubReadingSystem object to the navigator
+ * @param {string} name
+ * @param {string} version
+ * @private
+ */
+ epubReadingSystem(name, version) {
+ navigator.epubReadingSystem = {
+ name: name,
+ version: version,
+ layoutStyle: this.layoutStyle(),
+ hasFeature: function (feature) {
+ switch (feature) {
+ case "dom-manipulation":
+ return true;
+ case "layout-changes":
+ return true;
+ case "touch-events":
+ return true;
+ case "mouse-events":
+ return true;
+ case "keyboard-events":
+ return true;
+ case "spine-scripting":
+ return false;
+ default:
+ return false;
+ }
+ }
+ };
+ return navigator.epubReadingSystem;
+ }
+
+ destroy() {
+ // this.document.removeEventListener('transitionend', this._resizeCheck);
+
+ this.removeListeners();
+
+ }
+}
+
+EventEmitter(Contents.prototype);
+
+export default Contents;
diff --git a/lib/epub.js/src/displayoptions.js b/lib/epub.js/src/displayoptions.js
new file mode 100644
index 0000000..a2793e2
--- /dev/null
+++ b/lib/epub.js/src/displayoptions.js
@@ -0,0 +1,70 @@
+import {qs, qsa } from "./utils/core";
+
+/**
+ * Open DisplayOptions Format Parser
+ * @class
+ * @param {document} displayOptionsDocument XML
+ */
+class DisplayOptions {
+ constructor(displayOptionsDocument) {
+ this.interactive = "";
+ this.fixedLayout = "";
+ this.openToSpread = "";
+ this.orientationLock = "";
+
+ if (displayOptionsDocument) {
+ this.parse(displayOptionsDocument);
+ }
+ }
+
+ /**
+ * Parse XML
+ * @param {document} displayOptionsDocument XML
+ * @return {DisplayOptions} self
+ */
+ parse(displayOptionsDocument) {
+ if(!displayOptionsDocument) {
+ return this;
+ }
+
+ const displayOptionsNode = qs(displayOptionsDocument, "display_options");
+ if(!displayOptionsNode) {
+ return this;
+ }
+
+ const options = qsa(displayOptionsNode, "option");
+ options.forEach((el) => {
+ let value = "";
+
+ if (el.childNodes.length) {
+ value = el.childNodes[0].nodeValue;
+ }
+
+ switch (el.attributes.name.value) {
+ case "interactive":
+ this.interactive = value;
+ break;
+ case "fixed-layout":
+ this.fixedLayout = value;
+ break;
+ case "open-to-spread":
+ this.openToSpread = value;
+ break;
+ case "orientation-lock":
+ this.orientationLock = value;
+ break;
+ }
+ });
+
+ return this;
+ }
+
+ destroy() {
+ this.interactive = undefined;
+ this.fixedLayout = undefined;
+ this.openToSpread = undefined;
+ this.orientationLock = undefined;
+ }
+}
+
+export default DisplayOptions;
diff --git a/lib/epub.js/src/epub.js b/lib/epub.js/src/epub.js
new file mode 100644
index 0000000..e6fcdfb
--- /dev/null
+++ b/lib/epub.js/src/epub.js
@@ -0,0 +1,35 @@
+import Book from "./book";
+import Rendition from "./rendition";
+import CFI from "./epubcfi";
+import Contents from "./contents";
+import * as utils from "./utils/core";
+import { EPUBJS_VERSION } from "./utils/constants";
+
+import IframeView from "./managers/views/iframe";
+import DefaultViewManager from "./managers/default";
+import ContinuousViewManager from "./managers/continuous";
+
+/**
+ * Creates a new Book
+ * @param {string|ArrayBuffer} url URL, Path or ArrayBuffer
+ * @param {object} options to pass to the book
+ * @returns {Book} a new Book object
+ * @example ePub("/path/to/book.epub", {})
+ */
+function ePub(url, options) {
+ return new Book(url, options);
+}
+
+ePub.VERSION = EPUBJS_VERSION;
+
+if (typeof(global) !== "undefined") {
+ global.EPUBJS_VERSION = EPUBJS_VERSION;
+}
+
+ePub.Book = Book;
+ePub.Rendition = Rendition;
+ePub.Contents = Contents;
+ePub.CFI = CFI;
+ePub.utils = utils;
+
+export default ePub;
diff --git a/lib/epub.js/src/epubcfi.js b/lib/epub.js/src/epubcfi.js
new file mode 100644
index 0000000..77142a3
--- /dev/null
+++ b/lib/epub.js/src/epubcfi.js
@@ -0,0 +1,1048 @@
+import {extend, type, findChildren, RangeObject, isNumber} from "./utils/core";
+
+const ELEMENT_NODE = 1;
+const TEXT_NODE = 3;
+const COMMENT_NODE = 8;
+const DOCUMENT_NODE = 9;
+
+/**
+ * Parsing and creation of EpubCFIs: http://www.idpf.org/epub/linking/cfi/epub-cfi.html
+
+ * Implements:
+ * - Character Offset: epubcfi(/6/4[chap01ref]!/4[body01]/10[para05]/2/1:3)
+ * - Simple Ranges : epubcfi(/6/4[chap01ref]!/4[body01]/10[para05],/2/1:1,/3:4)
+
+ * Does Not Implement:
+ * - Temporal Offset (~)
+ * - Spatial Offset (@)
+ * - Temporal-Spatial Offset (~ + @)
+ * - Text Location Assertion ([)
+ * @class
+ @param {string | Range | Node } [cfiFrom]
+ @param {string | object} [base]
+ @param {string} [ignoreClass] class to ignore when parsing DOM
+*/
+class EpubCFI {
+ constructor(cfiFrom, base, ignoreClass){
+ var type;
+
+ this.str = "";
+
+ this.base = {};
+ this.spinePos = 0; // For compatibility
+
+ this.range = false; // true || false;
+
+ this.path = {};
+ this.start = null;
+ this.end = null;
+
+ // Allow instantiation without the "new" keyword
+ if (!(this instanceof EpubCFI)) {
+ return new EpubCFI(cfiFrom, base, ignoreClass);
+ }
+
+ if(typeof base === "string") {
+ this.base = this.parseComponent(base);
+ } else if(typeof base === "object" && base.steps) {
+ this.base = base;
+ }
+
+ type = this.checkType(cfiFrom);
+
+
+ if(type === "string") {
+ this.str = cfiFrom;
+ return extend(this, this.parse(cfiFrom));
+ } else if (type === "range") {
+ return extend(this, this.fromRange(cfiFrom, this.base, ignoreClass));
+ } else if (type === "node") {
+ return extend(this, this.fromNode(cfiFrom, this.base, ignoreClass));
+ } else if (type === "EpubCFI" && cfiFrom.path) {
+ return cfiFrom;
+ } else if (!cfiFrom) {
+ return this;
+ } else {
+ throw new TypeError("not a valid argument for EpubCFI");
+ }
+
+ }
+
+ /**
+ * Check the type of constructor input
+ * @private
+ */
+ checkType(cfi) {
+
+ if (this.isCfiString(cfi)) {
+ return "string";
+ // Is a range object
+ } else if (cfi && typeof cfi === "object" && (type(cfi) === "Range" || typeof(cfi.startContainer) != "undefined")){
+ return "range";
+ } else if (cfi && typeof cfi === "object" && typeof(cfi.nodeType) != "undefined" ){ // || typeof cfi === "function"
+ return "node";
+ } else if (cfi && typeof cfi === "object" && cfi instanceof EpubCFI){
+ return "EpubCFI";
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Parse a cfi string to a CFI object representation
+ * @param {string} cfiStr
+ * @returns {object} cfi
+ */
+ parse(cfiStr) {
+ var cfi = {
+ spinePos: -1,
+ range: false,
+ base: {},
+ path: {},
+ start: null,
+ end: null
+ };
+ var baseComponent, pathComponent, range;
+
+ if(typeof cfiStr !== "string") {
+ return {spinePos: -1};
+ }
+
+ if(cfiStr.indexOf("epubcfi(") === 0 && cfiStr[cfiStr.length-1] === ")") {
+ // Remove intial epubcfi( and ending )
+ cfiStr = cfiStr.slice(8, cfiStr.length-1);
+ }
+
+ baseComponent = this.getChapterComponent(cfiStr);
+
+ // Make sure this is a valid cfi or return
+ if(!baseComponent) {
+ return {spinePos: -1};
+ }
+
+ cfi.base = this.parseComponent(baseComponent);
+
+ pathComponent = this.getPathComponent(cfiStr);
+ cfi.path = this.parseComponent(pathComponent);
+
+ range = this.getRange(cfiStr);
+
+ if(range) {
+ cfi.range = true;
+ cfi.start = this.parseComponent(range[0]);
+ cfi.end = this.parseComponent(range[1]);
+ }
+
+ // Get spine node position
+ // cfi.spineSegment = cfi.base.steps[1];
+
+ // Chapter segment is always the second step
+ cfi.spinePos = cfi.base.steps[1].index;
+
+ return cfi;
+ }
+
+ parseComponent(componentStr){
+ var component = {
+ steps: [],
+ terminal: {
+ offset: null,
+ assertion: null
+ }
+ };
+ var parts = componentStr.split(":");
+ var steps = parts[0].split("/");
+ var terminal;
+
+ if(parts.length > 1) {
+ terminal = parts[1];
+ component.terminal = this.parseTerminal(terminal);
+ }
+
+ if (steps[0] === "") {
+ steps.shift(); // Ignore the first slash
+ }
+
+ component.steps = steps.map(function(step){
+ return this.parseStep(step);
+ }.bind(this));
+
+ return component;
+ }
+
+ parseStep(stepStr){
+ var type, num, index, has_brackets, id;
+
+ has_brackets = stepStr.match(/\[(.*)\]/);
+ if(has_brackets && has_brackets[1]){
+ id = has_brackets[1];
+ }
+
+ //-- Check if step is a text node or element
+ num = parseInt(stepStr);
+
+ if(isNaN(num)) {
+ return;
+ }
+
+ if(num % 2 === 0) { // Even = is an element
+ type = "element";
+ index = num / 2 - 1;
+ } else {
+ type = "text";
+ index = (num - 1 ) / 2;
+ }
+
+ return {
+ "type" : type,
+ "index" : index,
+ "id" : id || null
+ };
+ }
+
+ parseTerminal(termialStr){
+ var characterOffset, textLocationAssertion;
+ var assertion = termialStr.match(/\[(.*)\]/);
+
+ if(assertion && assertion[1]){
+ characterOffset = parseInt(termialStr.split("[")[0]);
+ textLocationAssertion = assertion[1];
+ } else {
+ characterOffset = parseInt(termialStr);
+ }
+
+ if (!isNumber(characterOffset)) {
+ characterOffset = null;
+ }
+
+ return {
+ "offset": characterOffset,
+ "assertion": textLocationAssertion
+ };
+
+ }
+
+ getChapterComponent(cfiStr) {
+
+ var indirection = cfiStr.split("!");
+
+ return indirection[0];
+ }
+
+ getPathComponent(cfiStr) {
+
+ var indirection = cfiStr.split("!");
+
+ if(indirection[1]) {
+ let ranges = indirection[1].split(",");
+ return ranges[0];
+ }
+
+ }
+
+ getRange(cfiStr) {
+
+ var ranges = cfiStr.split(",");
+
+ if(ranges.length === 3){
+ return [
+ ranges[1],
+ ranges[2]
+ ];
+ }
+
+ return false;
+ }
+
+ getCharecterOffsetComponent(cfiStr) {
+ var splitStr = cfiStr.split(":");
+ return splitStr[1] || "";
+ }
+
+ joinSteps(steps) {
+ if(!steps) {
+ return "";
+ }
+
+ return steps.map(function(part){
+ var segment = "";
+
+ if(part.type === "element") {
+ segment += (part.index + 1) * 2;
+ }
+
+ if(part.type === "text") {
+ segment += 1 + (2 * part.index); // TODO: double check that this is odd
+ }
+
+ if(part.id) {
+ segment += "[" + part.id + "]";
+ }
+
+ return segment;
+
+ }).join("/");
+
+ }
+
+ segmentString(segment) {
+ var segmentString = "/";
+
+ segmentString += this.joinSteps(segment.steps);
+
+ if(segment.terminal && segment.terminal.offset != null){
+ segmentString += ":" + segment.terminal.offset;
+ }
+
+ if(segment.terminal && segment.terminal.assertion != null){
+ segmentString += "[" + segment.terminal.assertion + "]";
+ }
+
+ return segmentString;
+ }
+
+ /**
+ * Convert CFI to a epubcfi(...) string
+ * @returns {string} epubcfi
+ */
+ toString() {
+ var cfiString = "epubcfi(";
+
+ cfiString += this.segmentString(this.base);
+
+ cfiString += "!";
+ cfiString += this.segmentString(this.path);
+
+ // Add Range, if present
+ if(this.range && this.start) {
+ cfiString += ",";
+ cfiString += this.segmentString(this.start);
+ }
+
+ if(this.range && this.end) {
+ cfiString += ",";
+ cfiString += this.segmentString(this.end);
+ }
+
+ cfiString += ")";
+
+ return cfiString;
+ }
+
+
+ /**
+ * Compare which of two CFIs is earlier in the text
+ * @returns {number} First is earlier = -1, Second is earlier = 1, They are equal = 0
+ */
+ compare(cfiOne, cfiTwo) {
+ var stepsA, stepsB;
+ var terminalA, terminalB;
+
+ var rangeAStartSteps, rangeAEndSteps;
+ var rangeBEndSteps, rangeBEndSteps;
+ var rangeAStartTerminal, rangeAEndTerminal;
+ var rangeBStartTerminal, rangeBEndTerminal;
+
+ if(typeof cfiOne === "string") {
+ cfiOne = new EpubCFI(cfiOne);
+ }
+ if(typeof cfiTwo === "string") {
+ cfiTwo = new EpubCFI(cfiTwo);
+ }
+ // Compare Spine Positions
+ if(cfiOne.spinePos > cfiTwo.spinePos) {
+ return 1;
+ }
+ if(cfiOne.spinePos < cfiTwo.spinePos) {
+ return -1;
+ }
+
+ if (cfiOne.range) {
+ stepsA = cfiOne.path.steps.concat(cfiOne.start.steps);
+ terminalA = cfiOne.start.terminal;
+ } else {
+ stepsA = cfiOne.path.steps;
+ terminalA = cfiOne.path.terminal;
+ }
+
+ if (cfiTwo.range) {
+ stepsB = cfiTwo.path.steps.concat(cfiTwo.start.steps);
+ terminalB = cfiTwo.start.terminal;
+ } else {
+ stepsB = cfiTwo.path.steps;
+ terminalB = cfiTwo.path.terminal;
+ }
+
+ // Compare Each Step in the First item
+ for (var i = 0; i < stepsA.length; i++) {
+ if(!stepsA[i]) {
+ return -1;
+ }
+ if(!stepsB[i]) {
+ return 1;
+ }
+ if(stepsA[i].index > stepsB[i].index) {
+ return 1;
+ }
+ if(stepsA[i].index < stepsB[i].index) {
+ return -1;
+ }
+ // Otherwise continue checking
+ }
+
+ // All steps in First equal to Second and First is Less Specific
+ if(stepsA.length < stepsB.length) {
+ return -1;
+ }
+
+ // Compare the charecter offset of the text node
+ if(terminalA.offset > terminalB.offset) {
+ return 1;
+ }
+ if(terminalA.offset < terminalB.offset) {
+ return -1;
+ }
+
+ // CFI's are equal
+ return 0;
+ }
+
+ step(node) {
+ var nodeType = (node.nodeType === TEXT_NODE) ? "text" : "element";
+
+ return {
+ "id" : node.id,
+ "tagName" : node.tagName,
+ "type" : nodeType,
+ "index" : this.position(node)
+ };
+ }
+
+ filteredStep(node, ignoreClass) {
+ var filteredNode = this.filter(node, ignoreClass);
+ var nodeType;
+
+ // Node filtered, so ignore
+ if (!filteredNode) {
+ return;
+ }
+
+ // Otherwise add the filter node in
+ nodeType = (filteredNode.nodeType === TEXT_NODE) ? "text" : "element";
+
+ return {
+ "id" : filteredNode.id,
+ "tagName" : filteredNode.tagName,
+ "type" : nodeType,
+ "index" : this.filteredPosition(filteredNode, ignoreClass)
+ };
+ }
+
+ pathTo(node, offset, ignoreClass) {
+ var segment = {
+ steps: [],
+ terminal: {
+ offset: null,
+ assertion: null
+ }
+ };
+ var currentNode = node;
+ var step;
+
+ while(currentNode && currentNode.parentNode &&
+ currentNode.parentNode.nodeType != DOCUMENT_NODE) {
+
+ if (ignoreClass) {
+ step = this.filteredStep(currentNode, ignoreClass);
+ } else {
+ step = this.step(currentNode);
+ }
+
+ if (step) {
+ segment.steps.unshift(step);
+ }
+
+ currentNode = currentNode.parentNode;
+
+ }
+
+ if (offset != null && offset >= 0) {
+
+ segment.terminal.offset = offset;
+
+ // Make sure we are getting to a textNode if there is an offset
+ if(segment.steps[segment.steps.length-1].type != "text") {
+ segment.steps.push({
+ "type" : "text",
+ "index" : 0
+ });
+ }
+
+ }
+
+
+ return segment;
+ }
+
+ equalStep(stepA, stepB) {
+ if (!stepA || !stepB) {
+ return false;
+ }
+
+ if(stepA.index === stepB.index &&
+ stepA.id === stepB.id &&
+ stepA.type === stepB.type) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Create a CFI object from a Range
+ * @param {Range} range
+ * @param {string | object} base
+ * @param {string} [ignoreClass]
+ * @returns {object} cfi
+ */
+ fromRange(range, base, ignoreClass) {
+ var cfi = {
+ range: false,
+ base: {},
+ path: {},
+ start: null,
+ end: null
+ };
+
+ var start = range.startContainer;
+ var end = range.endContainer;
+
+ var startOffset = range.startOffset;
+ var endOffset = range.endOffset;
+
+ var needsIgnoring = false;
+
+ if (ignoreClass) {
+ // Tell pathTo if / what to ignore
+ needsIgnoring = (start.ownerDocument.querySelector("." + ignoreClass) != null);
+ }
+
+
+ if (typeof base === "string") {
+ cfi.base = this.parseComponent(base);
+ cfi.spinePos = cfi.base.steps[1].index;
+ } else if (typeof base === "object") {
+ cfi.base = base;
+ }
+
+ if (range.collapsed) {
+ if (needsIgnoring) {
+ startOffset = this.patchOffset(start, startOffset, ignoreClass);
+ }
+ cfi.path = this.pathTo(start, startOffset, ignoreClass);
+ } else {
+ cfi.range = true;
+
+ if (needsIgnoring) {
+ startOffset = this.patchOffset(start, startOffset, ignoreClass);
+ }
+
+ cfi.start = this.pathTo(start, startOffset, ignoreClass);
+ if (needsIgnoring) {
+ endOffset = this.patchOffset(end, endOffset, ignoreClass);
+ }
+
+ cfi.end = this.pathTo(end, endOffset, ignoreClass);
+
+ // Create a new empty path
+ cfi.path = {
+ steps: [],
+ terminal: null
+ };
+
+ // Push steps that are shared between start and end to the common path
+ var len = cfi.start.steps.length;
+ var i;
+
+ for (i = 0; i < len; i++) {
+ if (this.equalStep(cfi.start.steps[i], cfi.end.steps[i])) {
+ if(i === len-1) {
+ // Last step is equal, check terminals
+ if(cfi.start.terminal === cfi.end.terminal) {
+ // CFI's are equal
+ cfi.path.steps.push(cfi.start.steps[i]);
+ // Not a range
+ cfi.range = false;
+ }
+ } else {
+ cfi.path.steps.push(cfi.start.steps[i]);
+ }
+
+ } else {
+ break;
+ }
+ }
+
+ cfi.start.steps = cfi.start.steps.slice(cfi.path.steps.length);
+ cfi.end.steps = cfi.end.steps.slice(cfi.path.steps.length);
+
+ // TODO: Add Sanity check to make sure that the end if greater than the start
+ }
+
+ return cfi;
+ }
+
+ /**
+ * Create a CFI object from a Node
+ * @param {Node} anchor
+ * @param {string | object} base
+ * @param {string} [ignoreClass]
+ * @returns {object} cfi
+ */
+ fromNode(anchor, base, ignoreClass) {
+ var cfi = {
+ range: false,
+ base: {},
+ path: {},
+ start: null,
+ end: null
+ };
+
+ if (typeof base === "string") {
+ cfi.base = this.parseComponent(base);
+ cfi.spinePos = cfi.base.steps[1].index;
+ } else if (typeof base === "object") {
+ cfi.base = base;
+ }
+
+ cfi.path = this.pathTo(anchor, null, ignoreClass);
+
+ return cfi;
+ }
+
+ filter(anchor, ignoreClass) {
+ var needsIgnoring;
+ var sibling; // to join with
+ var parent, previousSibling, nextSibling;
+ var isText = false;
+
+ if (anchor.nodeType === TEXT_NODE) {
+ isText = true;
+ parent = anchor.parentNode;
+ needsIgnoring = anchor.parentNode.classList.contains(ignoreClass);
+ } else {
+ isText = false;
+ needsIgnoring = anchor.classList.contains(ignoreClass);
+ }
+
+ if (needsIgnoring && isText) {
+ previousSibling = parent.previousSibling;
+ nextSibling = parent.nextSibling;
+
+ // If the sibling is a text node, join the nodes
+ if (previousSibling && previousSibling.nodeType === TEXT_NODE) {
+ sibling = previousSibling;
+ } else if (nextSibling && nextSibling.nodeType === TEXT_NODE) {
+ sibling = nextSibling;
+ }
+
+ if (sibling) {
+ return sibling;
+ } else {
+ // Parent will be ignored on next step
+ return anchor;
+ }
+
+ } else if (needsIgnoring && !isText) {
+ // Otherwise just skip the element node
+ return false;
+ } else {
+ // No need to filter
+ return anchor;
+ }
+
+ }
+
+ patchOffset(anchor, offset, ignoreClass) {
+ if (anchor.nodeType != TEXT_NODE) {
+ throw new Error("Anchor must be a text node");
+ }
+
+ var curr = anchor;
+ var totalOffset = offset;
+
+ // If the parent is a ignored node, get offset from it's start
+ if (anchor.parentNode.classList.contains(ignoreClass)) {
+ curr = anchor.parentNode;
+ }
+
+ while (curr.previousSibling) {
+ if(curr.previousSibling.nodeType === ELEMENT_NODE) {
+ // Originally a text node, so join
+ if(curr.previousSibling.classList.contains(ignoreClass)){
+ totalOffset += curr.previousSibling.textContent.length;
+ } else {
+ break; // Normal node, dont join
+ }
+ } else {
+ // If the previous sibling is a text node, join the nodes
+ totalOffset += curr.previousSibling.textContent.length;
+ }
+
+ curr = curr.previousSibling;
+ }
+
+ return totalOffset;
+
+ }
+
+ normalizedMap(children, nodeType, ignoreClass) {
+ var output = {};
+ var prevIndex = -1;
+ var i, len = children.length;
+ var currNodeType;
+ var prevNodeType;
+
+ for (i = 0; i < len; i++) {
+
+ currNodeType = children[i].nodeType;
+
+ // Check if needs ignoring
+ if (currNodeType === ELEMENT_NODE &&
+ children[i].classList.contains(ignoreClass)) {
+ currNodeType = TEXT_NODE;
+ }
+
+ if (i > 0 &&
+ currNodeType === TEXT_NODE &&
+ prevNodeType === TEXT_NODE) {
+ // join text nodes
+ output[i] = prevIndex;
+ } else if (nodeType === currNodeType){
+ prevIndex = prevIndex + 1;
+ output[i] = prevIndex;
+ }
+
+ prevNodeType = currNodeType;
+
+ }
+
+ return output;
+ }
+
+ position(anchor) {
+ var children, index;
+ if (anchor.nodeType === ELEMENT_NODE) {
+ children = anchor.parentNode.children;
+ if (!children) {
+ children = findChildren(anchor.parentNode);
+ }
+ index = Array.prototype.indexOf.call(children, anchor);
+ } else {
+ children = this.textNodes(anchor.parentNode);
+ index = children.indexOf(anchor);
+ }
+
+ return index;
+ }
+
+ filteredPosition(anchor, ignoreClass) {
+ var children, index, map;
+
+ if (anchor.nodeType === ELEMENT_NODE) {
+ children = anchor.parentNode.children;
+ map = this.normalizedMap(children, ELEMENT_NODE, ignoreClass);
+ } else {
+ children = anchor.parentNode.childNodes;
+ // Inside an ignored node
+ if(anchor.parentNode.classList.contains(ignoreClass)) {
+ anchor = anchor.parentNode;
+ children = anchor.parentNode.childNodes;
+ }
+ map = this.normalizedMap(children, TEXT_NODE, ignoreClass);
+ }
+
+
+ index = Array.prototype.indexOf.call(children, anchor);
+
+ return map[index];
+ }
+
+ stepsToXpath(steps) {
+ var xpath = [".", "*"];
+
+ steps.forEach(function(step){
+ var position = step.index + 1;
+
+ if(step.id){
+ xpath.push("*[position()=" + position + " and @id='" + step.id + "']");
+ } else if(step.type === "text") {
+ xpath.push("text()[" + position + "]");
+ } else {
+ xpath.push("*[" + position + "]");
+ }
+ });
+
+ return xpath.join("/");
+ }
+
+
+ /*
+
+ To get the last step if needed:
+
+ // Get the terminal step
+ lastStep = steps[steps.length-1];
+ // Get the query string
+ query = this.stepsToQuery(steps);
+ // Find the containing element
+ startContainerParent = doc.querySelector(query);
+ // Find the text node within that element
+ if(startContainerParent && lastStep.type == "text") {
+ container = startContainerParent.childNodes[lastStep.index];
+ }
+ */
+ stepsToQuerySelector(steps) {
+ var query = ["html"];
+
+ steps.forEach(function(step){
+ var position = step.index + 1;
+
+ if(step.id){
+ query.push("#" + step.id);
+ } else if(step.type === "text") {
+ // unsupported in querySelector
+ // query.push("text()[" + position + "]");
+ } else {
+ query.push("*:nth-child(" + position + ")");
+ }
+ });
+
+ return query.join(">");
+
+ }
+
+ textNodes(container, ignoreClass) {
+ return Array.prototype.slice.call(container.childNodes).
+ filter(function (node) {
+ if (node.nodeType === TEXT_NODE) {
+ return true;
+ } else if (ignoreClass && node.classList.contains(ignoreClass)) {
+ return true;
+ }
+ return false;
+ });
+ }
+
+ walkToNode(steps, _doc, ignoreClass) {
+ var doc = _doc || document;
+ var container = doc.documentElement;
+ var children;
+ var step;
+ var len = steps.length;
+ var i;
+
+ for (i = 0; i < len; i++) {
+ step = steps[i];
+
+ if(step.type === "element") {
+ //better to get a container using id as some times step.index may not be correct
+ //For ex.https://github.com/futurepress/epub.js/issues/561
+ if(step.id) {
+ container = doc.getElementById(step.id);
+ }
+ else {
+ children = container.children || findChildren(container);
+ container = children[step.index];
+ }
+ } else if(step.type === "text") {
+ container = this.textNodes(container, ignoreClass)[step.index];
+ }
+ if(!container) {
+ //Break the for loop as due to incorrect index we can get error if
+ //container is undefined so that other functionailties works fine
+ //like navigation
+ break;
+ }
+
+ }
+
+ return container;
+ }
+
+ findNode(steps, _doc, ignoreClass) {
+ var doc = _doc || document;
+ var container;
+ var xpath;
+
+ if(!ignoreClass && typeof doc.evaluate != "undefined") {
+ xpath = this.stepsToXpath(steps);
+ container = doc.evaluate(xpath, doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
+ } else if(ignoreClass) {
+ container = this.walkToNode(steps, doc, ignoreClass);
+ } else {
+ container = this.walkToNode(steps, doc);
+ }
+
+ return container;
+ }
+
+ fixMiss(steps, offset, _doc, ignoreClass) {
+ var container = this.findNode(steps.slice(0,-1), _doc, ignoreClass);
+ var children = container.childNodes;
+ var map = this.normalizedMap(children, TEXT_NODE, ignoreClass);
+ var child;
+ var len;
+ var lastStepIndex = steps[steps.length-1].index;
+
+ for (let childIndex in map) {
+ if (!map.hasOwnProperty(childIndex)) return;
+
+ if(map[childIndex] === lastStepIndex) {
+ child = children[childIndex];
+ len = child.textContent.length;
+ if(offset > len) {
+ offset = offset - len;
+ } else {
+ if (child.nodeType === ELEMENT_NODE) {
+ container = child.childNodes[0];
+ } else {
+ container = child;
+ }
+ break;
+ }
+ }
+ }
+
+ return {
+ container: container,
+ offset: offset
+ };
+
+ }
+
+ /**
+ * Creates a DOM range representing a CFI
+ * @param {document} _doc document referenced in the base
+ * @param {string} [ignoreClass]
+ * @return {Range}
+ */
+ toRange(_doc, ignoreClass) {
+ var doc = _doc || document;
+ var range;
+ var start, end, startContainer, endContainer;
+ var cfi = this;
+ var startSteps, endSteps;
+ var needsIgnoring = ignoreClass ? (doc.querySelector("." + ignoreClass) != null) : false;
+ var missed;
+
+ if (typeof(doc.createRange) !== "undefined") {
+ range = doc.createRange();
+ } else {
+ range = new RangeObject();
+ }
+
+ if (cfi.range) {
+ start = cfi.start;
+ startSteps = cfi.path.steps.concat(start.steps);
+ startContainer = this.findNode(startSteps, doc, needsIgnoring ? ignoreClass : null);
+ end = cfi.end;
+ endSteps = cfi.path.steps.concat(end.steps);
+ endContainer = this.findNode(endSteps, doc, needsIgnoring ? ignoreClass : null);
+ } else {
+ start = cfi.path;
+ startSteps = cfi.path.steps;
+ startContainer = this.findNode(cfi.path.steps, doc, needsIgnoring ? ignoreClass : null);
+ }
+
+ if(startContainer) {
+ try {
+
+ if(start.terminal.offset != null) {
+ range.setStart(startContainer, start.terminal.offset);
+ } else {
+ range.setStart(startContainer, 0);
+ }
+
+ } catch (e) {
+ missed = this.fixMiss(startSteps, start.terminal.offset, doc, needsIgnoring ? ignoreClass : null);
+ range.setStart(missed.container, missed.offset);
+ }
+ } else {
+ console.log("No startContainer found for", this.toString());
+ // No start found
+ return null;
+ }
+
+ if (endContainer) {
+ try {
+
+ if(end.terminal.offset != null) {
+ range.setEnd(endContainer, end.terminal.offset);
+ } else {
+ range.setEnd(endContainer, 0);
+ }
+
+ } catch (e) {
+ missed = this.fixMiss(endSteps, cfi.end.terminal.offset, doc, needsIgnoring ? ignoreClass : null);
+ range.setEnd(missed.container, missed.offset);
+ }
+ }
+
+
+ // doc.defaultView.getSelection().addRange(range);
+ return range;
+ }
+
+ /**
+ * Check if a string is wrapped with "epubcfi()"
+ * @param {string} str
+ * @returns {boolean}
+ */
+ isCfiString(str) {
+ if(typeof str === "string" &&
+ str.indexOf("epubcfi(") === 0 &&
+ str[str.length-1] === ")") {
+ return true;
+ }
+
+ return false;
+ }
+
+ generateChapterComponent(_spineNodeIndex, _pos, id) {
+ var pos = parseInt(_pos),
+ spineNodeIndex = (_spineNodeIndex + 1) * 2,
+ cfi = "/"+spineNodeIndex+"/";
+
+ cfi += (pos + 1) * 2;
+
+ if(id) {
+ cfi += "[" + id + "]";
+ }
+
+ return cfi;
+ }
+
+ /**
+ * Collapse a CFI Range to a single CFI Position
+ * @param {boolean} [toStart=false]
+ */
+ collapse(toStart) {
+ if (!this.range) {
+ return;
+ }
+
+ this.range = false;
+
+ if (toStart) {
+ this.path.steps = this.path.steps.concat(this.start.steps);
+ this.path.terminal = this.start.terminal;
+ } else {
+ this.path.steps = this.path.steps.concat(this.end.steps);
+ this.path.terminal = this.end.terminal;
+ }
+
+ }
+}
+
+export default EpubCFI;
diff --git a/lib/epub.js/src/index.js b/lib/epub.js/src/index.js
new file mode 100644
index 0000000..16ef8b6
--- /dev/null
+++ b/lib/epub.js/src/index.js
@@ -0,0 +1,15 @@
+import Book from "./book";
+import EpubCFI from "./epubcfi";
+import Rendition from "./rendition";
+import Contents from "./contents";
+import Layout from "./layout";
+import ePub from "./epub";
+
+export default ePub;
+export {
+ Book,
+ EpubCFI,
+ Rendition,
+ Contents,
+ Layout
+};
diff --git a/lib/epub.js/src/layout.js b/lib/epub.js/src/layout.js
new file mode 100644
index 0000000..4f16a0f
--- /dev/null
+++ b/lib/epub.js/src/layout.js
@@ -0,0 +1,260 @@
+import { extend } from "./utils/core";
+import { EVENTS } from "./utils/constants";
+import EventEmitter from "event-emitter";
+
+/**
+ * Figures out the CSS values to apply for a layout
+ * @class
+ * @param {object} settings
+ * @param {string} [settings.layout='reflowable']
+ * @param {string} [settings.spread]
+ * @param {number} [settings.minSpreadWidth=800]
+ * @param {boolean} [settings.evenSpreads=false]
+ */
+class Layout {
+ constructor(settings) {
+ this.settings = settings;
+ this.name = settings.layout || "reflowable";
+ this._spread = (settings.spread === "none") ? false : true;
+ this._minSpreadWidth = settings.minSpreadWidth || 800;
+ this._evenSpreads = settings.evenSpreads || false;
+
+ if (settings.flow === "scrolled" ||
+ settings.flow === "scrolled-continuous" ||
+ settings.flow === "scrolled-doc") {
+ this._flow = "scrolled";
+ } else {
+ this._flow = "paginated";
+ }
+
+
+ this.width = 0;
+ this.height = 0;
+ this.spreadWidth = 0;
+ this.delta = 0;
+
+ this.columnWidth = 0;
+ this.gap = 0;
+ this.divisor = 1;
+
+ this.props = {
+ name: this.name,
+ spread: this._spread,
+ flow: this._flow,
+ width: 0,
+ height: 0,
+ spreadWidth: 0,
+ delta: 0,
+ columnWidth: 0,
+ gap: 0,
+ divisor: 1
+ };
+
+ }
+
+ /**
+ * Switch the flow between paginated and scrolled
+ * @param {string} flow paginated | scrolled
+ * @return {string} simplified flow
+ */
+ flow(flow) {
+ if (typeof(flow) != "undefined") {
+ if (flow === "scrolled" ||
+ flow === "scrolled-continuous" ||
+ flow === "scrolled-doc") {
+ this._flow = "scrolled";
+ } else {
+ this._flow = "paginated";
+ }
+ // this.props.flow = this._flow;
+ this.update({flow: this._flow});
+ }
+ return this._flow;
+ }
+
+ /**
+ * Switch between using spreads or not, and set the
+ * width at which they switch to single.
+ * @param {string} spread "none" | "always" | "auto"
+ * @param {number} min integer in pixels
+ * @return {boolean} spread true | false
+ */
+ spread(spread, min) {
+
+ if (spread) {
+ this._spread = (spread === "none") ? false : true;
+ // this.props.spread = this._spread;
+ this.update({spread: this._spread});
+ }
+
+ if (min >= 0) {
+ this._minSpreadWidth = min;
+ }
+
+ return this._spread;
+ }
+
+ /**
+ * Calculate the dimensions of the pagination
+ * @param {number} _width width of the rendering
+ * @param {number} _height height of the rendering
+ * @param {number} _gap width of the gap between columns
+ */
+ calculate(_width, _height, _gap){
+
+ var divisor = 1;
+ var gap = _gap || 0;
+
+ //-- Check the width and create even width columns
+ // var fullWidth = Math.floor(_width);
+ var width = _width;
+ var height = _height;
+
+ var section = Math.floor(width / 12);
+
+ var columnWidth;
+ var spreadWidth;
+ var pageWidth;
+ var delta;
+
+ if (this._spread && width >= this._minSpreadWidth) {
+ divisor = 2;
+ } else {
+ divisor = 1;
+ }
+
+ if (this.name === "reflowable" && this._flow === "paginated" && !(_gap >= 0)) {
+ gap = ((section % 2 === 0) ? section : section - 1);
+ }
+
+ if (this.name === "pre-paginated" ) {
+ gap = 0;
+ }
+
+ //-- Double Page
+ if(divisor > 1) {
+ // width = width - gap;
+ // columnWidth = (width - gap) / divisor;
+ // gap = gap / divisor;
+ columnWidth = (width / divisor) - gap;
+ pageWidth = columnWidth + gap;
+ } else {
+ columnWidth = width;
+ pageWidth = width;
+ }
+
+ if (this.name === "pre-paginated" && divisor > 1) {
+ width = columnWidth;
+ }
+
+ spreadWidth = (columnWidth * divisor) + gap;
+
+ delta = width;
+
+ this.width = width;
+ this.height = height;
+ this.spreadWidth = spreadWidth;
+ this.pageWidth = pageWidth;
+ this.delta = delta;
+
+ this.columnWidth = columnWidth;
+ this.gap = gap;
+ this.divisor = divisor;
+
+ // this.props.width = width;
+ // this.props.height = _height;
+ // this.props.spreadWidth = spreadWidth;
+ // this.props.pageWidth = pageWidth;
+ // this.props.delta = delta;
+ //
+ // this.props.columnWidth = colWidth;
+ // this.props.gap = gap;
+ // this.props.divisor = divisor;
+
+ this.update({
+ width,
+ height,
+ spreadWidth,
+ pageWidth,
+ delta,
+ columnWidth,
+ gap,
+ divisor
+ });
+
+ }
+
+ /**
+ * Apply Css to a Document
+ * @param {Contents} contents
+ * @return {Promise}
+ */
+ format(contents, section, axis){
+ var formating;
+
+ if (this.name === "pre-paginated") {
+ formating = contents.fit(this.columnWidth, this.height, section);
+ } else if (this._flow === "paginated") {
+ formating = contents.columns(this.width, this.height, this.columnWidth, this.gap, this.settings.direction);
+ } else if (axis && axis === "horizontal") {
+ formating = contents.size(null, this.height);
+ } else {
+ formating = contents.size(this.width, null);
+ }
+
+ return formating; // might be a promise in some View Managers
+ }
+
+ /**
+ * Count number of pages
+ * @param {number} totalLength
+ * @param {number} pageLength
+ * @return {{spreads: Number, pages: Number}}
+ */
+ count(totalLength, pageLength) {
+
+ let spreads, pages;
+
+ if (this.name === "pre-paginated") {
+ spreads = 1;
+ pages = 1;
+ } else if (this._flow === "paginated") {
+ pageLength = pageLength || this.delta;
+ spreads = Math.ceil( totalLength / pageLength);
+ pages = spreads * this.divisor;
+ } else { // scrolled
+ pageLength = pageLength || this.height;
+ spreads = Math.ceil( totalLength / pageLength);
+ pages = spreads;
+ }
+
+ return {
+ spreads,
+ pages
+ };
+
+ }
+
+ /**
+ * Update props that have changed
+ * @private
+ * @param {object} props
+ */
+ update(props) {
+ // Remove props that haven't changed
+ Object.keys(props).forEach((propName) => {
+ if (this.props[propName] === props[propName]) {
+ delete props[propName];
+ }
+ });
+
+ if(Object.keys(props).length > 0) {
+ let newProps = extend(this.props, props);
+ this.emit(EVENTS.LAYOUT.UPDATED, newProps, props);
+ }
+ }
+}
+
+EventEmitter(Layout.prototype);
+
+export default Layout;
diff --git a/lib/epub.js/src/locations.js b/lib/epub.js/src/locations.js
new file mode 100644
index 0000000..913c40d
--- /dev/null
+++ b/lib/epub.js/src/locations.js
@@ -0,0 +1,501 @@
+import {qs, sprint, locationOf, defer} from "./utils/core";
+import Queue from "./utils/queue";
+import EpubCFI from "./epubcfi";
+import { EVENTS } from "./utils/constants";
+import EventEmitter from "event-emitter";
+
+/**
+ * Find Locations for a Book
+ * @param {Spine} spine
+ * @param {request} request
+ * @param {number} [pause=100]
+ */
+class Locations {
+ constructor(spine, request, pause) {
+ this.spine = spine;
+ this.request = request;
+ this.pause = pause || 100;
+
+ this.q = new Queue(this);
+ this.epubcfi = new EpubCFI();
+
+ this._locations = [];
+ this._locationsWords = [];
+ this.total = 0;
+
+ this.break = 150;
+
+ this._current = 0;
+
+ this._wordCounter = 0;
+
+ this.currentLocation = '';
+ this._currentCfi ='';
+ this.processingTimeout = undefined;
+ }
+
+ /**
+ * Load all of sections in the book to generate locations
+ * @param {int} chars how many chars to split on
+ * @return {object} locations
+ */
+ generate(chars) {
+
+ if (chars) {
+ this.break = chars;
+ }
+
+ this.q.pause();
+
+ this.spine.each(function(section) {
+ if (section.linear) {
+ this.q.enqueue(this.process.bind(this), section);
+ }
+ }.bind(this));
+
+ return this.q.run().then(function() {
+ this.total = this._locations.length - 1;
+
+ if (this._currentCfi) {
+ this.currentLocation = this._currentCfi;
+ }
+
+ return this._locations;
+ // console.log(this.percentage(this.book.rendition.location.start), this.percentage(this.book.rendition.location.end));
+ }.bind(this));
+
+ }
+
+ createRange () {
+ return {
+ startContainer: undefined,
+ startOffset: undefined,
+ endContainer: undefined,
+ endOffset: undefined
+ };
+ }
+
+ process(section) {
+
+ return section.load(this.request)
+ .then(function(contents) {
+ var completed = new defer();
+ var locations = this.parse(contents, section.cfiBase);
+ this._locations = this._locations.concat(locations);
+
+ section.unload();
+
+ this.processingTimeout = setTimeout(() => completed.resolve(locations), this.pause);
+ return completed.promise;
+ }.bind(this));
+
+ }
+
+ parse(contents, cfiBase, chars) {
+ var locations = [];
+ var range;
+ var doc = contents.ownerDocument;
+ var body = qs(doc, "body");
+ var counter = 0;
+ var prev;
+ var _break = chars || this.break;
+ var parser = function(node) {
+ var len = node.length;
+ var dist;
+ var pos = 0;
+
+ if (node.textContent.trim().length === 0) {
+ return false; // continue
+ }
+
+ // Start range
+ if (counter == 0) {
+ range = this.createRange();
+ range.startContainer = node;
+ range.startOffset = 0;
+ }
+
+ dist = _break - counter;
+
+ // Node is smaller than a break,
+ // skip over it
+ if(dist > len){
+ counter += len;
+ pos = len;
+ }
+
+
+ while (pos < len) {
+ dist = _break - counter;
+
+ if (counter === 0) {
+ // Start new range
+ pos += 1;
+ range = this.createRange();
+ range.startContainer = node;
+ range.startOffset = pos;
+ }
+
+ // pos += dist;
+
+ // Gone over
+ if(pos + dist >= len){
+ // Continue counter for next node
+ counter += len - pos;
+ // break
+ pos = len;
+ // At End
+ } else {
+ // Advance pos
+ pos += dist;
+
+ // End the previous range
+ range.endContainer = node;
+ range.endOffset = pos;
+ // cfi = section.cfiFromRange(range);
+ let cfi = new EpubCFI(range, cfiBase).toString();
+ locations.push(cfi);
+ counter = 0;
+ }
+ }
+ prev = node;
+ };
+
+ sprint(body, parser.bind(this));
+
+ // Close remaining
+ if (range && range.startContainer && prev) {
+ range.endContainer = prev;
+ range.endOffset = prev.length;
+ let cfi = new EpubCFI(range, cfiBase).toString();
+ locations.push(cfi);
+ counter = 0;
+ }
+
+ return locations;
+ }
+
+
+ /**
+ * Load all of sections in the book to generate locations
+ * @param {string} startCfi start position
+ * @param {int} wordCount how many words to split on
+ * @param {int} count result count
+ * @return {object} locations
+ */
+ generateFromWords(startCfi, wordCount, count) {
+ var start = startCfi ? new EpubCFI(startCfi) : undefined;
+ this.q.pause();
+ this._locationsWords = [];
+ this._wordCounter = 0;
+
+ this.spine.each(function(section) {
+ if (section.linear) {
+ if (start) {
+ if (section.index >= start.spinePos) {
+ this.q.enqueue(this.processWords.bind(this), section, wordCount, start, count);
+ }
+ } else {
+ this.q.enqueue(this.processWords.bind(this), section, wordCount, start, count);
+ }
+ }
+ }.bind(this));
+
+ return this.q.run().then(function() {
+ if (this._currentCfi) {
+ this.currentLocation = this._currentCfi;
+ }
+
+ return this._locationsWords;
+ }.bind(this));
+
+ }
+
+ processWords(section, wordCount, startCfi, count) {
+ if (count && this._locationsWords.length >= count) {
+ return Promise.resolve();
+ }
+
+ return section.load(this.request)
+ .then(function(contents) {
+ var completed = new defer();
+ var locations = this.parseWords(contents, section, wordCount, startCfi);
+ var remainingCount = count - this._locationsWords.length;
+ this._locationsWords = this._locationsWords.concat(locations.length >= count ? locations.slice(0, remainingCount) : locations);
+
+ section.unload();
+
+ this.processingTimeout = setTimeout(() => completed.resolve(locations), this.pause);
+ return completed.promise;
+ }.bind(this));
+ }
+
+ //http://stackoverflow.com/questions/18679576/counting-words-in-string
+ countWords(s) {
+ s = s.replace(/(^\s*)|(\s*$)/gi, "");//exclude start and end white-space
+ s = s.replace(/[ ]{2,}/gi, " ");//2 or more space to 1
+ s = s.replace(/\n /, "\n"); // exclude newline with a start spacing
+ return s.split(" ").length;
+ }
+
+ parseWords(contents, section, wordCount, startCfi) {
+ var cfiBase = section.cfiBase;
+ var locations = [];
+ var doc = contents.ownerDocument;
+ var body = qs(doc, "body");
+ var prev;
+ var _break = wordCount;
+ var foundStartNode = startCfi ? startCfi.spinePos !== section.index : true;
+ var startNode;
+ if (startCfi && section.index === startCfi.spinePos) {
+ startNode = startCfi.findNode(startCfi.range ? startCfi.path.steps.concat(startCfi.start.steps) : startCfi.path.steps, contents.ownerDocument);
+ }
+ var parser = function(node) {
+ if (!foundStartNode) {
+ if (node === startNode) {
+ foundStartNode = true;
+ } else {
+ return false;
+ }
+ }
+ if (node.textContent.length < 10) {
+ if (node.textContent.trim().length === 0) {
+ return false;
+ }
+ }
+ var len = this.countWords(node.textContent);
+ var dist;
+ var pos = 0;
+
+ if (len === 0) {
+ return false; // continue
+ }
+
+ dist = _break - this._wordCounter;
+
+ // Node is smaller than a break,
+ // skip over it
+ if (dist > len) {
+ this._wordCounter += len;
+ pos = len;
+ }
+
+
+ while (pos < len) {
+ dist = _break - this._wordCounter;
+
+ // Gone over
+ if (pos + dist >= len) {
+ // Continue counter for next node
+ this._wordCounter += len - pos;
+ // break
+ pos = len;
+ // At End
+ } else {
+ // Advance pos
+ pos += dist;
+
+ let cfi = new EpubCFI(node, cfiBase);
+ locations.push({ cfi: cfi.toString(), wordCount: this._wordCounter });
+ this._wordCounter = 0;
+ }
+ }
+ prev = node;
+ };
+
+ sprint(body, parser.bind(this));
+
+ return locations;
+ }
+
+ /**
+ * Get a location from an EpubCFI
+ * @param {EpubCFI} cfi
+ * @return {number}
+ */
+ locationFromCfi(cfi){
+ let loc;
+ if (EpubCFI.prototype.isCfiString(cfi)) {
+ cfi = new EpubCFI(cfi);
+ }
+ // Check if the location has not been set yet
+ if(this._locations.length === 0) {
+ return -1;
+ }
+
+ loc = locationOf(cfi, this._locations, this.epubcfi.compare);
+
+ if (loc > this.total) {
+ return this.total;
+ }
+
+ return loc;
+ }
+
+ /**
+ * Get a percentage position in locations from an EpubCFI
+ * @param {EpubCFI} cfi
+ * @return {number}
+ */
+ percentageFromCfi(cfi) {
+ if(this._locations.length === 0) {
+ return null;
+ }
+ // Find closest cfi
+ var loc = this.locationFromCfi(cfi);
+ // Get percentage in total
+ return this.percentageFromLocation(loc);
+ }
+
+ /**
+ * Get a percentage position from a location index
+ * @param {number} location
+ * @return {number}
+ */
+ percentageFromLocation(loc) {
+ if (!loc || !this.total) {
+ return 0;
+ }
+
+ return (loc / this.total);
+ }
+
+ /**
+ * Get an EpubCFI from location index
+ * @param {number} loc
+ * @return {EpubCFI} cfi
+ */
+ cfiFromLocation(loc){
+ var cfi = -1;
+ // check that pg is an int
+ if(typeof loc != "number"){
+ loc = parseInt(loc);
+ }
+
+ if(loc >= 0 && loc < this._locations.length) {
+ cfi = this._locations[loc];
+ }
+
+ return cfi;
+ }
+
+ /**
+ * Get an EpubCFI from location percentage
+ * @param {number} percentage
+ * @return {EpubCFI} cfi
+ */
+ cfiFromPercentage(percentage){
+ let loc;
+ if (percentage > 1) {
+ console.warn("Normalize cfiFromPercentage value to between 0 - 1");
+ }
+
+ // Make sure 1 goes to very end
+ if (percentage >= 1) {
+ let cfi = new EpubCFI(this._locations[this.total]);
+ cfi.collapse();
+ return cfi.toString();
+ }
+
+ loc = Math.ceil(this.total * percentage);
+ return this.cfiFromLocation(loc);
+ }
+
+ /**
+ * Load locations from JSON
+ * @param {json} locations
+ */
+ load(locations){
+ if (typeof locations === "string") {
+ this._locations = JSON.parse(locations);
+ } else {
+ this._locations = locations;
+ }
+ this.total = this._locations.length - 1;
+ return this._locations;
+ }
+
+ /**
+ * Save locations to JSON
+ * @return {json}
+ */
+ save(){
+ return JSON.stringify(this._locations);
+ }
+
+ getCurrent(){
+ return this._current;
+ }
+
+ setCurrent(curr){
+ var loc;
+
+ if(typeof curr == "string"){
+ this._currentCfi = curr;
+ } else if (typeof curr == "number") {
+ this._current = curr;
+ } else {
+ return;
+ }
+
+ if(this._locations.length === 0) {
+ return;
+ }
+
+ if(typeof curr == "string"){
+ loc = this.locationFromCfi(curr);
+ this._current = loc;
+ } else {
+ loc = curr;
+ }
+
+ this.emit(EVENTS.LOCATIONS.CHANGED, {
+ percentage: this.percentageFromLocation(loc)
+ });
+ }
+
+ /**
+ * Get the current location
+ */
+ get currentLocation() {
+ return this._current;
+ }
+
+ /**
+ * Set the current location
+ */
+ set currentLocation(curr) {
+ this.setCurrent(curr);
+ }
+
+ /**
+ * Locations length
+ */
+ length () {
+ return this._locations.length;
+ }
+
+ destroy () {
+ this.spine = undefined;
+ this.request = undefined;
+ this.pause = undefined;
+
+ this.q.stop();
+ this.q = undefined;
+ this.epubcfi = undefined;
+
+ this._locations = undefined
+ this.total = undefined;
+
+ this.break = undefined;
+ this._current = undefined;
+
+ this.currentLocation = undefined;
+ this._currentCfi = undefined;
+ clearTimeout(this.processingTimeout);
+ }
+}
+
+EventEmitter(Locations.prototype);
+
+export default Locations;
diff --git a/lib/epub.js/src/managers/continuous/index.js b/lib/epub.js/src/managers/continuous/index.js
new file mode 100644
index 0000000..e6a9e61
--- /dev/null
+++ b/lib/epub.js/src/managers/continuous/index.js
@@ -0,0 +1,588 @@
+import {extend, defer, requestAnimationFrame} from "../../utils/core";
+import DefaultViewManager from "../default";
+import Snap from "../helpers/snap";
+import { EVENTS } from "../../utils/constants";
+import debounce from "lodash/debounce";
+
+class ContinuousViewManager extends DefaultViewManager {
+ constructor(options) {
+ super(options);
+
+ this.name = "continuous";
+
+ this.settings = extend(this.settings || {}, {
+ infinite: true,
+ overflow: undefined,
+ axis: undefined,
+ writingMode: undefined,
+ flow: "scrolled",
+ offset: 500,
+ offsetDelta: 250,
+ width: undefined,
+ height: undefined,
+ snap: false,
+ afterScrolledTimeout: 10
+ });
+
+ extend(this.settings, options.settings || {});
+
+ // Gap can be 0, but defaults doesn't handle that
+ if (options.settings.gap != "undefined" && options.settings.gap === 0) {
+ this.settings.gap = options.settings.gap;
+ }
+
+ this.viewSettings = {
+ ignoreClass: this.settings.ignoreClass,
+ axis: this.settings.axis,
+ flow: this.settings.flow,
+ layout: this.layout,
+ width: 0,
+ height: 0,
+ forceEvenPages: false
+ };
+
+ this.scrollTop = 0;
+ this.scrollLeft = 0;
+ }
+
+ display(section, target){
+ return DefaultViewManager.prototype.display.call(this, section, target)
+ .then(function () {
+ return this.fill();
+ }.bind(this));
+ }
+
+ fill(_full){
+ var full = _full || new defer();
+
+ this.q.enqueue(() => {
+ return this.check();
+ }).then((result) => {
+ if (result) {
+ this.fill(full);
+ } else {
+ full.resolve();
+ }
+ });
+
+ return full.promise;
+ }
+
+ moveTo(offset){
+ // var bounds = this.stage.bounds();
+ // var dist = Math.floor(offset.top / bounds.height) * bounds.height;
+ var distX = 0,
+ distY = 0;
+
+ var offsetX = 0,
+ offsetY = 0;
+
+ if(!this.isPaginated) {
+ distY = offset.top;
+ offsetY = offset.top+this.settings.offsetDelta;
+ } else {
+ distX = Math.floor(offset.left / this.layout.delta) * this.layout.delta;
+ offsetX = distX+this.settings.offsetDelta;
+ }
+
+ if (distX > 0 || distY > 0) {
+ this.scrollBy(distX, distY, true);
+ }
+ }
+
+ afterResized(view){
+ this.emit(EVENTS.MANAGERS.RESIZE, view.section);
+ }
+
+ // Remove Previous Listeners if present
+ removeShownListeners(view){
+
+ // view.off("shown", this.afterDisplayed);
+ // view.off("shown", this.afterDisplayedAbove);
+ view.onDisplayed = function(){};
+
+ }
+
+ add(section){
+ var view = this.createView(section);
+
+ this.views.append(view);
+
+ view.on(EVENTS.VIEWS.RESIZED, (bounds) => {
+ view.expanded = true;
+ });
+
+ view.on(EVENTS.VIEWS.AXIS, (axis) => {
+ this.updateAxis(axis);
+ });
+
+ view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => {
+ this.updateWritingMode(mode);
+ });
+
+ // view.on(EVENTS.VIEWS.SHOWN, this.afterDisplayed.bind(this));
+ view.onDisplayed = this.afterDisplayed.bind(this);
+ view.onResize = this.afterResized.bind(this);
+
+ return view.display(this.request);
+ }
+
+ append(section){
+ var view = this.createView(section);
+
+ view.on(EVENTS.VIEWS.RESIZED, (bounds) => {
+ view.expanded = true;
+ });
+
+ view.on(EVENTS.VIEWS.AXIS, (axis) => {
+ this.updateAxis(axis);
+ });
+
+ view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => {
+ this.updateWritingMode(mode);
+ });
+
+ this.views.append(view);
+
+ view.onDisplayed = this.afterDisplayed.bind(this);
+
+ return view;
+ }
+
+ prepend(section){
+ var view = this.createView(section);
+
+ view.on(EVENTS.VIEWS.RESIZED, (bounds) => {
+ this.counter(bounds);
+ view.expanded = true;
+ });
+
+ view.on(EVENTS.VIEWS.AXIS, (axis) => {
+ this.updateAxis(axis);
+ });
+
+ view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => {
+ this.updateWritingMode(mode);
+ });
+
+ this.views.prepend(view);
+
+ view.onDisplayed = this.afterDisplayed.bind(this);
+
+ return view;
+ }
+
+ counter(bounds){
+ if(this.settings.axis === "vertical") {
+ this.scrollBy(0, bounds.heightDelta, true);
+ } else {
+ this.scrollBy(bounds.widthDelta, 0, true);
+ }
+ }
+
+ update(_offset){
+ var container = this.bounds();
+ var views = this.views.all();
+ var viewsLength = views.length;
+ var visible = [];
+ var offset = typeof _offset != "undefined" ? _offset : (this.settings.offset || 0);
+ var isVisible;
+ var view;
+
+ var updating = new defer();
+ var promises = [];
+ for (var i = 0; i < viewsLength; i++) {
+ view = views[i];
+
+ isVisible = this.isVisible(view, offset, offset, container);
+
+ if(isVisible === true) {
+ // console.log("visible " + view.index, view.displayed);
+
+ if (!view.displayed) {
+ let displayed = view.display(this.request)
+ .then(function (view) {
+ view.show();
+ }, (err) => {
+ view.hide();
+ });
+ promises.push(displayed);
+ } else {
+ view.show();
+ }
+ visible.push(view);
+ } else {
+ this.q.enqueue(view.destroy.bind(view));
+ // console.log("hidden " + view.index, view.displayed);
+
+ clearTimeout(this.trimTimeout);
+ this.trimTimeout = setTimeout(function(){
+ this.q.enqueue(this.trim.bind(this));
+ }.bind(this), 250);
+ }
+
+ }
+
+ if(promises.length){
+ return Promise.all(promises)
+ .catch((err) => {
+ updating.reject(err);
+ });
+ } else {
+ updating.resolve();
+ return updating.promise;
+ }
+
+ }
+
+ check(_offsetLeft, _offsetTop){
+ var checking = new defer();
+ var newViews = [];
+
+ var horizontal = (this.settings.axis === "horizontal");
+ var delta = this.settings.offset || 0;
+
+ if (_offsetLeft && horizontal) {
+ delta = _offsetLeft;
+ }
+
+ if (_offsetTop && !horizontal) {
+ delta = _offsetTop;
+ }
+
+ var bounds = this._bounds; // bounds saved this until resize
+
+ let offset = horizontal ? this.scrollLeft : this.scrollTop;
+ let visibleLength = horizontal ? Math.floor(bounds.width) : bounds.height;
+ let contentLength = horizontal ? this.container.scrollWidth : this.container.scrollHeight;
+ let writingMode = (this.writingMode && this.writingMode.indexOf("vertical") === 0) ? "vertical" : "horizontal";
+ let rtlScrollType = this.settings.rtlScrollType;
+ let rtl = this.settings.direction === "rtl";
+
+ if (!this.settings.fullsize) {
+ // Scroll offset starts at width of element
+ if (rtl && rtlScrollType === "default" && writingMode === "horizontal") {
+ offset = contentLength - visibleLength - offset;
+ }
+ // Scroll offset starts at 0 and goes negative
+ if (rtl && rtlScrollType === "negative" && writingMode === "horizontal") {
+ offset = offset * -1;
+ }
+ } else {
+ // Scroll offset starts at 0 and goes negative
+ if ((horizontal && rtl && rtlScrollType === "negative") ||
+ (!horizontal && rtl && rtlScrollType === "default")) {
+ offset = offset * -1;
+ }
+ }
+
+ let prepend = () => {
+ let first = this.views.first();
+ let prev = first && first.section.prev();
+
+ if(prev) {
+ newViews.push(this.prepend(prev));
+ }
+ };
+
+ let append = () => {
+ let last = this.views.last();
+ let next = last && last.section.next();
+
+ if(next) {
+ newViews.push(this.append(next));
+ }
+
+ };
+
+ let end = offset + visibleLength + delta;
+ let start = offset - delta;
+
+ if (end >= contentLength) {
+ append();
+ }
+
+ if (start < 0) {
+ prepend();
+ }
+
+
+ let promises = newViews.map((view) => {
+ return view.display(this.request);
+ });
+
+ if(newViews.length){
+ return Promise.all(promises)
+ .then(() => {
+ return this.check();
+ })
+ .then(() => {
+ // Check to see if anything new is on screen after rendering
+ return this.update(delta);
+ }, (err) => {
+ return err;
+ });
+ } else {
+ this.q.enqueue(function(){
+ this.update();
+ }.bind(this));
+ checking.resolve(false);
+ return checking.promise;
+ }
+
+
+ }
+
+ trim(){
+ var task = new defer();
+ var displayed = this.views.displayed();
+ var first = displayed[0];
+ var last = displayed[displayed.length-1];
+ var firstIndex = this.views.indexOf(first);
+ var lastIndex = this.views.indexOf(last);
+ var above = this.views.slice(0, firstIndex);
+ var below = this.views.slice(lastIndex+1);
+
+ // Erase all but last above
+ for (var i = 0; i < above.length-1; i++) {
+ this.erase(above[i], above);
+ }
+
+ // Erase all except first below
+ for (var j = 1; j < below.length; j++) {
+ this.erase(below[j]);
+ }
+
+ task.resolve();
+ return task.promise;
+ }
+
+ erase(view, above){ //Trim
+
+ var prevTop;
+ var prevLeft;
+
+ if(!this.settings.fullsize) {
+ prevTop = this.container.scrollTop;
+ prevLeft = this.container.scrollLeft;
+ } else {
+ prevTop = window.scrollY;
+ prevLeft = window.scrollX;
+ }
+
+ var bounds = view.bounds();
+
+ this.views.remove(view);
+
+ if(above) {
+ if (this.settings.axis === "vertical") {
+ this.scrollTo(0, prevTop - bounds.height, true);
+ } else {
+ if(this.settings.direction === 'rtl') {
+ if (!this.settings.fullsize) {
+ this.scrollTo(prevLeft, 0, true);
+ } else {
+ this.scrollTo(prevLeft + Math.floor(bounds.width), 0, true);
+ }
+ } else {
+ this.scrollTo(prevLeft - Math.floor(bounds.width), 0, true);
+ }
+ }
+ }
+
+ }
+
+ addEventListeners(stage){
+
+ window.addEventListener("unload", function(e){
+ this.ignore = true;
+ // this.scrollTo(0,0);
+ this.destroy();
+ }.bind(this));
+
+ this.addScrollListeners();
+
+ if (this.isPaginated && this.settings.snap) {
+ this.snapper = new Snap(this, this.settings.snap && (typeof this.settings.snap === "object") && this.settings.snap);
+ }
+ }
+
+ addScrollListeners() {
+ var scroller;
+
+ this.tick = requestAnimationFrame;
+
+ let dir = this.settings.direction === "rtl" && this.settings.rtlScrollType === "default" ? -1 : 1;
+
+ this.scrollDeltaVert = 0;
+ this.scrollDeltaHorz = 0;
+
+ if(!this.settings.fullsize) {
+ scroller = this.container;
+ this.scrollTop = this.container.scrollTop;
+ this.scrollLeft = this.container.scrollLeft;
+ } else {
+ scroller = window;
+ this.scrollTop = window.scrollY * dir;
+ this.scrollLeft = window.scrollX * dir;
+ }
+
+ this._onScroll = this.onScroll.bind(this);
+ scroller.addEventListener("scroll", this._onScroll);
+ this._scrolled = debounce(this.scrolled.bind(this), 30);
+ // this.tick.call(window, this.onScroll.bind(this));
+
+ this.didScroll = false;
+
+ }
+
+ removeEventListeners(){
+ var scroller;
+
+ if(!this.settings.fullsize) {
+ scroller = this.container;
+ } else {
+ scroller = window;
+ }
+
+ scroller.removeEventListener("scroll", this._onScroll);
+ this._onScroll = undefined;
+ }
+
+ onScroll(){
+ let scrollTop;
+ let scrollLeft;
+ let dir = this.settings.direction === "rtl" && this.settings.rtlScrollType === "default" ? -1 : 1;
+
+ if(!this.settings.fullsize) {
+ scrollTop = this.container.scrollTop;
+ scrollLeft = this.container.scrollLeft;
+ } else {
+ scrollTop = window.scrollY * dir;
+ scrollLeft = window.scrollX * dir;
+ }
+
+ this.scrollTop = scrollTop;
+ this.scrollLeft = scrollLeft;
+
+ if(!this.ignore) {
+
+ this._scrolled();
+
+ } else {
+ this.ignore = false;
+ }
+
+ this.scrollDeltaVert += Math.abs(scrollTop-this.prevScrollTop);
+ this.scrollDeltaHorz += Math.abs(scrollLeft-this.prevScrollLeft);
+
+ this.prevScrollTop = scrollTop;
+ this.prevScrollLeft = scrollLeft;
+
+ clearTimeout(this.scrollTimeout);
+ this.scrollTimeout = setTimeout(function(){
+ this.scrollDeltaVert = 0;
+ this.scrollDeltaHorz = 0;
+ }.bind(this), 150);
+
+ clearTimeout(this.afterScrolled);
+
+ this.didScroll = false;
+
+ }
+
+ scrolled() {
+
+ this.q.enqueue(function() {
+ return this.check();
+ }.bind(this));
+
+ this.emit(EVENTS.MANAGERS.SCROLL, {
+ top: this.scrollTop,
+ left: this.scrollLeft
+ });
+
+ clearTimeout(this.afterScrolled);
+ this.afterScrolled = setTimeout(function () {
+
+ // Don't report scroll if we are about the snap
+ if (this.snapper && this.snapper.supportsTouch && this.snapper.needsSnap()) {
+ return;
+ }
+
+ this.emit(EVENTS.MANAGERS.SCROLLED, {
+ top: this.scrollTop,
+ left: this.scrollLeft
+ });
+
+ }.bind(this), this.settings.afterScrolledTimeout);
+ }
+
+ next(){
+
+ let delta = this.layout.props.name === "pre-paginated" &&
+ this.layout.props.spread ? this.layout.props.delta * 2 : this.layout.props.delta;
+
+ if(!this.views.length) return;
+
+ if(this.isPaginated && this.settings.axis === "horizontal") {
+
+ this.scrollBy(delta, 0, true);
+
+ } else {
+
+ this.scrollBy(0, this.layout.height, true);
+
+ }
+
+ this.q.enqueue(function() {
+ return this.check();
+ }.bind(this));
+ }
+
+ prev(){
+
+ let delta = this.layout.props.name === "pre-paginated" &&
+ this.layout.props.spread ? this.layout.props.delta * 2 : this.layout.props.delta;
+
+ if(!this.views.length) return;
+
+ if(this.isPaginated && this.settings.axis === "horizontal") {
+
+ this.scrollBy(-delta, 0, true);
+
+ } else {
+
+ this.scrollBy(0, -this.layout.height, true);
+
+ }
+
+ this.q.enqueue(function() {
+ return this.check();
+ }.bind(this));
+ }
+
+ updateFlow(flow){
+ if (this.rendered && this.snapper) {
+ this.snapper.destroy();
+ this.snapper = undefined;
+ }
+
+ super.updateFlow(flow, "scroll");
+
+ if (this.rendered && this.isPaginated && this.settings.snap) {
+ this.snapper = new Snap(this, this.settings.snap && (typeof this.settings.snap === "object") && this.settings.snap);
+ }
+ }
+
+ destroy(){
+ super.destroy();
+
+ if (this.snapper) {
+ this.snapper.destroy();
+ }
+ }
+
+}
+
+export default ContinuousViewManager;
diff --git a/lib/epub.js/src/managers/default/index.js b/lib/epub.js/src/managers/default/index.js
new file mode 100644
index 0000000..b24a4f6
--- /dev/null
+++ b/lib/epub.js/src/managers/default/index.js
@@ -0,0 +1,1073 @@
+import EventEmitter from "event-emitter";
+import {extend, defer, windowBounds, isNumber} from "../../utils/core";
+import scrollType from "../../utils/scrolltype";
+import Mapping from "../../mapping";
+import Queue from "../../utils/queue";
+import Stage from "../helpers/stage";
+import Views from "../helpers/views";
+import { EVENTS } from "../../utils/constants";
+
+class DefaultViewManager {
+ constructor(options) {
+
+ this.name = "default";
+ this.optsSettings = options.settings;
+ this.View = options.view;
+ this.request = options.request;
+ this.renditionQueue = options.queue;
+ this.q = new Queue(this);
+
+ this.settings = extend(this.settings || {}, {
+ infinite: true,
+ hidden: false,
+ width: undefined,
+ height: undefined,
+ axis: undefined,
+ writingMode: undefined,
+ flow: "scrolled",
+ ignoreClass: "",
+ fullsize: undefined
+ });
+
+ extend(this.settings, options.settings || {});
+
+ this.viewSettings = {
+ ignoreClass: this.settings.ignoreClass,
+ axis: this.settings.axis,
+ flow: this.settings.flow,
+ layout: this.layout,
+ method: this.settings.method, // srcdoc, blobUrl, write
+ width: 0,
+ height: 0,
+ forceEvenPages: true
+ };
+
+ this.rendered = false;
+
+ }
+
+ render(element, size){
+ let tag = element.tagName;
+
+ if (typeof this.settings.fullsize === "undefined" &&
+ tag && (tag.toLowerCase() == "body" ||
+ tag.toLowerCase() == "html")) {
+ this.settings.fullsize = true;
+ }
+
+ if (this.settings.fullsize) {
+ this.settings.overflow = "visible";
+ this.overflow = this.settings.overflow;
+ }
+
+ this.settings.size = size;
+
+ this.settings.rtlScrollType = scrollType();
+
+ // Save the stage
+ this.stage = new Stage({
+ width: size.width,
+ height: size.height,
+ overflow: this.overflow,
+ hidden: this.settings.hidden,
+ axis: this.settings.axis,
+ fullsize: this.settings.fullsize,
+ direction: this.settings.direction
+ });
+
+ this.stage.attachTo(element);
+
+ // Get this stage container div
+ this.container = this.stage.getContainer();
+
+ // Views array methods
+ this.views = new Views(this.container);
+
+ // Calculate Stage Size
+ this._bounds = this.bounds();
+ this._stageSize = this.stage.size();
+
+ // Set the dimensions for views
+ this.viewSettings.width = this._stageSize.width;
+ this.viewSettings.height = this._stageSize.height;
+
+ // Function to handle a resize event.
+ // Will only attach if width and height are both fixed.
+ this.stage.onResize(this.onResized.bind(this));
+
+ this.stage.onOrientationChange(this.onOrientationChange.bind(this));
+
+ // Add Event Listeners
+ this.addEventListeners();
+
+ // Add Layout method
+ // this.applyLayoutMethod();
+ if (this.layout) {
+ this.updateLayout();
+ }
+
+ this.rendered = true;
+
+ }
+
+ addEventListeners(){
+ var scroller;
+
+ window.addEventListener("unload", function(e){
+ this.destroy();
+ }.bind(this));
+
+ if(!this.settings.fullsize) {
+ scroller = this.container;
+ } else {
+ scroller = window;
+ }
+
+ this._onScroll = this.onScroll.bind(this);
+ scroller.addEventListener("scroll", this._onScroll);
+ }
+
+ removeEventListeners(){
+ var scroller;
+
+ if(!this.settings.fullsize) {
+ scroller = this.container;
+ } else {
+ scroller = window;
+ }
+
+ scroller.removeEventListener("scroll", this._onScroll);
+ this._onScroll = undefined;
+ }
+
+ destroy(){
+ clearTimeout(this.orientationTimeout);
+ clearTimeout(this.resizeTimeout);
+ clearTimeout(this.afterScrolled);
+
+ this.clear();
+
+ this.removeEventListeners();
+
+ this.stage.destroy();
+
+ this.rendered = false;
+
+ /*
+
+ clearTimeout(this.trimTimeout);
+ if(this.settings.hidden) {
+ this.element.removeChild(this.wrapper);
+ } else {
+ this.element.removeChild(this.container);
+ }
+ */
+ }
+
+ onOrientationChange(e) {
+ let {orientation} = window;
+
+ if(this.optsSettings.resizeOnOrientationChange) {
+ this.resize();
+ }
+
+ // Per ampproject:
+ // In IOS 10.3, the measured size of an element is incorrect if the
+ // element size depends on window size directly and the measurement
+ // happens in window.resize event. Adding a timeout for correct
+ // measurement. See https://github.com/ampproject/amphtml/issues/8479
+ clearTimeout(this.orientationTimeout);
+ this.orientationTimeout = setTimeout(function(){
+ this.orientationTimeout = undefined;
+
+ if(this.optsSettings.resizeOnOrientationChange) {
+ this.resize();
+ }
+
+ this.emit(EVENTS.MANAGERS.ORIENTATION_CHANGE, orientation);
+ }.bind(this), 500);
+
+ }
+
+ onResized(e) {
+ this.resize();
+ }
+
+ resize(width, height, epubcfi){
+ let stageSize = this.stage.size(width, height);
+
+ // For Safari, wait for orientation to catch up
+ // if the window is a square
+ this.winBounds = windowBounds();
+ if (this.orientationTimeout &&
+ this.winBounds.width === this.winBounds.height) {
+ // reset the stage size for next resize
+ this._stageSize = undefined;
+ return;
+ }
+
+ if (this._stageSize &&
+ this._stageSize.width === stageSize.width &&
+ this._stageSize.height === stageSize.height ) {
+ // Size is the same, no need to resize
+ return;
+ }
+
+ this._stageSize = stageSize;
+
+ this._bounds = this.bounds();
+
+ // Clear current views
+ this.clear();
+
+ // Update for new views
+ this.viewSettings.width = this._stageSize.width;
+ this.viewSettings.height = this._stageSize.height;
+
+ this.updateLayout();
+
+ this.emit(EVENTS.MANAGERS.RESIZED, {
+ width: this._stageSize.width,
+ height: this._stageSize.height
+ }, epubcfi);
+ }
+
+ createView(section, forceRight) {
+ return new this.View(section, extend(this.viewSettings, { forceRight }) );
+ }
+
+ handleNextPrePaginated(forceRight, section, action) {
+ let next;
+
+ if (this.layout.name === "pre-paginated" && this.layout.divisor > 1) {
+ if (forceRight || section.index === 0) {
+ // First page (cover) should stand alone for pre-paginated books
+ return;
+ }
+ next = section.next();
+ if (next && !next.properties.includes("page-spread-left")) {
+ return action.call(this, next);
+ }
+ }
+ }
+
+ display(section, target){
+
+ var displaying = new defer();
+ var displayed = displaying.promise;
+
+ // Check if moving to target is needed
+ if (target === section.href || isNumber(target)) {
+ target = undefined;
+ }
+
+ // Check to make sure the section we want isn't already shown
+ var visible = this.views.find(section);
+
+ // View is already shown, just move to correct location in view
+ if(visible && section && this.layout.name !== "pre-paginated") {
+ let offset = visible.offset();
+
+ if (this.settings.direction === "ltr") {
+ this.scrollTo(offset.left, offset.top, true);
+ } else {
+ let width = visible.width();
+ this.scrollTo(offset.left + width, offset.top, true);
+ }
+
+ if(target) {
+ let offset = visible.locationOf(target);
+ let width = visible.width();
+ this.moveTo(offset, width);
+ }
+
+ displaying.resolve();
+ return displayed;
+ }
+
+ // Hide all current views
+ this.clear();
+
+ let forceRight = false;
+ if (this.layout.name === "pre-paginated" && this.layout.divisor === 2 && section.properties.includes("page-spread-right")) {
+ forceRight = true;
+ }
+
+ this.add(section, forceRight)
+ .then(function(view){
+
+ // Move to correct place within the section, if needed
+ if(target) {
+ let offset = view.locationOf(target);
+ let width = view.width();
+ this.moveTo(offset, width);
+ }
+
+ }.bind(this), (err) => {
+ displaying.reject(err);
+ })
+ .then(function(){
+ return this.handleNextPrePaginated(forceRight, section, this.add);
+ }.bind(this))
+ .then(function(){
+
+ this.views.show();
+
+ displaying.resolve();
+
+ }.bind(this));
+ // .then(function(){
+ // return this.hooks.display.trigger(view);
+ // }.bind(this))
+ // .then(function(){
+ // this.views.show();
+ // }.bind(this));
+ return displayed;
+ }
+
+ afterDisplayed(view){
+ this.emit(EVENTS.MANAGERS.ADDED, view);
+ }
+
+ afterResized(view){
+ this.emit(EVENTS.MANAGERS.RESIZE, view.section);
+ }
+
+ moveTo(offset, width){
+ var distX = 0,
+ distY = 0;
+
+ if(!this.isPaginated) {
+ distY = offset.top;
+ } else {
+ distX = Math.floor(offset.left / this.layout.delta) * this.layout.delta;
+
+ if (distX + this.layout.delta > this.container.scrollWidth) {
+ distX = this.container.scrollWidth - this.layout.delta;
+ }
+
+ distY = Math.floor(offset.top / this.layout.delta) * this.layout.delta;
+
+ if (distY + this.layout.delta > this.container.scrollHeight) {
+ distY = this.container.scrollHeight - this.layout.delta;
+ }
+ }
+ if(this.settings.direction === 'rtl'){
+ /***
+ the `floor` function above (L343) is on positive values, so we should add one `layout.delta`
+ to distX or use `Math.ceil` function, or multiply offset.left by -1
+ before `Math.floor`
+ */
+ distX = distX + this.layout.delta
+ distX = distX - width
+ }
+ this.scrollTo(distX, distY, true);
+ }
+
+ add(section, forceRight){
+ var view = this.createView(section, forceRight);
+
+ this.views.append(view);
+
+ // view.on(EVENTS.VIEWS.SHOWN, this.afterDisplayed.bind(this));
+ view.onDisplayed = this.afterDisplayed.bind(this);
+ view.onResize = this.afterResized.bind(this);
+
+ view.on(EVENTS.VIEWS.AXIS, (axis) => {
+ this.updateAxis(axis);
+ });
+
+ view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => {
+ this.updateWritingMode(mode);
+ });
+
+ return view.display(this.request);
+ }
+
+ append(section, forceRight){
+ var view = this.createView(section, forceRight);
+ this.views.append(view);
+
+ view.onDisplayed = this.afterDisplayed.bind(this);
+ view.onResize = this.afterResized.bind(this);
+
+ view.on(EVENTS.VIEWS.AXIS, (axis) => {
+ this.updateAxis(axis);
+ });
+
+ view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => {
+ this.updateWritingMode(mode);
+ });
+
+ return view.display(this.request);
+ }
+
+ prepend(section, forceRight){
+ var view = this.createView(section, forceRight);
+
+ view.on(EVENTS.VIEWS.RESIZED, (bounds) => {
+ this.counter(bounds);
+ });
+
+ this.views.prepend(view);
+
+ view.onDisplayed = this.afterDisplayed.bind(this);
+ view.onResize = this.afterResized.bind(this);
+
+ view.on(EVENTS.VIEWS.AXIS, (axis) => {
+ this.updateAxis(axis);
+ });
+
+ view.on(EVENTS.VIEWS.WRITING_MODE, (mode) => {
+ this.updateWritingMode(mode);
+ });
+
+ return view.display(this.request);
+ }
+
+ counter(bounds){
+ if(this.settings.axis === "vertical") {
+ this.scrollBy(0, bounds.heightDelta, true);
+ } else {
+ this.scrollBy(bounds.widthDelta, 0, true);
+ }
+
+ }
+
+ // resizeView(view) {
+ //
+ // if(this.settings.globalLayoutProperties.layout === "pre-paginated") {
+ // view.lock("both", this.bounds.width, this.bounds.height);
+ // } else {
+ // view.lock("width", this.bounds.width, this.bounds.height);
+ // }
+ //
+ // };
+
+ next(){
+ var next;
+ var left;
+
+ let dir = this.settings.direction;
+
+ if(!this.views.length) return;
+
+ if(this.isPaginated && this.settings.axis === "horizontal" && (!dir || dir === "ltr")) {
+
+ this.scrollLeft = this.container.scrollLeft;
+
+ left = this.container.scrollLeft + this.container.offsetWidth + this.layout.delta;
+
+ if(left <= this.container.scrollWidth) {
+ this.scrollBy(this.layout.delta, 0, true);
+ } else {
+ next = this.views.last().section.next();
+ }
+ } else if (this.isPaginated && this.settings.axis === "horizontal" && dir === "rtl") {
+
+ this.scrollLeft = this.container.scrollLeft;
+
+ if (this.settings.rtlScrollType === "default"){
+ left = this.container.scrollLeft;
+
+ if (left > 0) {
+ this.scrollBy(this.layout.delta, 0, true);
+ } else {
+ next = this.views.last().section.next();
+ }
+ } else {
+ left = this.container.scrollLeft + ( this.layout.delta * -1 );
+
+ if (left > this.container.scrollWidth * -1){
+ this.scrollBy(this.layout.delta, 0, true);
+ } else {
+ next = this.views.last().section.next();
+ }
+ }
+
+ } else if (this.isPaginated && this.settings.axis === "vertical") {
+
+ this.scrollTop = this.container.scrollTop;
+
+ let top = this.container.scrollTop + this.container.offsetHeight;
+
+ if(top < this.container.scrollHeight) {
+ this.scrollBy(0, this.layout.height, true);
+ } else {
+ next = this.views.last().section.next();
+ }
+
+ } else {
+ next = this.views.last().section.next();
+ }
+
+ if(next) {
+ this.clear();
+ // The new section may have a different writing-mode from the old section. Thus, we need to update layout.
+ this.updateLayout();
+
+ let forceRight = false;
+ if (this.layout.name === "pre-paginated" && this.layout.divisor === 2 && next.properties.includes("page-spread-right")) {
+ forceRight = true;
+ }
+
+ return this.append(next, forceRight)
+ .then(function(){
+ return this.handleNextPrePaginated(forceRight, next, this.append);
+ }.bind(this), (err) => {
+ return err;
+ })
+ .then(function(){
+
+ // Reset position to start for scrolled-doc vertical-rl in default mode
+ if (!this.isPaginated &&
+ this.settings.axis === "horizontal" &&
+ this.settings.direction === "rtl" &&
+ this.settings.rtlScrollType === "default") {
+
+ this.scrollTo(this.container.scrollWidth, 0, true);
+ }
+ this.views.show();
+ }.bind(this));
+ }
+
+
+ }
+
+ prev(){
+ var prev;
+ var left;
+ let dir = this.settings.direction;
+
+ if(!this.views.length) return;
+
+ if(this.isPaginated && this.settings.axis === "horizontal" && (!dir || dir === "ltr")) {
+
+ this.scrollLeft = this.container.scrollLeft;
+
+ left = this.container.scrollLeft;
+
+ if(left > 0) {
+ this.scrollBy(-this.layout.delta, 0, true);
+ } else {
+ prev = this.views.first().section.prev();
+ }
+
+ } else if (this.isPaginated && this.settings.axis === "horizontal" && dir === "rtl") {
+
+ this.scrollLeft = this.container.scrollLeft;
+
+ if (this.settings.rtlScrollType === "default"){
+ left = this.container.scrollLeft + this.container.offsetWidth;
+
+ if (left < this.container.scrollWidth) {
+ this.scrollBy(-this.layout.delta, 0, true);
+ } else {
+ prev = this.views.first().section.prev();
+ }
+ }
+ else{
+ left = this.container.scrollLeft;
+
+ if (left < 0) {
+ this.scrollBy(-this.layout.delta, 0, true);
+ } else {
+ prev = this.views.first().section.prev();
+ }
+ }
+
+ } else if (this.isPaginated && this.settings.axis === "vertical") {
+
+ this.scrollTop = this.container.scrollTop;
+
+ let top = this.container.scrollTop;
+
+ if(top > 0) {
+ this.scrollBy(0, -(this.layout.height), true);
+ } else {
+ prev = this.views.first().section.prev();
+ }
+
+ } else {
+
+ prev = this.views.first().section.prev();
+
+ }
+
+ if(prev) {
+ this.clear();
+ // The new section may have a different writing-mode from the old section. Thus, we need to update layout.
+ this.updateLayout();
+
+ let forceRight = false;
+ if (this.layout.name === "pre-paginated" && this.layout.divisor === 2 && typeof prev.prev() !== "object") {
+ forceRight = true;
+ }
+
+ return this.prepend(prev, forceRight)
+ .then(function(){
+ var left;
+ if (this.layout.name === "pre-paginated" && this.layout.divisor > 1) {
+ left = prev.prev();
+ if (left) {
+ return this.prepend(left);
+ }
+ }
+ }.bind(this), (err) => {
+ return err;
+ })
+ .then(function(){
+ if(this.isPaginated && this.settings.axis === "horizontal") {
+ if (this.settings.direction === "rtl") {
+ if (this.settings.rtlScrollType === "default"){
+ this.scrollTo(0, 0, true);
+ }
+ else{
+ this.scrollTo((this.container.scrollWidth * -1) + this.layout.delta, 0, true);
+ }
+ } else {
+ this.scrollTo(this.container.scrollWidth - this.layout.delta, 0, true);
+ }
+ }
+ this.views.show();
+ }.bind(this));
+ }
+ }
+
+ current(){
+ var visible = this.visible();
+ if(visible.length){
+ // Current is the last visible view
+ return visible[visible.length-1];
+ }
+ return null;
+ }
+
+ clear () {
+
+ // this.q.clear();
+
+ if (this.views) {
+ this.views.hide();
+ this.scrollTo(0,0, true);
+ this.views.clear();
+ }
+ }
+
+ currentLocation(){
+ this.updateLayout();
+ if (this.isPaginated && this.settings.axis === "horizontal") {
+ this.location = this.paginatedLocation();
+ } else {
+ this.location = this.scrolledLocation();
+ }
+ return this.location;
+ }
+
+ scrolledLocation() {
+ let visible = this.visible();
+ let container = this.container.getBoundingClientRect();
+ let pageHeight = (container.height < window.innerHeight) ? container.height : window.innerHeight;
+ let pageWidth = (container.width < window.innerWidth) ? container.width : window.innerWidth;
+ let vertical = (this.settings.axis === "vertical");
+ let rtl = (this.settings.direction === "rtl");
+
+ let offset = 0;
+ let used = 0;
+
+ if(this.settings.fullsize) {
+ offset = vertical ? window.scrollY : window.scrollX;
+ }
+
+ let sections = visible.map((view) => {
+ let {index, href} = view.section;
+ let position = view.position();
+ let width = view.width();
+ let height = view.height();
+
+ let startPos;
+ let endPos;
+ let stopPos;
+ let totalPages;
+
+ if (vertical) {
+ startPos = offset + container.top - position.top + used;
+ endPos = startPos + pageHeight - used;
+ totalPages = this.layout.count(height, pageHeight).pages;
+ stopPos = pageHeight;
+ } else {
+ startPos = offset + container.left - position.left + used;
+ endPos = startPos + pageWidth - used;
+ totalPages = this.layout.count(width, pageWidth).pages;
+ stopPos = pageWidth;
+ }
+
+ let currPage = Math.ceil(startPos / stopPos);
+ let pages = [];
+ let endPage = Math.ceil(endPos / stopPos);
+
+ // Reverse page counts for horizontal rtl
+ if (this.settings.direction === "rtl" && !vertical) {
+ let tempStartPage = currPage;
+ currPage = totalPages - endPage;
+ endPage = totalPages - tempStartPage;
+ }
+
+ pages = [];
+ for (var i = currPage; i <= endPage; i++) {
+ let pg = i + 1;
+ pages.push(pg);
+ }
+
+ let mapping = this.mapping.page(view.contents, view.section.cfiBase, startPos, endPos);
+
+ return {
+ index,
+ href,
+ pages,
+ totalPages,
+ mapping
+ };
+ });
+
+ return sections;
+ }
+
+ paginatedLocation(){
+ let visible = this.visible();
+ let container = this.container.getBoundingClientRect();
+
+ let left = 0;
+ let used = 0;
+
+ if(this.settings.fullsize) {
+ left = window.scrollX;
+ }
+
+ let sections = visible.map((view) => {
+ let {index, href} = view.section;
+ let offset;
+ let position = view.position();
+ let width = view.width();
+
+ // Find mapping
+ let start;
+ let end;
+ let pageWidth;
+
+ if (this.settings.direction === "rtl") {
+ offset = container.right - left;
+ pageWidth = Math.min(Math.abs(offset - position.left), this.layout.width) - used;
+ end = position.width - (position.right - offset) - used;
+ start = end - pageWidth;
+ } else {
+ offset = container.left + left;
+ pageWidth = Math.min(position.right - offset, this.layout.width) - used;
+ start = offset - position.left + used;
+ end = start + pageWidth;
+ }
+
+ used += pageWidth;
+
+ let mapping = this.mapping.page(view.contents, view.section.cfiBase, start, end);
+
+ let totalPages = this.layout.count(width).pages;
+ let startPage = Math.floor(start / this.layout.pageWidth);
+ let pages = [];
+ let endPage = Math.floor(end / this.layout.pageWidth);
+
+ // start page should not be negative
+ if (startPage < 0) {
+ startPage = 0;
+ endPage = endPage + 1;
+ }
+
+ // Reverse page counts for rtl
+ if (this.settings.direction === "rtl") {
+ let tempStartPage = startPage;
+ startPage = totalPages - endPage;
+ endPage = totalPages - tempStartPage;
+ }
+
+
+ for (var i = startPage + 1; i <= endPage; i++) {
+ let pg = i;
+ pages.push(pg);
+ }
+
+ return {
+ index,
+ href,
+ pages,
+ totalPages,
+ mapping
+ };
+ });
+
+ return sections;
+ }
+
+ isVisible(view, offsetPrev, offsetNext, _container){
+ var position = view.position();
+ var container = _container || this.bounds();
+
+ if(this.settings.axis === "horizontal" &&
+ position.right > container.left - offsetPrev &&
+ position.left < container.right + offsetNext) {
+
+ return true;
+
+ } else if(this.settings.axis === "vertical" &&
+ position.bottom > container.top - offsetPrev &&
+ position.top < container.bottom + offsetNext) {
+
+ return true;
+ }
+
+ return false;
+
+ }
+
+ visible(){
+ var container = this.bounds();
+ var views = this.views.displayed();
+ var viewsLength = views.length;
+ var visible = [];
+ var isVisible;
+ var view;
+
+ for (var i = 0; i < viewsLength; i++) {
+ view = views[i];
+ isVisible = this.isVisible(view, 0, 0, container);
+
+ if(isVisible === true) {
+ visible.push(view);
+ }
+
+ }
+ return visible;
+ }
+
+ scrollBy(x, y, silent){
+ let dir = this.settings.direction === "rtl" ? -1 : 1;
+
+ if(silent) {
+ this.ignore = true;
+ }
+
+ if(!this.settings.fullsize) {
+ if(x) this.container.scrollLeft += x * dir;
+ if(y) this.container.scrollTop += y;
+ } else {
+ window.scrollBy(x * dir, y * dir);
+ }
+ this.scrolled = true;
+ }
+
+ scrollTo(x, y, silent){
+ if(silent) {
+ this.ignore = true;
+ }
+
+ if(!this.settings.fullsize) {
+ this.container.scrollLeft = x;
+ this.container.scrollTop = y;
+ } else {
+ window.scrollTo(x,y);
+ }
+ this.scrolled = true;
+ }
+
+ onScroll(){
+ let scrollTop;
+ let scrollLeft;
+
+ if(!this.settings.fullsize) {
+ scrollTop = this.container.scrollTop;
+ scrollLeft = this.container.scrollLeft;
+ } else {
+ scrollTop = window.scrollY;
+ scrollLeft = window.scrollX;
+ }
+
+ this.scrollTop = scrollTop;
+ this.scrollLeft = scrollLeft;
+
+ if(!this.ignore) {
+ this.emit(EVENTS.MANAGERS.SCROLL, {
+ top: scrollTop,
+ left: scrollLeft
+ });
+
+ clearTimeout(this.afterScrolled);
+ this.afterScrolled = setTimeout(function () {
+ this.emit(EVENTS.MANAGERS.SCROLLED, {
+ top: this.scrollTop,
+ left: this.scrollLeft
+ });
+ }.bind(this), 20);
+
+
+
+ } else {
+ this.ignore = false;
+ }
+
+ }
+
+ bounds() {
+ var bounds;
+
+ bounds = this.stage.bounds();
+
+ return bounds;
+ }
+
+ applyLayout(layout) {
+
+ this.layout = layout;
+ this.updateLayout();
+ if (this.views && this.views.length > 0 && this.layout.name === "pre-paginated") {
+ this.display(this.views.first().section);
+ }
+ // this.manager.layout(this.layout.format);
+ }
+
+ updateLayout() {
+
+ if (!this.stage) {
+ return;
+ }
+
+ this._stageSize = this.stage.size();
+
+ if(!this.isPaginated) {
+ this.layout.calculate(this._stageSize.width, this._stageSize.height);
+ } else {
+ this.layout.calculate(
+ this._stageSize.width,
+ this._stageSize.height,
+ this.settings.gap
+ );
+
+ // Set the look ahead offset for what is visible
+ this.settings.offset = this.layout.delta / this.layout.divisor;
+
+ // this.stage.addStyleRules("iframe", [{"margin-right" : this.layout.gap + "px"}]);
+
+ }
+
+ // Set the dimensions for views
+ this.viewSettings.width = this.layout.width;
+ this.viewSettings.height = this.layout.height;
+
+ this.setLayout(this.layout);
+ }
+
+ setLayout(layout){
+
+ this.viewSettings.layout = layout;
+
+ this.mapping = new Mapping(layout.props, this.settings.direction, this.settings.axis);
+
+ if(this.views) {
+
+ this.views.forEach(function(view){
+ if (view) {
+ view.setLayout(layout);
+ }
+ });
+
+ }
+
+ }
+
+ updateWritingMode(mode) {
+ this.writingMode = mode;
+ }
+
+ updateAxis(axis, forceUpdate){
+
+ if (!forceUpdate && axis === this.settings.axis) {
+ return;
+ }
+
+ this.settings.axis = axis;
+
+ this.stage && this.stage.axis(axis);
+
+ this.viewSettings.axis = axis;
+
+ if (this.mapping) {
+ this.mapping = new Mapping(this.layout.props, this.settings.direction, this.settings.axis);
+ }
+
+ if (this.layout) {
+ if (axis === "vertical") {
+ this.layout.spread("none");
+ } else {
+ this.layout.spread(this.layout.settings.spread);
+ }
+ }
+ }
+
+ updateFlow(flow, defaultScrolledOverflow="auto"){
+ let isPaginated = (flow === "paginated" || flow === "auto");
+
+ this.isPaginated = isPaginated;
+
+ if (flow === "scrolled-doc" ||
+ flow === "scrolled-continuous" ||
+ flow === "scrolled") {
+ this.updateAxis("vertical");
+ } else {
+ this.updateAxis("horizontal");
+ }
+
+ this.viewSettings.flow = flow;
+
+ if (!this.settings.overflow) {
+ this.overflow = isPaginated ? "hidden" : defaultScrolledOverflow;
+ } else {
+ this.overflow = this.settings.overflow;
+ }
+
+ this.stage && this.stage.overflow(this.overflow);
+
+ this.updateLayout();
+
+ }
+
+ getContents(){
+ var contents = [];
+ if (!this.views) {
+ return contents;
+ }
+ this.views.forEach(function(view){
+ const viewContents = view && view.contents;
+ if (viewContents) {
+ contents.push(viewContents);
+ }
+ });
+ return contents;
+ }
+
+ direction(dir="ltr") {
+ this.settings.direction = dir;
+
+ this.stage && this.stage.direction(dir);
+
+ this.viewSettings.direction = dir;
+
+ this.updateLayout();
+ }
+
+ isRendered() {
+ return this.rendered;
+ }
+}
+
+//-- Enable binding events to Manager
+EventEmitter(DefaultViewManager.prototype);
+
+export default DefaultViewManager;
diff --git a/lib/epub.js/src/managers/helpers/snap.js b/lib/epub.js/src/managers/helpers/snap.js
new file mode 100644
index 0000000..db0aaff
--- /dev/null
+++ b/lib/epub.js/src/managers/helpers/snap.js
@@ -0,0 +1,338 @@
+import {extend, defer, requestAnimationFrame, prefixed} from "../../utils/core";
+import { EVENTS, DOM_EVENTS } from "../../utils/constants";
+import EventEmitter from "event-emitter";
+
+// easing equations from https://github.com/danro/easing-js/blob/master/easing.js
+const PI_D2 = (Math.PI / 2);
+const EASING_EQUATIONS = {
+ easeOutSine: function (pos) {
+ return Math.sin(pos * PI_D2);
+ },
+ easeInOutSine: function (pos) {
+ return (-0.5 * (Math.cos(Math.PI * pos) - 1));
+ },
+ easeInOutQuint: function (pos) {
+ if ((pos /= 0.5) < 1) {
+ return 0.5 * Math.pow(pos, 5);
+ }
+ return 0.5 * (Math.pow((pos - 2), 5) + 2);
+ },
+ easeInCubic: function(pos) {
+ return Math.pow(pos, 3);
+ }
+};
+
+class Snap {
+ constructor(manager, options) {
+
+ this.settings = extend({
+ duration: 80,
+ minVelocity: 0.2,
+ minDistance: 10,
+ easing: EASING_EQUATIONS['easeInCubic']
+ }, options || {});
+
+ this.supportsTouch = this.supportsTouch();
+
+ if (this.supportsTouch) {
+ this.setup(manager);
+ }
+ }
+
+ setup(manager) {
+ this.manager = manager;
+
+ this.layout = this.manager.layout;
+
+ this.fullsize = this.manager.settings.fullsize;
+ if (this.fullsize) {
+ this.element = this.manager.stage.element;
+ this.scroller = window;
+ this.disableScroll();
+ } else {
+ this.element = this.manager.stage.container;
+ this.scroller = this.element;
+ this.element.style["WebkitOverflowScrolling"] = "touch";
+ }
+
+ // this.overflow = this.manager.overflow;
+
+ // set lookahead offset to page width
+ this.manager.settings.offset = this.layout.width;
+ this.manager.settings.afterScrolledTimeout = this.settings.duration * 2;
+
+ this.isVertical = this.manager.settings.axis === "vertical";
+
+ // disable snapping if not paginated or axis in not horizontal
+ if (!this.manager.isPaginated || this.isVertical) {
+ return;
+ }
+
+ this.touchCanceler = false;
+ this.resizeCanceler = false;
+ this.snapping = false;
+
+
+ this.scrollLeft;
+ this.scrollTop;
+
+ this.startTouchX = undefined;
+ this.startTouchY = undefined;
+ this.startTime = undefined;
+ this.endTouchX = undefined;
+ this.endTouchY = undefined;
+ this.endTime = undefined;
+
+ this.addListeners();
+ }
+
+ supportsTouch() {
+ if (('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch) {
+ return true;
+ }
+
+ return false;
+ }
+
+ disableScroll() {
+ this.element.style.overflow = "hidden";
+ }
+
+ enableScroll() {
+ this.element.style.overflow = "";
+ }
+
+ addListeners() {
+ this._onResize = this.onResize.bind(this);
+ window.addEventListener('resize', this._onResize);
+
+ this._onScroll = this.onScroll.bind(this);
+ this.scroller.addEventListener('scroll', this._onScroll);
+
+ this._onTouchStart = this.onTouchStart.bind(this);
+ this.scroller.addEventListener('touchstart', this._onTouchStart, { passive: true });
+ this.on('touchstart', this._onTouchStart);
+
+ this._onTouchMove = this.onTouchMove.bind(this);
+ this.scroller.addEventListener('touchmove', this._onTouchMove, { passive: true });
+ this.on('touchmove', this._onTouchMove);
+
+ this._onTouchEnd = this.onTouchEnd.bind(this);
+ this.scroller.addEventListener('touchend', this._onTouchEnd, { passive: true });
+ this.on('touchend', this._onTouchEnd);
+
+ this._afterDisplayed = this.afterDisplayed.bind(this);
+ this.manager.on(EVENTS.MANAGERS.ADDED, this._afterDisplayed);
+ }
+
+ removeListeners() {
+ window.removeEventListener('resize', this._onResize);
+ this._onResize = undefined;
+
+ this.scroller.removeEventListener('scroll', this._onScroll);
+ this._onScroll = undefined;
+
+ this.scroller.removeEventListener('touchstart', this._onTouchStart, { passive: true });
+ this.off('touchstart', this._onTouchStart);
+ this._onTouchStart = undefined;
+
+ this.scroller.removeEventListener('touchmove', this._onTouchMove, { passive: true });
+ this.off('touchmove', this._onTouchMove);
+ this._onTouchMove = undefined;
+
+ this.scroller.removeEventListener('touchend', this._onTouchEnd, { passive: true });
+ this.off('touchend', this._onTouchEnd);
+ this._onTouchEnd = undefined;
+
+ this.manager.off(EVENTS.MANAGERS.ADDED, this._afterDisplayed);
+ this._afterDisplayed = undefined;
+ }
+
+ afterDisplayed(view) {
+ let contents = view.contents;
+ ["touchstart", "touchmove", "touchend"].forEach((e) => {
+ contents.on(e, (ev) => this.triggerViewEvent(ev, contents));
+ });
+ }
+
+ triggerViewEvent(e, contents){
+ this.emit(e.type, e, contents);
+ }
+
+ onScroll(e) {
+ this.scrollLeft = this.fullsize ? window.scrollX : this.scroller.scrollLeft;
+ this.scrollTop = this.fullsize ? window.scrollY : this.scroller.scrollTop;
+ }
+
+ onResize(e) {
+ this.resizeCanceler = true;
+ }
+
+ onTouchStart(e) {
+ let { screenX, screenY } = e.touches[0];
+
+ if (this.fullsize) {
+ this.enableScroll();
+ }
+
+ this.touchCanceler = true;
+
+ if (!this.startTouchX) {
+ this.startTouchX = screenX;
+ this.startTouchY = screenY;
+ this.startTime = this.now();
+ }
+
+ this.endTouchX = screenX;
+ this.endTouchY = screenY;
+ this.endTime = this.now();
+ }
+
+ onTouchMove(e) {
+ let { screenX, screenY } = e.touches[0];
+ let deltaY = Math.abs(screenY - this.endTouchY);
+
+ this.touchCanceler = true;
+
+
+ if (!this.fullsize && deltaY < 10) {
+ this.element.scrollLeft -= screenX - this.endTouchX;
+ }
+
+ this.endTouchX = screenX;
+ this.endTouchY = screenY;
+ this.endTime = this.now();
+ }
+
+ onTouchEnd(e) {
+ if (this.fullsize) {
+ this.disableScroll();
+ }
+
+ this.touchCanceler = false;
+
+ let swipped = this.wasSwiped();
+
+ if (swipped !== 0) {
+ this.snap(swipped);
+ } else {
+ this.snap();
+ }
+
+ this.startTouchX = undefined;
+ this.startTouchY = undefined;
+ this.startTime = undefined;
+ this.endTouchX = undefined;
+ this.endTouchY = undefined;
+ this.endTime = undefined;
+ }
+
+ wasSwiped() {
+ let snapWidth = this.layout.pageWidth * this.layout.divisor;
+ let distance = (this.endTouchX - this.startTouchX);
+ let absolute = Math.abs(distance);
+ let time = this.endTime - this.startTime;
+ let velocity = (distance / time);
+ let minVelocity = this.settings.minVelocity;
+
+ if (absolute <= this.settings.minDistance || absolute >= snapWidth) {
+ return 0;
+ }
+
+ if (velocity > minVelocity) {
+ // previous
+ return -1;
+ } else if (velocity < -minVelocity) {
+ // next
+ return 1;
+ }
+ }
+
+ needsSnap() {
+ let left = this.scrollLeft;
+ let snapWidth = this.layout.pageWidth * this.layout.divisor;
+ return (left % snapWidth) !== 0;
+ }
+
+ snap(howMany=0) {
+ let left = this.scrollLeft;
+ let snapWidth = this.layout.pageWidth * this.layout.divisor;
+ let snapTo = Math.round(left / snapWidth) * snapWidth;
+
+ if (howMany) {
+ snapTo += (howMany * snapWidth);
+ }
+
+ return this.smoothScrollTo(snapTo);
+ }
+
+ smoothScrollTo(destination) {
+ const deferred = new defer();
+ const start = this.scrollLeft;
+ const startTime = this.now();
+
+ const duration = this.settings.duration;
+ const easing = this.settings.easing;
+
+ this.snapping = true;
+
+ // add animation loop
+ function tick() {
+ const now = this.now();
+ const time = Math.min(1, ((now - startTime) / duration));
+ const timeFunction = easing(time);
+
+
+ if (this.touchCanceler || this.resizeCanceler) {
+ this.resizeCanceler = false;
+ this.snapping = false;
+ deferred.resolve();
+ return;
+ }
+
+ if (time < 1) {
+ window.requestAnimationFrame(tick.bind(this));
+ this.scrollTo(start + ((destination - start) * time), 0);
+ } else {
+ this.scrollTo(destination, 0);
+ this.snapping = false;
+ deferred.resolve();
+ }
+ }
+
+ tick.call(this);
+
+ return deferred.promise;
+ }
+
+ scrollTo(left=0, top=0) {
+ if (this.fullsize) {
+ window.scroll(left, top);
+ } else {
+ this.scroller.scrollLeft = left;
+ this.scroller.scrollTop = top;
+ }
+ }
+
+ now() {
+ return ('now' in window.performance) ? performance.now() : new Date().getTime();
+ }
+
+ destroy() {
+ if (!this.scroller) {
+ return;
+ }
+
+ if (this.fullsize) {
+ this.enableScroll();
+ }
+
+ this.removeListeners();
+
+ this.scroller = undefined;
+ }
+}
+
+EventEmitter(Snap.prototype);
+
+export default Snap;
diff --git a/lib/epub.js/src/managers/helpers/stage.js b/lib/epub.js/src/managers/helpers/stage.js
new file mode 100644
index 0000000..d0f67e6
--- /dev/null
+++ b/lib/epub.js/src/managers/helpers/stage.js
@@ -0,0 +1,363 @@
+import {uuid, isNumber, isElement, windowBounds, extend} from "../../utils/core";
+import throttle from 'lodash/throttle'
+
+class Stage {
+ constructor(_options) {
+ this.settings = _options || {};
+ this.id = "epubjs-container-" + uuid();
+
+ this.container = this.create(this.settings);
+
+ if(this.settings.hidden) {
+ this.wrapper = this.wrap(this.container);
+ }
+
+ }
+
+ /*
+ * Creates an element to render to.
+ * Resizes to passed width and height or to the elements size
+ */
+ create(options){
+ let height = options.height;// !== false ? options.height : "100%";
+ let width = options.width;// !== false ? options.width : "100%";
+ let overflow = options.overflow || false;
+ let axis = options.axis || "vertical";
+ let direction = options.direction;
+
+ extend(this.settings, options);
+
+ if(options.height && isNumber(options.height)) {
+ height = options.height + "px";
+ }
+
+ if(options.width && isNumber(options.width)) {
+ width = options.width + "px";
+ }
+
+ // Create new container element
+ let container = document.createElement("div");
+
+ container.id = this.id;
+ container.classList.add("epub-container");
+
+ // Style Element
+ // container.style.fontSize = "0";
+ container.style.wordSpacing = "0";
+ container.style.lineHeight = "0";
+ container.style.verticalAlign = "top";
+ container.style.position = "relative";
+
+ if(axis === "horizontal") {
+ // container.style.whiteSpace = "nowrap";
+ container.style.display = "flex";
+ container.style.flexDirection = "row";
+ container.style.flexWrap = "nowrap";
+ }
+
+ if(width){
+ container.style.width = width;
+ }
+
+ if(height){
+ container.style.height = height;
+ }
+
+ if (overflow) {
+ if (overflow === "scroll" && axis === "vertical") {
+ container.style["overflow-y"] = overflow;
+ container.style["overflow-x"] = "hidden";
+ } else if (overflow === "scroll" && axis === "horizontal") {
+ container.style["overflow-y"] = "hidden";
+ container.style["overflow-x"] = overflow;
+ } else {
+ container.style["overflow"] = overflow;
+ }
+ }
+
+ if (direction) {
+ container.dir = direction;
+ container.style["direction"] = direction;
+ }
+
+ if (direction && this.settings.fullsize) {
+ document.body.style["direction"] = direction;
+ }
+
+ return container;
+ }
+
+ wrap(container) {
+ var wrapper = document.createElement("div");
+
+ wrapper.style.visibility = "hidden";
+ wrapper.style.overflow = "hidden";
+ wrapper.style.width = "0";
+ wrapper.style.height = "0";
+
+ wrapper.appendChild(container);
+ return wrapper;
+ }
+
+
+ getElement(_element){
+ var element;
+
+ if(isElement(_element)) {
+ element = _element;
+ } else if (typeof _element === "string") {
+ element = document.getElementById(_element);
+ }
+
+ if(!element){
+ throw new Error("Not an Element");
+ }
+
+ return element;
+ }
+
+ attachTo(what){
+
+ var element = this.getElement(what);
+ var base;
+
+ if(!element){
+ return;
+ }
+
+ if(this.settings.hidden) {
+ base = this.wrapper;
+ } else {
+ base = this.container;
+ }
+
+ element.appendChild(base);
+
+ this.element = element;
+
+ return element;
+
+ }
+
+ getContainer() {
+ return this.container;
+ }
+
+ onResize(func){
+ // Only listen to window for resize event if width and height are not fixed.
+ // This applies if it is set to a percent or auto.
+ if(!isNumber(this.settings.width) ||
+ !isNumber(this.settings.height) ) {
+ this.resizeFunc = throttle(func, 50);
+ window.addEventListener("resize", this.resizeFunc, false);
+ }
+
+ }
+
+ onOrientationChange(func){
+ this.orientationChangeFunc = func;
+ window.addEventListener("orientationchange", this.orientationChangeFunc, false);
+ }
+
+ size(width, height){
+ var bounds;
+ let _width = width || this.settings.width;
+ let _height = height || this.settings.height;
+
+ // If width or height are set to false, inherit them from containing element
+ if(width === null) {
+ bounds = this.element.getBoundingClientRect();
+
+ if(bounds.width) {
+ width = Math.floor(bounds.width);
+ this.container.style.width = width + "px";
+ }
+ } else {
+ if (isNumber(width)) {
+ this.container.style.width = width + "px";
+ } else {
+ this.container.style.width = width;
+ }
+ }
+
+ if(height === null) {
+ bounds = bounds || this.element.getBoundingClientRect();
+
+ if(bounds.height) {
+ height = bounds.height;
+ this.container.style.height = height + "px";
+ }
+
+ } else {
+ if (isNumber(height)) {
+ this.container.style.height = height + "px";
+ } else {
+ this.container.style.height = height;
+ }
+ }
+
+ if(!isNumber(width)) {
+ width = this.container.clientWidth;
+ }
+
+ if(!isNumber(height)) {
+ height = this.container.clientHeight;
+ }
+
+ this.containerStyles = window.getComputedStyle(this.container);
+
+ this.containerPadding = {
+ left: parseFloat(this.containerStyles["padding-left"]) || 0,
+ right: parseFloat(this.containerStyles["padding-right"]) || 0,
+ top: parseFloat(this.containerStyles["padding-top"]) || 0,
+ bottom: parseFloat(this.containerStyles["padding-bottom"]) || 0
+ };
+
+ // Bounds not set, get them from window
+ let _windowBounds = windowBounds();
+ let bodyStyles = window.getComputedStyle(document.body);
+ let bodyPadding = {
+ left: parseFloat(bodyStyles["padding-left"]) || 0,
+ right: parseFloat(bodyStyles["padding-right"]) || 0,
+ top: parseFloat(bodyStyles["padding-top"]) || 0,
+ bottom: parseFloat(bodyStyles["padding-bottom"]) || 0
+ };
+
+ if (!_width) {
+ width = _windowBounds.width -
+ bodyPadding.left -
+ bodyPadding.right;
+ }
+
+ if ((this.settings.fullsize && !_height) || !_height) {
+ height = _windowBounds.height -
+ bodyPadding.top -
+ bodyPadding.bottom;
+ }
+
+ return {
+ width: width -
+ this.containerPadding.left -
+ this.containerPadding.right,
+ height: height -
+ this.containerPadding.top -
+ this.containerPadding.bottom
+ };
+
+ }
+
+ bounds(){
+ let box;
+ if (this.container.style.overflow !== "visible") {
+ box = this.container && this.container.getBoundingClientRect();
+ }
+
+ if(!box || !box.width || !box.height) {
+ return windowBounds();
+ } else {
+ return box;
+ }
+
+ }
+
+ getSheet(){
+ var style = document.createElement("style");
+
+ // WebKit hack --> https://davidwalsh.name/add-rules-stylesheets
+ style.appendChild(document.createTextNode(""));
+
+ document.head.appendChild(style);
+
+ return style.sheet;
+ }
+
+ addStyleRules(selector, rulesArray){
+ var scope = "#" + this.id + " ";
+ var rules = "";
+
+ if(!this.sheet){
+ this.sheet = this.getSheet();
+ }
+
+ rulesArray.forEach(function(set) {
+ for (var prop in set) {
+ if(set.hasOwnProperty(prop)) {
+ rules += prop + ":" + set[prop] + ";";
+ }
+ }
+ });
+
+ this.sheet.insertRule(scope + selector + " {" + rules + "}", 0);
+ }
+
+ axis(axis) {
+ if(axis === "horizontal") {
+ this.container.style.display = "flex";
+ this.container.style.flexDirection = "row";
+ this.container.style.flexWrap = "nowrap";
+ } else {
+ this.container.style.display = "block";
+ }
+ this.settings.axis = axis;
+ }
+
+ // orientation(orientation) {
+ // if (orientation === "landscape") {
+ //
+ // } else {
+ //
+ // }
+ //
+ // this.orientation = orientation;
+ // }
+
+ direction(dir) {
+ if (this.container) {
+ this.container.dir = dir;
+ this.container.style["direction"] = dir;
+ }
+
+ if (this.settings.fullsize) {
+ document.body.style["direction"] = dir;
+ }
+ this.settings.dir = dir;
+ }
+
+ overflow(overflow) {
+ if (this.container) {
+ if (overflow === "scroll" && this.settings.axis === "vertical") {
+ this.container.style["overflow-y"] = overflow;
+ this.container.style["overflow-x"] = "hidden";
+ } else if (overflow === "scroll" && this.settings.axis === "horizontal") {
+ this.container.style["overflow-y"] = "hidden";
+ this.container.style["overflow-x"] = overflow;
+ } else {
+ this.container.style["overflow"] = overflow;
+ }
+ }
+ this.settings.overflow = overflow;
+ }
+
+ destroy() {
+ var base;
+
+ if (this.element) {
+
+ if(this.settings.hidden) {
+ base = this.wrapper;
+ } else {
+ base = this.container;
+ }
+
+ if(this.element.contains(this.container)) {
+ this.element.removeChild(this.container);
+ }
+
+ window.removeEventListener("resize", this.resizeFunc);
+ window.removeEventListener("orientationChange", this.orientationChangeFunc);
+
+ }
+ }
+}
+
+export default Stage;
diff --git a/lib/epub.js/src/managers/helpers/views.js b/lib/epub.js/src/managers/helpers/views.js
new file mode 100644
index 0000000..4368da2
--- /dev/null
+++ b/lib/epub.js/src/managers/helpers/views.js
@@ -0,0 +1,167 @@
+class Views {
+ constructor(container) {
+ this.container = container;
+ this._views = [];
+ this.length = 0;
+ this.hidden = false;
+ }
+
+ all() {
+ return this._views;
+ }
+
+ first() {
+ return this._views[0];
+ }
+
+ last() {
+ return this._views[this._views.length-1];
+ }
+
+ indexOf(view) {
+ return this._views.indexOf(view);
+ }
+
+ slice() {
+ return this._views.slice.apply(this._views, arguments);
+ }
+
+ get(i) {
+ return this._views[i];
+ }
+
+ append(view){
+ this._views.push(view);
+ if(this.container){
+ this.container.appendChild(view.element);
+ }
+ this.length++;
+ return view;
+ }
+
+ prepend(view){
+ this._views.unshift(view);
+ if(this.container){
+ this.container.insertBefore(view.element, this.container.firstChild);
+ }
+ this.length++;
+ return view;
+ }
+
+ insert(view, index) {
+ this._views.splice(index, 0, view);
+
+ if(this.container){
+ if(index < this.container.children.length){
+ this.container.insertBefore(view.element, this.container.children[index]);
+ } else {
+ this.container.appendChild(view.element);
+ }
+ }
+
+ this.length++;
+ return view;
+ }
+
+ remove(view) {
+ var index = this._views.indexOf(view);
+
+ if(index > -1) {
+ this._views.splice(index, 1);
+ }
+
+
+ this.destroy(view);
+
+ this.length--;
+ }
+
+ destroy(view) {
+ if(view.displayed){
+ view.destroy();
+ }
+
+ if(this.container){
+ this.container.removeChild(view.element);
+ }
+ view = null;
+ }
+
+ // Iterators
+
+ forEach() {
+ return this._views.forEach.apply(this._views, arguments);
+ }
+
+ clear(){
+ // Remove all views
+ var view;
+ var len = this.length;
+
+ if(!this.length) return;
+
+ for (var i = 0; i < len; i++) {
+ view = this._views[i];
+ this.destroy(view);
+ }
+
+ this._views = [];
+ this.length = 0;
+ }
+
+ find(section){
+
+ var view;
+ var len = this.length;
+
+ for (var i = 0; i < len; i++) {
+ view = this._views[i];
+ if(view.displayed && view.section.index == section.index) {
+ return view;
+ }
+ }
+
+ }
+
+ displayed(){
+ var displayed = [];
+ var view;
+ var len = this.length;
+
+ for (var i = 0; i < len; i++) {
+ view = this._views[i];
+ if(view.displayed){
+ displayed.push(view);
+ }
+ }
+ return displayed;
+ }
+
+ show(){
+ var view;
+ var len = this.length;
+
+ for (var i = 0; i < len; i++) {
+ view = this._views[i];
+ if(view.displayed){
+ view.show();
+ }
+ }
+ this.hidden = false;
+ }
+
+ hide(){
+ var view;
+ var len = this.length;
+
+ for (var i = 0; i < len; i++) {
+ view = this._views[i];
+ if(view.displayed){
+ view.hide();
+ }
+ }
+ this.hidden = true;
+ }
+}
+
+export default Views;
diff --git a/lib/epub.js/src/managers/views/iframe.js b/lib/epub.js/src/managers/views/iframe.js
new file mode 100644
index 0000000..76b2c1d
--- /dev/null
+++ b/lib/epub.js/src/managers/views/iframe.js
@@ -0,0 +1,835 @@
+import EventEmitter from "event-emitter";
+import {extend, borders, uuid, isNumber, bounds, defer, createBlobUrl, revokeBlobUrl} from "../../utils/core";
+import EpubCFI from "../../epubcfi";
+import Contents from "../../contents";
+import { EVENTS } from "../../utils/constants";
+import { Pane, Highlight, Underline } from "marks-pane";
+
+class IframeView {
+ constructor(section, options) {
+ this.settings = extend({
+ ignoreClass : "",
+ axis: undefined, //options.layout && options.layout.props.flow === "scrolled" ? "vertical" : "horizontal",
+ direction: undefined,
+ width: 0,
+ height: 0,
+ layout: undefined,
+ globalLayoutProperties: {},
+ method: undefined,
+ forceRight: false
+ }, options || {});
+
+ this.id = "epubjs-view-" + uuid();
+ this.section = section;
+ this.index = section.index;
+
+ this.element = this.container(this.settings.axis);
+
+ this.added = false;
+ this.displayed = false;
+ this.rendered = false;
+
+ // this.width = this.settings.width;
+ // this.height = this.settings.height;
+
+ this.fixedWidth = 0;
+ this.fixedHeight = 0;
+
+ // Blank Cfi for Parsing
+ this.epubcfi = new EpubCFI();
+
+ this.layout = this.settings.layout;
+ // Dom events to listen for
+ // this.listenedEvents = ["keydown", "keyup", "keypressed", "mouseup", "mousedown", "click", "touchend", "touchstart"];
+
+ this.pane = undefined;
+ this.highlights = {};
+ this.underlines = {};
+ this.marks = {};
+
+ }
+
+ container(axis) {
+ var element = document.createElement("div");
+
+ element.classList.add("epub-view");
+
+ // this.element.style.minHeight = "100px";
+ element.style.height = "0px";
+ element.style.width = "0px";
+ element.style.overflow = "hidden";
+ element.style.position = "relative";
+ element.style.display = "block";
+
+ if(axis && axis == "horizontal"){
+ element.style.flex = "none";
+ } else {
+ element.style.flex = "initial";
+ }
+
+ return element;
+ }
+
+ create() {
+
+ if(this.iframe) {
+ return this.iframe;
+ }
+
+ if(!this.element) {
+ this.element = this.createContainer();
+ }
+
+ this.iframe = document.createElement("iframe");
+ this.iframe.id = this.id;
+ this.iframe.scrolling = "no"; // Might need to be removed: breaks ios width calculations
+ this.iframe.style.overflow = "hidden";
+ this.iframe.seamless = "seamless";
+ // Back up if seamless isn't supported
+ this.iframe.style.border = "none";
+
+ this.iframe.setAttribute("enable-annotation", "true");
+
+ this.resizing = true;
+
+ // this.iframe.style.display = "none";
+ this.element.style.visibility = "hidden";
+ this.iframe.style.visibility = "hidden";
+
+ this.iframe.style.width = "0";
+ this.iframe.style.height = "0";
+ this._width = 0;
+ this._height = 0;
+
+ this.element.setAttribute("ref", this.index);
+
+ this.added = true;
+
+ this.elementBounds = bounds(this.element);
+
+ // if(width || height){
+ // this.resize(width, height);
+ // } else if(this.width && this.height){
+ // this.resize(this.width, this.height);
+ // } else {
+ // this.iframeBounds = bounds(this.iframe);
+ // }
+
+
+ if(("srcdoc" in this.iframe)) {
+ this.supportsSrcdoc = true;
+ } else {
+ this.supportsSrcdoc = false;
+ }
+
+ if (!this.settings.method) {
+ this.settings.method = this.supportsSrcdoc ? "srcdoc" : "write";
+ }
+
+ return this.iframe;
+ }
+
+ render(request, show) {
+
+ // view.onLayout = this.layout.format.bind(this.layout);
+ this.create();
+
+ // Fit to size of the container, apply padding
+ this.size();
+
+ if(!this.sectionRender) {
+ this.sectionRender = this.section.render(request);
+ }
+
+ // Render Chain
+ return this.sectionRender
+ .then(function(contents){
+ return this.load(contents);
+ }.bind(this))
+ .then(function(){
+
+ // find and report the writingMode axis
+ let writingMode = this.contents.writingMode();
+
+ // Set the axis based on the flow and writing mode
+ let axis;
+ if (this.settings.flow === "scrolled") {
+ axis = (writingMode.indexOf("vertical") === 0) ? "horizontal" : "vertical";
+ } else {
+ axis = (writingMode.indexOf("vertical") === 0) ? "vertical" : "horizontal";
+ }
+
+ if (writingMode.indexOf("vertical") === 0 && this.settings.flow === "paginated") {
+ this.layout.delta = this.layout.height;
+ }
+
+ this.setAxis(axis);
+ this.emit(EVENTS.VIEWS.AXIS, axis);
+
+ this.setWritingMode(writingMode);
+ this.emit(EVENTS.VIEWS.WRITING_MODE, writingMode);
+
+
+ // apply the layout function to the contents
+ this.layout.format(this.contents, this.section, this.axis);
+
+ // Listen for events that require an expansion of the iframe
+ this.addListeners();
+
+ return new Promise((resolve, reject) => {
+ // Expand the iframe to the full size of the content
+ this.expand();
+
+ if (this.settings.forceRight) {
+ this.element.style.marginLeft = this.width() + "px";
+ }
+ resolve();
+ });
+
+ }.bind(this), function(e){
+ this.emit(EVENTS.VIEWS.LOAD_ERROR, e);
+ return new Promise((resolve, reject) => {
+ reject(e);
+ });
+ }.bind(this))
+ .then(function() {
+ this.emit(EVENTS.VIEWS.RENDERED, this.section);
+ }.bind(this));
+
+ }
+
+ reset () {
+ if (this.iframe) {
+ this.iframe.style.width = "0";
+ this.iframe.style.height = "0";
+ this._width = 0;
+ this._height = 0;
+ this._textWidth = undefined;
+ this._contentWidth = undefined;
+ this._textHeight = undefined;
+ this._contentHeight = undefined;
+ }
+ this._needsReframe = true;
+ }
+
+ // Determine locks base on settings
+ size(_width, _height) {
+ var width = _width || this.settings.width;
+ var height = _height || this.settings.height;
+
+ if(this.layout.name === "pre-paginated") {
+ this.lock("both", width, height);
+ } else if(this.settings.axis === "horizontal") {
+ this.lock("height", width, height);
+ } else {
+ this.lock("width", width, height);
+ }
+
+ this.settings.width = width;
+ this.settings.height = height;
+ }
+
+ // Lock an axis to element dimensions, taking borders into account
+ lock(what, width, height) {
+ var elBorders = borders(this.element);
+ var iframeBorders;
+
+ if(this.iframe) {
+ iframeBorders = borders(this.iframe);
+ } else {
+ iframeBorders = {width: 0, height: 0};
+ }
+
+ if(what == "width" && isNumber(width)){
+ this.lockedWidth = width - elBorders.width - iframeBorders.width;
+ // this.resize(this.lockedWidth, width); // width keeps ratio correct
+ }
+
+ if(what == "height" && isNumber(height)){
+ this.lockedHeight = height - elBorders.height - iframeBorders.height;
+ // this.resize(width, this.lockedHeight);
+ }
+
+ if(what === "both" &&
+ isNumber(width) &&
+ isNumber(height)){
+
+ this.lockedWidth = width - elBorders.width - iframeBorders.width;
+ this.lockedHeight = height - elBorders.height - iframeBorders.height;
+ // this.resize(this.lockedWidth, this.lockedHeight);
+ }
+
+ if(this.displayed && this.iframe) {
+
+ // this.contents.layout();
+ this.expand();
+ }
+
+
+
+ }
+
+ // Resize a single axis based on content dimensions
+ expand(force) {
+ var width = this.lockedWidth;
+ var height = this.lockedHeight;
+ var columns;
+
+ var textWidth, textHeight;
+
+ if(!this.iframe || this._expanding) return;
+
+ this._expanding = true;
+
+ if(this.layout.name === "pre-paginated") {
+ width = this.layout.columnWidth;
+ height = this.layout.height;
+ }
+ // Expand Horizontally
+ else if(this.settings.axis === "horizontal") {
+ // Get the width of the text
+ width = this.contents.textWidth();
+
+ if (width % this.layout.pageWidth > 0) {
+ width = Math.ceil(width / this.layout.pageWidth) * this.layout.pageWidth;
+ }
+
+ if (this.settings.forceEvenPages) {
+ columns = (width / this.layout.pageWidth);
+ if ( this.layout.divisor > 1 &&
+ this.layout.name === "reflowable" &&
+ (columns % 2 > 0)) {
+ // add a blank page
+ width += this.layout.pageWidth;
+ }
+ }
+
+ } // Expand Vertically
+ else if(this.settings.axis === "vertical") {
+ height = this.contents.textHeight();
+ if (this.settings.flow === "paginated" &&
+ height % this.layout.height > 0) {
+ height = Math.ceil(height / this.layout.height) * this.layout.height;
+ }
+ }
+
+ // Only Resize if dimensions have changed or
+ // if Frame is still hidden, so needs reframing
+ if(this._needsReframe || width != this._width || height != this._height){
+ this.reframe(width, height);
+ }
+
+ this._expanding = false;
+ }
+
+ reframe(width, height) {
+ var size;
+
+ if(isNumber(width)){
+ this.element.style.width = width + "px";
+ this.iframe.style.width = width + "px";
+ this._width = width;
+ }
+
+ if(isNumber(height)){
+ this.element.style.height = height + "px";
+ this.iframe.style.height = height + "px";
+ this._height = height;
+ }
+
+ let widthDelta = this.prevBounds ? width - this.prevBounds.width : width;
+ let heightDelta = this.prevBounds ? height - this.prevBounds.height : height;
+
+ size = {
+ width: width,
+ height: height,
+ widthDelta: widthDelta,
+ heightDelta: heightDelta,
+ };
+
+ this.pane && this.pane.render();
+
+ requestAnimationFrame(() => {
+ let mark;
+ for (let m in this.marks) {
+ if (this.marks.hasOwnProperty(m)) {
+ mark = this.marks[m];
+ this.placeMark(mark.element, mark.range);
+ }
+ }
+ });
+
+ this.onResize(this, size);
+
+ this.emit(EVENTS.VIEWS.RESIZED, size);
+
+ this.prevBounds = size;
+
+ this.elementBounds = bounds(this.element);
+
+ }
+
+
+ load(contents) {
+ var loading = new defer();
+ var loaded = loading.promise;
+
+ if(!this.iframe) {
+ loading.reject(new Error("No Iframe Available"));
+ return loaded;
+ }
+
+ this.iframe.onload = function(event) {
+
+ this.onLoad(event, loading);
+
+ }.bind(this);
+
+ if (this.settings.method === "blobUrl") {
+ this.blobUrl = createBlobUrl(contents, "application/xhtml+xml");
+ this.iframe.src = this.blobUrl;
+ this.element.appendChild(this.iframe);
+ } else if(this.settings.method === "srcdoc"){
+ this.iframe.srcdoc = contents;
+ this.element.appendChild(this.iframe);
+ } else {
+
+ this.element.appendChild(this.iframe);
+
+ this.document = this.iframe.contentDocument;
+
+ if(!this.document) {
+ loading.reject(new Error("No Document Available"));
+ return loaded;
+ }
+
+ this.iframe.contentDocument.open();
+ // For Cordova windows platform
+ if(window.MSApp && MSApp.execUnsafeLocalFunction) {
+ var outerThis = this;
+ MSApp.execUnsafeLocalFunction(function () {
+ outerThis.iframe.contentDocument.write(contents);
+ });
+ } else {
+ this.iframe.contentDocument.write(contents);
+ }
+ this.iframe.contentDocument.close();
+
+ }
+
+ return loaded;
+ }
+
+ onLoad(event, promise) {
+
+ this.window = this.iframe.contentWindow;
+ this.document = this.iframe.contentDocument;
+
+ this.contents = new Contents(this.document, this.document.body, this.section.cfiBase, this.section.index);
+
+ this.rendering = false;
+
+ var link = this.document.querySelector("link[rel='canonical']");
+ if (link) {
+ link.setAttribute("href", this.section.canonical);
+ } else {
+ link = this.document.createElement("link");
+ link.setAttribute("rel", "canonical");
+ link.setAttribute("href", this.section.canonical);
+ this.document.querySelector("head").appendChild(link);
+ }
+
+ this.contents.on(EVENTS.CONTENTS.EXPAND, () => {
+ if(this.displayed && this.iframe) {
+ this.expand();
+ if (this.contents) {
+ this.layout.format(this.contents);
+ }
+ }
+ });
+
+ this.contents.on(EVENTS.CONTENTS.RESIZE, (e) => {
+ if(this.displayed && this.iframe) {
+ this.expand();
+ if (this.contents) {
+ this.layout.format(this.contents);
+ }
+ }
+ });
+
+ promise.resolve(this.contents);
+ }
+
+ setLayout(layout) {
+ this.layout = layout;
+
+ if (this.contents) {
+ this.layout.format(this.contents);
+ this.expand();
+ }
+ }
+
+ setAxis(axis) {
+
+ this.settings.axis = axis;
+
+ if(axis == "horizontal"){
+ this.element.style.flex = "none";
+ } else {
+ this.element.style.flex = "initial";
+ }
+
+ this.size();
+
+ }
+
+ setWritingMode(mode) {
+ // this.element.style.writingMode = writingMode;
+ this.writingMode = mode;
+ }
+
+ addListeners() {
+ //TODO: Add content listeners for expanding
+ }
+
+ removeListeners(layoutFunc) {
+ //TODO: remove content listeners for expanding
+ }
+
+ display(request) {
+ var displayed = new defer();
+
+ if (!this.displayed) {
+
+ this.render(request)
+ .then(function () {
+
+ this.emit(EVENTS.VIEWS.DISPLAYED, this);
+ this.onDisplayed(this);
+
+ this.displayed = true;
+ displayed.resolve(this);
+
+ }.bind(this), function (err) {
+ displayed.reject(err, this);
+ });
+
+ } else {
+ displayed.resolve(this);
+ }
+
+
+ return displayed.promise;
+ }
+
+ show() {
+
+ this.element.style.visibility = "visible";
+
+ if(this.iframe){
+ this.iframe.style.visibility = "visible";
+
+ // Remind Safari to redraw the iframe
+ this.iframe.style.transform = "translateZ(0)";
+ this.iframe.offsetWidth;
+ this.iframe.style.transform = null;
+ }
+
+ this.emit(EVENTS.VIEWS.SHOWN, this);
+ }
+
+ hide() {
+ // this.iframe.style.display = "none";
+ this.element.style.visibility = "hidden";
+ this.iframe.style.visibility = "hidden";
+
+ this.stopExpanding = true;
+ this.emit(EVENTS.VIEWS.HIDDEN, this);
+ }
+
+ offset() {
+ return {
+ top: this.element.offsetTop,
+ left: this.element.offsetLeft
+ }
+ }
+
+ width() {
+ return this._width;
+ }
+
+ height() {
+ return this._height;
+ }
+
+ position() {
+ return this.element.getBoundingClientRect();
+ }
+
+ locationOf(target) {
+ var parentPos = this.iframe.getBoundingClientRect();
+ var targetPos = this.contents.locationOf(target, this.settings.ignoreClass);
+
+ return {
+ "left": targetPos.left,
+ "top": targetPos.top
+ };
+ }
+
+ onDisplayed(view) {
+ // Stub, override with a custom functions
+ }
+
+ onResize(view, e) {
+ // Stub, override with a custom functions
+ }
+
+ bounds(force) {
+ if(force || !this.elementBounds) {
+ this.elementBounds = bounds(this.element);
+ }
+
+ return this.elementBounds;
+ }
+
+ highlight(cfiRange, data={}, cb, className = "epubjs-hl", styles = {}) {
+ if (!this.contents) {
+ return;
+ }
+ const attributes = Object.assign({"fill": "yellow", "fill-opacity": "0.3", "mix-blend-mode": "multiply"}, styles);
+ let range = this.contents.range(cfiRange);
+
+ let emitter = () => {
+ this.emit(EVENTS.VIEWS.MARK_CLICKED, cfiRange, data);
+ };
+
+ data["epubcfi"] = cfiRange;
+
+ if (!this.pane) {
+ this.pane = new Pane(this.iframe, this.element);
+ }
+
+ let m = new Highlight(range, className, data, attributes);
+ let h = this.pane.addMark(m);
+
+ this.highlights[cfiRange] = { "mark": h, "element": h.element, "listeners": [emitter, cb] };
+
+ h.element.setAttribute("ref", className);
+ h.element.addEventListener("click", emitter);
+ h.element.addEventListener("touchstart", emitter);
+
+ if (cb) {
+ h.element.addEventListener("click", cb);
+ h.element.addEventListener("touchstart", cb);
+ }
+ return h;
+ }
+
+ underline(cfiRange, data={}, cb, className = "epubjs-ul", styles = {}) {
+ if (!this.contents) {
+ return;
+ }
+ const attributes = Object.assign({"stroke": "black", "stroke-opacity": "0.3", "mix-blend-mode": "multiply"}, styles);
+ let range = this.contents.range(cfiRange);
+ let emitter = () => {
+ this.emit(EVENTS.VIEWS.MARK_CLICKED, cfiRange, data);
+ };
+
+ data["epubcfi"] = cfiRange;
+
+ if (!this.pane) {
+ this.pane = new Pane(this.iframe, this.element);
+ }
+
+ let m = new Underline(range, className, data, attributes);
+ let h = this.pane.addMark(m);
+
+ this.underlines[cfiRange] = { "mark": h, "element": h.element, "listeners": [emitter, cb] };
+
+ h.element.setAttribute("ref", className);
+ h.element.addEventListener("click", emitter);
+ h.element.addEventListener("touchstart", emitter);
+
+ if (cb) {
+ h.element.addEventListener("click", cb);
+ h.element.addEventListener("touchstart", cb);
+ }
+ return h;
+ }
+
+ mark(cfiRange, data={}, cb) {
+ if (!this.contents) {
+ return;
+ }
+
+ if (cfiRange in this.marks) {
+ let item = this.marks[cfiRange];
+ return item;
+ }
+
+ let range = this.contents.range(cfiRange);
+ if (!range) {
+ return;
+ }
+ let container = range.commonAncestorContainer;
+ let parent = (container.nodeType === 1) ? container : container.parentNode;
+
+ let emitter = (e) => {
+ this.emit(EVENTS.VIEWS.MARK_CLICKED, cfiRange, data);
+ };
+
+ if (range.collapsed && container.nodeType === 1) {
+ range = new Range();
+ range.selectNodeContents(container);
+ } else if (range.collapsed) { // Webkit doesn't like collapsed ranges
+ range = new Range();
+ range.selectNodeContents(parent);
+ }
+
+ let mark = this.document.createElement("a");
+ mark.setAttribute("ref", "epubjs-mk");
+ mark.style.position = "absolute";
+
+ mark.dataset["epubcfi"] = cfiRange;
+
+ if (data) {
+ Object.keys(data).forEach((key) => {
+ mark.dataset[key] = data[key];
+ });
+ }
+
+ if (cb) {
+ mark.addEventListener("click", cb);
+ mark.addEventListener("touchstart", cb);
+ }
+
+ mark.addEventListener("click", emitter);
+ mark.addEventListener("touchstart", emitter);
+
+ this.placeMark(mark, range);
+
+ this.element.appendChild(mark);
+
+ this.marks[cfiRange] = { "element": mark, "range": range, "listeners": [emitter, cb] };
+
+ return parent;
+ }
+
+ placeMark(element, range) {
+ let top, right, left;
+
+ if(this.layout.name === "pre-paginated" ||
+ this.settings.axis !== "horizontal") {
+ let pos = range.getBoundingClientRect();
+ top = pos.top;
+ right = pos.right;
+ } else {
+ // Element might break columns, so find the left most element
+ let rects = range.getClientRects();
+
+ let rect;
+ for (var i = 0; i != rects.length; i++) {
+ rect = rects[i];
+ if (!left || rect.left < left) {
+ left = rect.left;
+ // right = rect.right;
+ right = Math.ceil(left / this.layout.props.pageWidth) * this.layout.props.pageWidth - (this.layout.gap / 2);
+ top = rect.top;
+ }
+ }
+ }
+
+ element.style.top = `${top}px`;
+ element.style.left = `${right}px`;
+ }
+
+ unhighlight(cfiRange) {
+ let item;
+ if (cfiRange in this.highlights) {
+ item = this.highlights[cfiRange];
+
+ this.pane.removeMark(item.mark);
+ item.listeners.forEach((l) => {
+ if (l) {
+ item.element.removeEventListener("click", l);
+ item.element.removeEventListener("touchstart", l);
+ };
+ });
+ delete this.highlights[cfiRange];
+ }
+ }
+
+ ununderline(cfiRange) {
+ let item;
+ if (cfiRange in this.underlines) {
+ item = this.underlines[cfiRange];
+ this.pane.removeMark(item.mark);
+ item.listeners.forEach((l) => {
+ if (l) {
+ item.element.removeEventListener("click", l);
+ item.element.removeEventListener("touchstart", l);
+ };
+ });
+ delete this.underlines[cfiRange];
+ }
+ }
+
+ unmark(cfiRange) {
+ let item;
+ if (cfiRange in this.marks) {
+ item = this.marks[cfiRange];
+ this.element.removeChild(item.element);
+ item.listeners.forEach((l) => {
+ if (l) {
+ item.element.removeEventListener("click", l);
+ item.element.removeEventListener("touchstart", l);
+ };
+ });
+ delete this.marks[cfiRange];
+ }
+ }
+
+ destroy() {
+
+ for (let cfiRange in this.highlights) {
+ this.unhighlight(cfiRange);
+ }
+
+ for (let cfiRange in this.underlines) {
+ this.ununderline(cfiRange);
+ }
+
+ for (let cfiRange in this.marks) {
+ this.unmark(cfiRange);
+ }
+
+ if (this.blobUrl) {
+ revokeBlobUrl(this.blobUrl);
+ }
+
+ if(this.displayed){
+ this.displayed = false;
+
+ this.removeListeners();
+ this.contents.destroy();
+
+ this.stopExpanding = true;
+ this.element.removeChild(this.iframe);
+
+ this.iframe = undefined;
+ this.contents = undefined;
+
+ this._textWidth = null;
+ this._textHeight = null;
+ this._width = null;
+ this._height = null;
+ }
+
+ // this.element.style.height = "0px";
+ // this.element.style.width = "0px";
+ }
+}
+
+EventEmitter(IframeView.prototype);
+
+export default IframeView;
diff --git a/lib/epub.js/src/managers/views/inline.js b/lib/epub.js/src/managers/views/inline.js
new file mode 100644
index 0000000..072b586
--- /dev/null
+++ b/lib/epub.js/src/managers/views/inline.js
@@ -0,0 +1,432 @@
+import EventEmitter from "event-emitter";
+import {extend, borders, uuid, isNumber, bounds, defer, qs, parse} from "../../utils/core";
+import EpubCFI from "../../epubcfi";
+import Contents from "../../contents";
+import { EVENTS } from "../../utils/constants";
+
+class InlineView {
+ constructor(section, options) {
+ this.settings = extend({
+ ignoreClass : "",
+ axis: "vertical",
+ width: 0,
+ height: 0,
+ layout: undefined,
+ globalLayoutProperties: {},
+ }, options || {});
+
+ this.id = "epubjs-view:" + uuid();
+ this.section = section;
+ this.index = section.index;
+
+ this.element = this.container(this.settings.axis);
+
+ this.added = false;
+ this.displayed = false;
+ this.rendered = false;
+
+ this.width = this.settings.width;
+ this.height = this.settings.height;
+
+ this.fixedWidth = 0;
+ this.fixedHeight = 0;
+
+ // Blank Cfi for Parsing
+ this.epubcfi = new EpubCFI();
+
+ this.layout = this.settings.layout;
+ // Dom events to listen for
+ // this.listenedEvents = ["keydown", "keyup", "keypressed", "mouseup", "mousedown", "click", "touchend", "touchstart"];
+
+ }
+
+ container(axis) {
+ var element = document.createElement("div");
+
+ element.classList.add("epub-view");
+
+ // if(this.settings.axis === "horizontal") {
+ // element.style.width = "auto";
+ // element.style.height = "0";
+ // } else {
+ // element.style.width = "0";
+ // element.style.height = "auto";
+ // }
+
+ element.style.overflow = "hidden";
+
+ if(axis && axis == "horizontal"){
+ element.style.display = "inline-block";
+ } else {
+ element.style.display = "block";
+ }
+
+ return element;
+ }
+
+ create() {
+
+ if(this.frame) {
+ return this.frame;
+ }
+
+ if(!this.element) {
+ this.element = this.createContainer();
+ }
+
+ this.frame = document.createElement("div");
+ this.frame.id = this.id;
+ this.frame.style.overflow = "hidden";
+ this.frame.style.wordSpacing = "initial";
+ this.frame.style.lineHeight = "initial";
+
+ this.resizing = true;
+
+ // this.frame.style.display = "none";
+ this.element.style.visibility = "hidden";
+ this.frame.style.visibility = "hidden";
+
+ if(this.settings.axis === "horizontal") {
+ this.frame.style.width = "auto";
+ this.frame.style.height = "0";
+ } else {
+ this.frame.style.width = "0";
+ this.frame.style.height = "auto";
+ }
+
+ this._width = 0;
+ this._height = 0;
+
+ this.element.appendChild(this.frame);
+ this.added = true;
+
+ this.elementBounds = bounds(this.element);
+
+ return this.frame;
+ }
+
+ render(request, show) {
+
+ // view.onLayout = this.layout.format.bind(this.layout);
+ this.create();
+
+ // Fit to size of the container, apply padding
+ this.size();
+
+ // Render Chain
+ return this.section.render(request)
+ .then(function(contents){
+ return this.load(contents);
+ }.bind(this))
+ // .then(function(doc){
+ // return this.hooks.content.trigger(view, this);
+ // }.bind(this))
+ .then(function(){
+ // this.settings.layout.format(view.contents);
+ // return this.hooks.layout.trigger(view, this);
+ }.bind(this))
+ // .then(function(){
+ // return this.display();
+ // }.bind(this))
+ // .then(function(){
+ // return this.hooks.render.trigger(view, this);
+ // }.bind(this))
+ .then(function(){
+
+ // apply the layout function to the contents
+ this.settings.layout.format(this.contents);
+
+ // Expand the iframe to the full size of the content
+ // this.expand();
+
+ // Listen for events that require an expansion of the iframe
+ this.addListeners();
+
+ if(show !== false) {
+ //this.q.enqueue(function(view){
+ this.show();
+ //}, view);
+ }
+ // this.map = new Map(view, this.layout);
+ //this.hooks.show.trigger(view, this);
+ this.emit(EVENTS.VIEWS.RENDERED, this.section);
+
+ }.bind(this))
+ .catch(function(e){
+ this.emit(EVENTS.VIEWS.LOAD_ERROR, e);
+ }.bind(this));
+
+ }
+
+ // Determine locks base on settings
+ size(_width, _height) {
+ var width = _width || this.settings.width;
+ var height = _height || this.settings.height;
+
+ if(this.layout.name === "pre-paginated") {
+ // TODO: check if these are different than the size set in chapter
+ this.lock("both", width, height);
+ } else if(this.settings.axis === "horizontal") {
+ this.lock("height", width, height);
+ } else {
+ this.lock("width", width, height);
+ }
+
+ }
+
+ // Lock an axis to element dimensions, taking borders into account
+ lock(what, width, height) {
+ var elBorders = borders(this.element);
+ var iframeBorders;
+
+ if(this.frame) {
+ iframeBorders = borders(this.frame);
+ } else {
+ iframeBorders = {width: 0, height: 0};
+ }
+
+ if(what == "width" && isNumber(width)){
+ this.lockedWidth = width - elBorders.width - iframeBorders.width;
+ this.resize(this.lockedWidth, false); // width keeps ratio correct
+ }
+
+ if(what == "height" && isNumber(height)){
+ this.lockedHeight = height - elBorders.height - iframeBorders.height;
+ this.resize(false, this.lockedHeight);
+ }
+
+ if(what === "both" &&
+ isNumber(width) &&
+ isNumber(height)){
+
+ this.lockedWidth = width - elBorders.width - iframeBorders.width;
+ this.lockedHeight = height - elBorders.height - iframeBorders.height;
+
+ this.resize(this.lockedWidth, this.lockedHeight);
+ }
+
+ }
+
+ // Resize a single axis based on content dimensions
+ expand(force) {
+ var width = this.lockedWidth;
+ var height = this.lockedHeight;
+
+ var textWidth, textHeight;
+
+ if(!this.frame || this._expanding) return;
+
+ this._expanding = true;
+
+ // Expand Horizontally
+ if(this.settings.axis === "horizontal") {
+ width = this.contentWidth(textWidth);
+ } // Expand Vertically
+ else if(this.settings.axis === "vertical") {
+ height = this.contentHeight(textHeight);
+ }
+
+ // Only Resize if dimensions have changed or
+ // if Frame is still hidden, so needs reframing
+ if(this._needsReframe || width != this._width || height != this._height){
+ this.resize(width, height);
+ }
+
+ this._expanding = false;
+ }
+
+ contentWidth(min) {
+ return this.frame.scrollWidth;
+ }
+
+ contentHeight(min) {
+ return this.frame.scrollHeight;
+ }
+
+
+ resize(width, height) {
+
+ if(!this.frame) return;
+
+ if(isNumber(width)){
+ this.frame.style.width = width + "px";
+ this._width = width;
+ }
+
+ if(isNumber(height)){
+ this.frame.style.height = height + "px";
+ this._height = height;
+ }
+
+ this.prevBounds = this.elementBounds;
+
+ this.elementBounds = bounds(this.element);
+
+ let size = {
+ width: this.elementBounds.width,
+ height: this.elementBounds.height,
+ widthDelta: this.elementBounds.width - this.prevBounds.width,
+ heightDelta: this.elementBounds.height - this.prevBounds.height,
+ };
+
+ this.onResize(this, size);
+
+ this.emit(EVENTS.VIEWS.RESIZED, size);
+
+ }
+
+
+ load(contents) {
+ var loading = new defer();
+ var loaded = loading.promise;
+ var doc = parse(contents, "text/html");
+ var body = qs(doc, "body");
+
+ /*
+ var srcs = doc.querySelectorAll("[src]");
+
+ Array.prototype.slice.call(srcs)
+ .forEach(function(item) {
+ var src = item.getAttribute("src");
+ var assetUri = URI(src);
+ var origin = assetUri.origin();
+ var absoluteUri;
+
+ if (!origin) {
+ absoluteUri = assetUri.absoluteTo(this.section.url);
+ item.src = absoluteUri;
+ }
+ }.bind(this));
+ */
+ this.frame.innerHTML = body.innerHTML;
+
+ this.document = this.frame.ownerDocument;
+ this.window = this.document.defaultView;
+
+ this.contents = new Contents(this.document, this.frame);
+
+ this.rendering = false;
+
+ loading.resolve(this.contents);
+
+
+ return loaded;
+ }
+
+ setLayout(layout) {
+ this.layout = layout;
+ }
+
+
+ resizeListenters() {
+ // Test size again
+ // clearTimeout(this.expanding);
+ // this.expanding = setTimeout(this.expand.bind(this), 350);
+ }
+
+ addListeners() {
+ //TODO: Add content listeners for expanding
+ }
+
+ removeListeners(layoutFunc) {
+ //TODO: remove content listeners for expanding
+ }
+
+ display(request) {
+ var displayed = new defer();
+
+ if (!this.displayed) {
+
+ this.render(request).then(function () {
+
+ this.emit(EVENTS.VIEWS.DISPLAYED, this);
+ this.onDisplayed(this);
+
+ this.displayed = true;
+
+ displayed.resolve(this);
+
+ }.bind(this));
+
+ } else {
+ displayed.resolve(this);
+ }
+
+
+ return displayed.promise;
+ }
+
+ show() {
+
+ this.element.style.visibility = "visible";
+
+ if(this.frame){
+ this.frame.style.visibility = "visible";
+ }
+
+ this.emit(EVENTS.VIEWS.SHOWN, this);
+ }
+
+ hide() {
+ // this.frame.style.display = "none";
+ this.element.style.visibility = "hidden";
+ this.frame.style.visibility = "hidden";
+
+ this.stopExpanding = true;
+ this.emit(EVENTS.VIEWS.HIDDEN, this);
+ }
+
+ position() {
+ return this.element.getBoundingClientRect();
+ }
+
+ locationOf(target) {
+ var parentPos = this.frame.getBoundingClientRect();
+ var targetPos = this.contents.locationOf(target, this.settings.ignoreClass);
+
+ return {
+ "left": window.scrollX + parentPos.left + targetPos.left,
+ "top": window.scrollY + parentPos.top + targetPos.top
+ };
+ }
+
+ onDisplayed(view) {
+ // Stub, override with a custom functions
+ }
+
+ onResize(view, e) {
+ // Stub, override with a custom functions
+ }
+
+ bounds() {
+ if(!this.elementBounds) {
+ this.elementBounds = bounds(this.element);
+ }
+ return this.elementBounds;
+ }
+
+ destroy() {
+
+ if(this.displayed){
+ this.displayed = false;
+
+ this.removeListeners();
+
+ this.stopExpanding = true;
+ this.element.removeChild(this.frame);
+ this.displayed = false;
+ this.frame = null;
+
+ this._textWidth = null;
+ this._textHeight = null;
+ this._width = null;
+ this._height = null;
+ }
+ // this.element.style.height = "0px";
+ // this.element.style.width = "0px";
+ }
+}
+
+EventEmitter(InlineView.prototype);
+
+export default InlineView;
diff --git a/lib/epub.js/src/mapping.js b/lib/epub.js/src/mapping.js
new file mode 100644
index 0000000..4216204
--- /dev/null
+++ b/lib/epub.js/src/mapping.js
@@ -0,0 +1,511 @@
+import EpubCFI from "./epubcfi";
+import { nodeBounds } from "./utils/core";
+
+/**
+ * Map text locations to CFI ranges
+ * @class
+ * @param {Layout} layout Layout to apply
+ * @param {string} [direction="ltr"] Text direction
+ * @param {string} [axis="horizontal"] vertical or horizontal axis
+ * @param {boolean} [dev] toggle developer highlighting
+ */
+class Mapping {
+ constructor(layout, direction, axis, dev=false) {
+ this.layout = layout;
+ this.horizontal = (axis === "horizontal") ? true : false;
+ this.direction = direction || "ltr";
+ this._dev = dev;
+ }
+
+ /**
+ * Find CFI pairs for entire section at once
+ */
+ section(view) {
+ var ranges = this.findRanges(view);
+ var map = this.rangeListToCfiList(view.section.cfiBase, ranges);
+
+ return map;
+ }
+
+ /**
+ * Find CFI pairs for a page
+ * @param {Contents} contents Contents from view
+ * @param {string} cfiBase string of the base for a cfi
+ * @param {number} start position to start at
+ * @param {number} end position to end at
+ */
+ page(contents, cfiBase, start, end) {
+ var root = contents && contents.document ? contents.document.body : false;
+ var result;
+
+ if (!root) {
+ return;
+ }
+
+ result = this.rangePairToCfiPair(cfiBase, {
+ start: this.findStart(root, start, end),
+ end: this.findEnd(root, start, end)
+ });
+
+ if (this._dev === true) {
+ let doc = contents.document;
+ let startRange = new EpubCFI(result.start).toRange(doc);
+ let endRange = new EpubCFI(result.end).toRange(doc);
+
+ let selection = doc.defaultView.getSelection();
+ let r = doc.createRange();
+ selection.removeAllRanges();
+ r.setStart(startRange.startContainer, startRange.startOffset);
+ r.setEnd(endRange.endContainer, endRange.endOffset);
+ selection.addRange(r);
+ }
+
+ return result;
+ }
+
+ /**
+ * Walk a node, preforming a function on each node it finds
+ * @private
+ * @param {Node} root Node to walkToNode
+ * @param {function} func walk function
+ * @return {*} returns the result of the walk function
+ */
+ walk(root, func) {
+ // IE11 has strange issue, if root is text node IE throws exception on
+ // calling treeWalker.nextNode(), saying
+ // Unexpected call to method or property access instead of returing null value
+ if(root && root.nodeType === Node.TEXT_NODE) {
+ return;
+ }
+ // safeFilter is required so that it can work in IE as filter is a function for IE
+ // and for other browser filter is an object.
+ var filter = {
+ acceptNode: function(node) {
+ if (node.data.trim().length > 0) {
+ return NodeFilter.FILTER_ACCEPT;
+ } else {
+ return NodeFilter.FILTER_REJECT;
+ }
+ }
+ };
+ var safeFilter = filter.acceptNode;
+ safeFilter.acceptNode = filter.acceptNode;
+
+ var treeWalker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, safeFilter, false);
+ var node;
+ var result;
+ while ((node = treeWalker.nextNode())) {
+ result = func(node);
+ if(result) break;
+ }
+
+ return result;
+ }
+
+ findRanges(view){
+ var columns = [];
+ var scrollWidth = view.contents.scrollWidth();
+ var spreads = Math.ceil( scrollWidth / this.layout.spreadWidth);
+ var count = spreads * this.layout.divisor;
+ var columnWidth = this.layout.columnWidth;
+ var gap = this.layout.gap;
+ var start, end;
+
+ for (var i = 0; i < count.pages; i++) {
+ start = (columnWidth + gap) * i;
+ end = (columnWidth * (i+1)) + (gap * i);
+ columns.push({
+ start: this.findStart(view.document.body, start, end),
+ end: this.findEnd(view.document.body, start, end)
+ });
+ }
+
+ return columns;
+ }
+
+ /**
+ * Find Start Range
+ * @private
+ * @param {Node} root root node
+ * @param {number} start position to start at
+ * @param {number} end position to end at
+ * @return {Range}
+ */
+ findStart(root, start, end){
+ var stack = [root];
+ var $el;
+ var found;
+ var $prev = root;
+
+ while (stack.length) {
+
+ $el = stack.shift();
+
+ found = this.walk($el, (node) => {
+ var left, right, top, bottom;
+ var elPos;
+ var elRange;
+
+
+ elPos = nodeBounds(node);
+
+ if (this.horizontal && this.direction === "ltr") {
+
+ left = this.horizontal ? elPos.left : elPos.top;
+ right = this.horizontal ? elPos.right : elPos.bottom;
+
+ if( left >= start && left <= end ) {
+ return node;
+ } else if (right > start) {
+ return node;
+ } else {
+ $prev = node;
+ stack.push(node);
+ }
+
+ } else if (this.horizontal && this.direction === "rtl") {
+
+ left = elPos.left;
+ right = elPos.right;
+
+ if( right <= end && right >= start ) {
+ return node;
+ } else if (left < end) {
+ return node;
+ } else {
+ $prev = node;
+ stack.push(node);
+ }
+
+ } else {
+
+ top = elPos.top;
+ bottom = elPos.bottom;
+
+ if( top >= start && top <= end ) {
+ return node;
+ } else if (bottom > start) {
+ return node;
+ } else {
+ $prev = node;
+ stack.push(node);
+ }
+
+ }
+
+
+ });
+
+ if(found) {
+ return this.findTextStartRange(found, start, end);
+ }
+
+ }
+
+ // Return last element
+ return this.findTextStartRange($prev, start, end);
+ }
+
+ /**
+ * Find End Range
+ * @private
+ * @param {Node} root root node
+ * @param {number} start position to start at
+ * @param {number} end position to end at
+ * @return {Range}
+ */
+ findEnd(root, start, end){
+ var stack = [root];
+ var $el;
+ var $prev = root;
+ var found;
+
+ while (stack.length) {
+
+ $el = stack.shift();
+
+ found = this.walk($el, (node) => {
+
+ var left, right, top, bottom;
+ var elPos;
+ var elRange;
+
+ elPos = nodeBounds(node);
+
+ if (this.horizontal && this.direction === "ltr") {
+
+ left = Math.round(elPos.left);
+ right = Math.round(elPos.right);
+
+ if(left > end && $prev) {
+ return $prev;
+ } else if(right > end) {
+ return node;
+ } else {
+ $prev = node;
+ stack.push(node);
+ }
+
+ } else if (this.horizontal && this.direction === "rtl") {
+
+ left = Math.round(this.horizontal ? elPos.left : elPos.top);
+ right = Math.round(this.horizontal ? elPos.right : elPos.bottom);
+
+ if(right < start && $prev) {
+ return $prev;
+ } else if(left < start) {
+ return node;
+ } else {
+ $prev = node;
+ stack.push(node);
+ }
+
+ } else {
+
+ top = Math.round(elPos.top);
+ bottom = Math.round(elPos.bottom);
+
+ if(top > end && $prev) {
+ return $prev;
+ } else if(bottom > end) {
+ return node;
+ } else {
+ $prev = node;
+ stack.push(node);
+ }
+
+ }
+
+ });
+
+
+ if(found){
+ return this.findTextEndRange(found, start, end);
+ }
+
+ }
+
+ // end of chapter
+ return this.findTextEndRange($prev, start, end);
+ }
+
+ /**
+ * Find Text Start Range
+ * @private
+ * @param {Node} root root node
+ * @param {number} start position to start at
+ * @param {number} end position to end at
+ * @return {Range}
+ */
+ findTextStartRange(node, start, end){
+ var ranges = this.splitTextNodeIntoRanges(node);
+ var range;
+ var pos;
+ var left, top, right;
+
+ for (var i = 0; i < ranges.length; i++) {
+ range = ranges[i];
+
+ pos = range.getBoundingClientRect();
+
+ if (this.horizontal && this.direction === "ltr") {
+
+ left = pos.left;
+ if( left >= start ) {
+ return range;
+ }
+
+ } else if (this.horizontal && this.direction === "rtl") {
+
+ right = pos.right;
+ if( right <= end ) {
+ return range;
+ }
+
+ } else {
+
+ top = pos.top;
+ if( top >= start ) {
+ return range;
+ }
+
+ }
+
+ // prev = range;
+
+ }
+
+ return ranges[0];
+ }
+
+ /**
+ * Find Text End Range
+ * @private
+ * @param {Node} root root node
+ * @param {number} start position to start at
+ * @param {number} end position to end at
+ * @return {Range}
+ */
+ findTextEndRange(node, start, end){
+ var ranges = this.splitTextNodeIntoRanges(node);
+ var prev;
+ var range;
+ var pos;
+ var left, right, top, bottom;
+
+ for (var i = 0; i < ranges.length; i++) {
+ range = ranges[i];
+
+ pos = range.getBoundingClientRect();
+
+ if (this.horizontal && this.direction === "ltr") {
+
+ left = pos.left;
+ right = pos.right;
+
+ if(left > end && prev) {
+ return prev;
+ } else if(right > end) {
+ return range;
+ }
+
+ } else if (this.horizontal && this.direction === "rtl") {
+
+ left = pos.left
+ right = pos.right;
+
+ if(right < start && prev) {
+ return prev;
+ } else if(left < start) {
+ return range;
+ }
+
+ } else {
+
+ top = pos.top;
+ bottom = pos.bottom;
+
+ if(top > end && prev) {
+ return prev;
+ } else if(bottom > end) {
+ return range;
+ }
+
+ }
+
+
+ prev = range;
+
+ }
+
+ // Ends before limit
+ return ranges[ranges.length-1];
+
+ }
+
+ /**
+ * Split up a text node into ranges for each word
+ * @private
+ * @param {Node} root root node
+ * @param {string} [_splitter] what to split on
+ * @return {Range[]}
+ */
+ splitTextNodeIntoRanges(node, _splitter){
+ var ranges = [];
+ var textContent = node.textContent || "";
+ var text = textContent.trim();
+ var range;
+ var doc = node.ownerDocument;
+ var splitter = _splitter || " ";
+
+ var pos = text.indexOf(splitter);
+
+ if(pos === -1 || node.nodeType != Node.TEXT_NODE) {
+ range = doc.createRange();
+ range.selectNodeContents(node);
+ return [range];
+ }
+
+ range = doc.createRange();
+ range.setStart(node, 0);
+ range.setEnd(node, pos);
+ ranges.push(range);
+ range = false;
+
+ while ( pos != -1 ) {
+
+ pos = text.indexOf(splitter, pos + 1);
+ if(pos > 0) {
+
+ if(range) {
+ range.setEnd(node, pos);
+ ranges.push(range);
+ }
+
+ range = doc.createRange();
+ range.setStart(node, pos+1);
+ }
+ }
+
+ if(range) {
+ range.setEnd(node, text.length);
+ ranges.push(range);
+ }
+
+ return ranges;
+ }
+
+
+ /**
+ * Turn a pair of ranges into a pair of CFIs
+ * @private
+ * @param {string} cfiBase base string for an EpubCFI
+ * @param {object} rangePair { start: Range, end: Range }
+ * @return {object} { start: "epubcfi(...)", end: "epubcfi(...)" }
+ */
+ rangePairToCfiPair(cfiBase, rangePair){
+
+ var startRange = rangePair.start;
+ var endRange = rangePair.end;
+
+ startRange.collapse(true);
+ endRange.collapse(false);
+
+ let startCfi = new EpubCFI(startRange, cfiBase).toString();
+ let endCfi = new EpubCFI(endRange, cfiBase).toString();
+
+ return {
+ start: startCfi,
+ end: endCfi
+ };
+
+ }
+
+ rangeListToCfiList(cfiBase, columns){
+ var map = [];
+ var cifPair;
+
+ for (var i = 0; i < columns.length; i++) {
+ cifPair = this.rangePairToCfiPair(cfiBase, columns[i]);
+
+ map.push(cifPair);
+
+ }
+
+ return map;
+ }
+
+ /**
+ * Set the axis for mapping
+ * @param {string} axis horizontal | vertical
+ * @return {boolean} is it horizontal?
+ */
+ axis(axis) {
+ if (axis) {
+ this.horizontal = (axis === "horizontal") ? true : false;
+ }
+ return this.horizontal;
+ }
+}
+
+export default Mapping;
diff --git a/lib/epub.js/src/navigation.js b/lib/epub.js/src/navigation.js
new file mode 100644
index 0000000..8d593a8
--- /dev/null
+++ b/lib/epub.js/src/navigation.js
@@ -0,0 +1,356 @@
+import {qs, qsa, querySelectorByType, filterChildren, getParentByTagName} from "./utils/core";
+
+/**
+ * Navigation Parser
+ * @param {document} xml navigation html / xhtml / ncx
+ */
+class Navigation {
+ constructor(xml) {
+ this.toc = [];
+ this.tocByHref = {};
+ this.tocById = {};
+
+ this.landmarks = [];
+ this.landmarksByType = {};
+
+ this.length = 0;
+ if (xml) {
+ this.parse(xml);
+ }
+ }
+
+ /**
+ * Parse out the navigation items
+ * @param {document} xml navigation html / xhtml / ncx
+ */
+ parse(xml) {
+ let isXml = xml.nodeType;
+ let html;
+ let ncx;
+
+ if (isXml) {
+ html = qs(xml, "html");
+ ncx = qs(xml, "ncx");
+ }
+
+ if (!isXml) {
+ this.toc = this.load(xml);
+ } else if(html) {
+ this.toc = this.parseNav(xml);
+ this.landmarks = this.parseLandmarks(xml);
+ } else if(ncx){
+ this.toc = this.parseNcx(xml);
+ }
+
+ this.length = 0;
+
+ this.unpack(this.toc);
+ }
+
+ /**
+ * Unpack navigation items
+ * @private
+ * @param {array} toc
+ */
+ unpack(toc) {
+ var item;
+
+ for (var i = 0; i < toc.length; i++) {
+ item = toc[i];
+
+ if (item.href) {
+ this.tocByHref[item.href] = i;
+ }
+
+ if (item.id) {
+ this.tocById[item.id] = i;
+ }
+
+ this.length++;
+
+ if (item.subitems.length) {
+ this.unpack(item.subitems);
+ }
+ }
+
+ }
+
+ /**
+ * Get an item from the navigation
+ * @param {string} target
+ * @return {object} navItem
+ */
+ get(target) {
+ var index;
+
+ if(!target) {
+ return this.toc;
+ }
+
+ if(target.indexOf("#") === 0) {
+ index = this.tocById[target.substring(1)];
+ } else if(target in this.tocByHref){
+ index = this.tocByHref[target];
+ }
+
+ return this.getByIndex(target, index, this.toc);
+ }
+
+ /**
+ * Get an item from navigation subitems recursively by index
+ * @param {string} target
+ * @param {number} index
+ * @param {array} navItems
+ * @return {object} navItem
+ */
+ getByIndex(target, index, navItems) {
+ if (navItems.length === 0) {
+ return;
+ }
+
+ const item = navItems[index];
+ if (item && (target === item.id || target === item.href)) {
+ return item;
+ } else {
+ let result;
+ for (let i = 0; i < navItems.length; ++i) {
+ result = this.getByIndex(target, index, navItems[i].subitems);
+ if (result) {
+ break;
+ }
+ }
+ return result;
+ }
+ }
+
+ /**
+ * Get a landmark by type
+ * List of types: https://idpf.github.io/epub-vocabs/structure/
+ * @param {string} type
+ * @return {object} landmarkItem
+ */
+ landmark(type) {
+ var index;
+
+ if(!type) {
+ return this.landmarks;
+ }
+
+ index = this.landmarksByType[type];
+
+ return this.landmarks[index];
+ }
+
+ /**
+ * Parse toc from a Epub > 3.0 Nav
+ * @private
+ * @param {document} navHtml
+ * @return {array} navigation list
+ */
+ parseNav(navHtml){
+ var navElement = querySelectorByType(navHtml, "nav", "toc");
+ var list = [];
+
+ if (!navElement) return list;
+
+ let navList = filterChildren(navElement, "ol", true);
+ if (!navList) return list;
+
+ list = this.parseNavList(navList);
+ return list;
+ }
+
+ /**
+ * Parses lists in the toc
+ * @param {document} navListHtml
+ * @param {string} parent id
+ * @return {array} navigation list
+ */
+ parseNavList(navListHtml, parent) {
+ const result = [];
+
+ if (!navListHtml) return result;
+ if (!navListHtml.children) return result;
+
+ for (let i = 0; i < navListHtml.children.length; i++) {
+ const item = this.navItem(navListHtml.children[i], parent);
+
+ if (item) {
+ result.push(item);
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Create a navItem
+ * @private
+ * @param {element} item
+ * @return {object} navItem
+ */
+ navItem(item, parent) {
+ let id = item.getAttribute("id") || undefined;
+ let content = filterChildren(item, "a", true);
+
+ if (!content) {
+ return;
+ }
+
+ let src = content.getAttribute("href") || "";
+
+ if (!id) {
+ id = src;
+ }
+ let text = content.textContent || "";
+
+ let subitems = [];
+ let nested = filterChildren(item, "ol", true);
+ if (nested) {
+ subitems = this.parseNavList(nested, id);
+ }
+
+ return {
+ "id": id,
+ "href": src,
+ "label": text,
+ "subitems" : subitems,
+ "parent" : parent
+ };
+ }
+
+ /**
+ * Parse landmarks from a Epub > 3.0 Nav
+ * @private
+ * @param {document} navHtml
+ * @return {array} landmarks list
+ */
+ parseLandmarks(navHtml){
+ var navElement = querySelectorByType(navHtml, "nav", "landmarks");
+ var navItems = navElement ? qsa(navElement, "li") : [];
+ var length = navItems.length;
+ var i;
+ var list = [];
+ var item;
+
+ if(!navItems || length === 0) return list;
+
+ for (i = 0; i < length; ++i) {
+ item = this.landmarkItem(navItems[i]);
+ if (item) {
+ list.push(item);
+ this.landmarksByType[item.type] = i;
+ }
+ }
+
+ return list;
+ }
+
+ /**
+ * Create a landmarkItem
+ * @private
+ * @param {element} item
+ * @return {object} landmarkItem
+ */
+ landmarkItem(item){
+ let content = filterChildren(item, "a", true);
+
+ if (!content) {
+ return;
+ }
+
+ let type = content.getAttributeNS("http://www.idpf.org/2007/ops", "type") || undefined;
+ let href = content.getAttribute("href") || "";
+ let text = content.textContent || "";
+
+ return {
+ "href": href,
+ "label": text,
+ "type" : type
+ };
+ }
+
+ /**
+ * Parse from a Epub > 3.0 NC
+ * @private
+ * @param {document} navHtml
+ * @return {array} navigation list
+ */
+ parseNcx(tocXml){
+ var navPoints = qsa(tocXml, "navPoint");
+ var length = navPoints.length;
+ var i;
+ var toc = {};
+ var list = [];
+ var item, parent;
+
+ if(!navPoints || length === 0) return list;
+
+ for (i = 0; i < length; ++i) {
+ item = this.ncxItem(navPoints[i]);
+ toc[item.id] = item;
+ if(!item.parent) {
+ list.push(item);
+ } else {
+ parent = toc[item.parent];
+ parent.subitems.push(item);
+ }
+ }
+
+ return list;
+ }
+
+ /**
+ * Create a ncxItem
+ * @private
+ * @param {element} item
+ * @return {object} ncxItem
+ */
+ ncxItem(item){
+ var id = item.getAttribute("id") || false,
+ content = qs(item, "content"),
+ src = content.getAttribute("src"),
+ navLabel = qs(item, "navLabel"),
+ text = navLabel.textContent ? navLabel.textContent : "",
+ subitems = [],
+ parentNode = item.parentNode,
+ parent;
+
+ if(parentNode && (parentNode.nodeName === "navPoint" || parentNode.nodeName.split(':').slice(-1)[0] === "navPoint")) {
+ parent = parentNode.getAttribute("id");
+ }
+
+
+ return {
+ "id": id,
+ "href": src,
+ "label": text,
+ "subitems" : subitems,
+ "parent" : parent
+ };
+ }
+
+ /**
+ * Load Spine Items
+ * @param {object} json the items to be loaded
+ * @return {Array} navItems
+ */
+ load(json) {
+ return json.map(item => {
+ item.label = item.title;
+ item.subitems = item.children ? this.load(item.children) : [];
+ return item;
+ });
+ }
+
+ /**
+ * forEach pass through
+ * @param {Function} fn function to run on each item
+ * @return {method} forEach loop
+ */
+ forEach(fn) {
+ return this.toc.forEach(fn);
+ }
+}
+
+export default Navigation;
diff --git a/lib/epub.js/src/packaging.js b/lib/epub.js/src/packaging.js
new file mode 100644
index 0000000..396b5d4
--- /dev/null
+++ b/lib/epub.js/src/packaging.js
@@ -0,0 +1,372 @@
+import {qs, qsa, qsp, indexOfElementNode} from "./utils/core";
+
+/**
+ * Open Packaging Format Parser
+ * @class
+ * @param {document} packageDocument OPF XML
+ */
+class Packaging {
+ constructor(packageDocument) {
+ this.manifest = {};
+ this.navPath = '';
+ this.ncxPath = '';
+ this.coverPath = '';
+ this.spineNodeIndex = 0;
+ this.spine = [];
+ this.metadata = {};
+
+ if (packageDocument) {
+ this.parse(packageDocument);
+ }
+ }
+
+ /**
+ * Parse OPF XML
+ * @param {document} packageDocument OPF XML
+ * @return {object} parsed package parts
+ */
+ parse(packageDocument){
+ var metadataNode, manifestNode, spineNode;
+
+ if(!packageDocument) {
+ throw new Error("Package File Not Found");
+ }
+
+ metadataNode = qs(packageDocument, "metadata");
+ if(!metadataNode) {
+ throw new Error("No Metadata Found");
+ }
+
+ manifestNode = qs(packageDocument, "manifest");
+ if(!manifestNode) {
+ throw new Error("No Manifest Found");
+ }
+
+ spineNode = qs(packageDocument, "spine");
+ if(!spineNode) {
+ throw new Error("No Spine Found");
+ }
+
+ this.manifest = this.parseManifest(manifestNode);
+ this.navPath = this.findNavPath(manifestNode);
+ this.ncxPath = this.findNcxPath(manifestNode, spineNode);
+ this.coverPath = this.findCoverPath(packageDocument);
+
+ this.spineNodeIndex = indexOfElementNode(spineNode);
+
+ this.spine = this.parseSpine(spineNode, this.manifest);
+
+ this.uniqueIdentifier = this.findUniqueIdentifier(packageDocument);
+ this.metadata = this.parseMetadata(metadataNode);
+
+ this.metadata.direction = spineNode.getAttribute("page-progression-direction");
+
+ return {
+ "metadata" : this.metadata,
+ "spine" : this.spine,
+ "manifest" : this.manifest,
+ "navPath" : this.navPath,
+ "ncxPath" : this.ncxPath,
+ "coverPath": this.coverPath,
+ "spineNodeIndex" : this.spineNodeIndex
+ };
+ }
+
+ /**
+ * Parse Metadata
+ * @private
+ * @param {node} xml
+ * @return {object} metadata
+ */
+ parseMetadata(xml){
+ var metadata = {};
+
+ metadata.title = this.getElementText(xml, "title");
+ metadata.creator = this.getElementText(xml, "creator");
+ metadata.description = this.getElementText(xml, "description");
+
+ metadata.pubdate = this.getElementText(xml, "date");
+
+ metadata.publisher = this.getElementText(xml, "publisher");
+
+ metadata.identifier = this.getElementText(xml, "identifier");
+ metadata.language = this.getElementText(xml, "language");
+ metadata.rights = this.getElementText(xml, "rights");
+
+ metadata.modified_date = this.getPropertyText(xml, "dcterms:modified");
+
+ metadata.layout = this.getPropertyText(xml, "rendition:layout");
+ metadata.orientation = this.getPropertyText(xml, "rendition:orientation");
+ metadata.flow = this.getPropertyText(xml, "rendition:flow");
+ metadata.viewport = this.getPropertyText(xml, "rendition:viewport");
+ metadata.media_active_class = this.getPropertyText(xml, "media:active-class");
+ metadata.spread = this.getPropertyText(xml, "rendition:spread");
+ // metadata.page_prog_dir = packageXml.querySelector("spine").getAttribute("page-progression-direction");
+
+ return metadata;
+ }
+
+ /**
+ * Parse Manifest
+ * @private
+ * @param {node} manifestXml
+ * @return {object} manifest
+ */
+ parseManifest(manifestXml){
+ var manifest = {};
+
+ //-- Turn items into an array
+ // var selected = manifestXml.querySelectorAll("item");
+ var selected = qsa(manifestXml, "item");
+ var items = Array.prototype.slice.call(selected);
+
+ //-- Create an object with the id as key
+ items.forEach(function(item){
+ var id = item.getAttribute("id"),
+ href = item.getAttribute("href") || "",
+ type = item.getAttribute("media-type") || "",
+ overlay = item.getAttribute("media-overlay") || "",
+ properties = item.getAttribute("properties") || "";
+
+ manifest[id] = {
+ "href" : href,
+ // "url" : href,
+ "type" : type,
+ "overlay" : overlay,
+ "properties" : properties.length ? properties.split(" ") : []
+ };
+
+ });
+
+ return manifest;
+
+ }
+
+ /**
+ * Parse Spine
+ * @private
+ * @param {node} spineXml
+ * @param {Packaging.manifest} manifest
+ * @return {object} spine
+ */
+ parseSpine(spineXml, manifest){
+ var spine = [];
+
+ var selected = qsa(spineXml, "itemref");
+ var items = Array.prototype.slice.call(selected);
+
+ // var epubcfi = new EpubCFI();
+
+ //-- Add to array to mantain ordering and cross reference with manifest
+ items.forEach(function(item, index){
+ var idref = item.getAttribute("idref");
+ // var cfiBase = epubcfi.generateChapterComponent(spineNodeIndex, index, Id);
+ var props = item.getAttribute("properties") || "";
+ var propArray = props.length ? props.split(" ") : [];
+ // var manifestProps = manifest[Id].properties;
+ // var manifestPropArray = manifestProps.length ? manifestProps.split(" ") : [];
+
+ var itemref = {
+ "idref" : idref,
+ "linear" : item.getAttribute("linear") || "yes",
+ "properties" : propArray,
+ // "href" : manifest[Id].href,
+ // "url" : manifest[Id].url,
+ "index" : index
+ // "cfiBase" : cfiBase
+ };
+ spine.push(itemref);
+ });
+
+ return spine;
+ }
+
+ /**
+ * Find Unique Identifier
+ * @private
+ * @param {node} packageXml
+ * @return {string} Unique Identifier text
+ */
+ findUniqueIdentifier(packageXml){
+ var uniqueIdentifierId = packageXml.documentElement.getAttribute("unique-identifier");
+ if (! uniqueIdentifierId) {
+ return "";
+ }
+ var identifier = packageXml.getElementById(uniqueIdentifierId);
+ if (! identifier) {
+ return "";
+ }
+
+ if (identifier.localName === "identifier" && identifier.namespaceURI === "http://purl.org/dc/elements/1.1/") {
+ return identifier.childNodes.length > 0 ? identifier.childNodes[0].nodeValue.trim() : "";
+ }
+
+ return "";
+ }
+
+ /**
+ * Find TOC NAV
+ * @private
+ * @param {element} manifestNode
+ * @return {string}
+ */
+ findNavPath(manifestNode){
+ // Find item with property "nav"
+ // Should catch nav irregardless of order
+ // var node = manifestNode.querySelector("item[properties$='nav'], item[properties^='nav '], item[properties*=' nav ']");
+ var node = qsp(manifestNode, "item", {"properties":"nav"});
+ return node ? node.getAttribute("href") : false;
+ }
+
+ /**
+ * Find TOC NCX
+ * media-type="application/x-dtbncx+xml" href="toc.ncx"
+ * @private
+ * @param {element} manifestNode
+ * @param {element} spineNode
+ * @return {string}
+ */
+ findNcxPath(manifestNode, spineNode){
+ // var node = manifestNode.querySelector("item[media-type='application/x-dtbncx+xml']");
+ var node = qsp(manifestNode, "item", {"media-type":"application/x-dtbncx+xml"});
+ var tocId;
+
+ // If we can't find the toc by media-type then try to look for id of the item in the spine attributes as
+ // according to http://www.idpf.org/epub/20/spec/OPF_2.0.1_draft.htm#Section2.4.1.2,
+ // "The item that describes the NCX must be referenced by the spine toc attribute."
+ if (!node) {
+ tocId = spineNode.getAttribute("toc");
+ if(tocId) {
+ // node = manifestNode.querySelector("item[id='" + tocId + "']");
+ node = manifestNode.querySelector(`#${tocId}`);
+ }
+ }
+
+ return node ? node.getAttribute("href") : false;
+ }
+
+ /**
+ * Find the Cover Path
+ * <item properties="cover-image" id="ci" href="cover.svg" media-type="image/svg+xml" />
+ * Fallback for Epub 2.0
+ * @private
+ * @param {node} packageXml
+ * @return {string} href
+ */
+ findCoverPath(packageXml){
+ var pkg = qs(packageXml, "package");
+ var epubVersion = pkg.getAttribute("version");
+
+ // Try parsing cover with epub 3.
+ // var node = packageXml.querySelector("item[properties='cover-image']");
+ var node = qsp(packageXml, "item", {"properties":"cover-image"});
+ if (node) return node.getAttribute("href");
+
+ // Fallback to epub 2.
+ var metaCover = qsp(packageXml, "meta", {"name":"cover"});
+
+ if (metaCover) {
+ var coverId = metaCover.getAttribute("content");
+ // var cover = packageXml.querySelector("item[id='" + coverId + "']");
+ var cover = packageXml.getElementById(coverId);
+ return cover ? cover.getAttribute("href") : "";
+ }
+ else {
+ return false;
+ }
+ }
+
+ /**
+ * Get text of a namespaced element
+ * @private
+ * @param {node} xml
+ * @param {string} tag
+ * @return {string} text
+ */
+ getElementText(xml, tag){
+ var found = xml.getElementsByTagNameNS("http://purl.org/dc/elements/1.1/", tag);
+ var el;
+
+ if(!found || found.length === 0) return "";
+
+ el = found[0];
+
+ if(el.childNodes.length){
+ return el.childNodes[0].nodeValue;
+ }
+
+ return "";
+
+ }
+
+ /**
+ * Get text by property
+ * @private
+ * @param {node} xml
+ * @param {string} property
+ * @return {string} text
+ */
+ getPropertyText(xml, property){
+ var el = qsp(xml, "meta", {"property":property});
+
+ if(el && el.childNodes.length){
+ return el.childNodes[0].nodeValue;
+ }
+
+ return "";
+ }
+
+ /**
+ * Load JSON Manifest
+ * @param {document} packageDocument OPF XML
+ * @return {object} parsed package parts
+ */
+ load(json) {
+ this.metadata = json.metadata;
+
+ let spine = json.readingOrder || json.spine;
+ this.spine = spine.map((item, index) =>{
+ item.index = index;
+ item.linear = item.linear || "yes";
+ return item;
+ });
+
+ json.resources.forEach((item, index) => {
+ this.manifest[index] = item;
+
+ if (item.rel && item.rel[0] === "cover") {
+ this.coverPath = item.href;
+ }
+ });
+
+ this.spineNodeIndex = 0;
+
+ this.toc = json.toc.map((item, index) =>{
+ item.label = item.title;
+ return item;
+ });
+
+ return {
+ "metadata" : this.metadata,
+ "spine" : this.spine,
+ "manifest" : this.manifest,
+ "navPath" : this.navPath,
+ "ncxPath" : this.ncxPath,
+ "coverPath": this.coverPath,
+ "spineNodeIndex" : this.spineNodeIndex,
+ "toc" : this.toc
+ };
+ }
+
+ destroy() {
+ this.manifest = undefined;
+ this.navPath = undefined;
+ this.ncxPath = undefined;
+ this.coverPath = undefined;
+ this.spineNodeIndex = undefined;
+ this.spine = undefined;
+ this.metadata = undefined;
+ }
+}
+
+export default Packaging;
diff --git a/lib/epub.js/src/pagelist.js b/lib/epub.js/src/pagelist.js
new file mode 100644
index 0000000..6de82f6
--- /dev/null
+++ b/lib/epub.js/src/pagelist.js
@@ -0,0 +1,274 @@
+import EpubCFI from "./epubcfi";
+import {
+ qs,
+ qsa,
+ querySelectorByType,
+ indexOfSorted,
+ locationOf
+} from "./utils/core";
+
+/**
+ * Page List Parser
+ * @param {document} [xml]
+ */
+class PageList {
+ constructor(xml) {
+ this.pages = [];
+ this.locations = [];
+ this.epubcfi = new EpubCFI();
+
+ this.firstPage = 0;
+ this.lastPage = 0;
+ this.totalPages = 0;
+
+ this.toc = undefined;
+ this.ncx = undefined;
+
+ if (xml) {
+ this.pageList = this.parse(xml);
+ }
+
+ if(this.pageList && this.pageList.length) {
+ this.process(this.pageList);
+ }
+ }
+
+ /**
+ * Parse PageList Xml
+ * @param {document} xml
+ */
+ parse(xml) {
+ var html = qs(xml, "html");
+ var ncx = qs(xml, "ncx");
+
+ if(html) {
+ return this.parseNav(xml);
+ } else if(ncx){
+ return this.parseNcx(xml);
+ }
+
+ }
+
+ /**
+ * Parse a Nav PageList
+ * @private
+ * @param {node} navHtml
+ * @return {PageList.item[]} list
+ */
+ parseNav(navHtml){
+ var navElement = querySelectorByType(navHtml, "nav", "page-list");
+ var navItems = navElement ? qsa(navElement, "li") : [];
+ var length = navItems.length;
+ var i;
+ var list = [];
+ var item;
+
+ if(!navItems || length === 0) return list;
+
+ for (i = 0; i < length; ++i) {
+ item = this.item(navItems[i]);
+ list.push(item);
+ }
+
+ return list;
+ }
+
+ parseNcx(navXml) {
+ var list = [];
+ var i = 0;
+ var item;
+ var pageList;
+ var pageTargets;
+ var length = 0;
+
+ pageList = qs(navXml, "pageList");
+ if (!pageList) return list;
+
+ pageTargets = qsa(pageList, "pageTarget");
+ length = pageTargets.length;
+
+ if (!pageTargets || pageTargets.length === 0) {
+ return list;
+ }
+
+ for (i = 0; i < length; ++i) {
+ item = this.ncxItem(pageTargets[i]);
+ list.push(item);
+ }
+
+ return list;
+ }
+
+ ncxItem(item) {
+ var navLabel = qs(item, "navLabel");
+ var navLabelText = qs(navLabel, "text");
+ var pageText = navLabelText.textContent;
+ var content = qs(item, "content");
+
+ var href = content.getAttribute("src");
+ var page = parseInt(pageText, 10);
+
+ return {
+ "href": href,
+ "page": page,
+ };
+ }
+
+ /**
+ * Page List Item
+ * @private
+ * @param {node} item
+ * @return {object} pageListItem
+ */
+ item(item){
+ var content = qs(item, "a"),
+ href = content.getAttribute("href") || "",
+ text = content.textContent || "",
+ page = parseInt(text),
+ isCfi = href.indexOf("epubcfi"),
+ split,
+ packageUrl,
+ cfi;
+
+ if(isCfi != -1) {
+ split = href.split("#");
+ packageUrl = split[0];
+ cfi = split.length > 1 ? split[1] : false;
+ return {
+ "cfi" : cfi,
+ "href" : href,
+ "packageUrl" : packageUrl,
+ "page" : page
+ };
+ } else {
+ return {
+ "href" : href,
+ "page" : page
+ };
+ }
+ }
+
+ /**
+ * Process pageList items
+ * @private
+ * @param {array} pageList
+ */
+ process(pageList){
+ pageList.forEach(function(item){
+ this.pages.push(item.page);
+ if (item.cfi) {
+ this.locations.push(item.cfi);
+ }
+ }, this);
+ this.firstPage = parseInt(this.pages[0]);
+ this.lastPage = parseInt(this.pages[this.pages.length-1]);
+ this.totalPages = this.lastPage - this.firstPage;
+ }
+
+ /**
+ * Get a PageList result from a EpubCFI
+ * @param {string} cfi EpubCFI String
+ * @return {number} page
+ */
+ pageFromCfi(cfi){
+ var pg = -1;
+
+ // Check if the pageList has not been set yet
+ if(this.locations.length === 0) {
+ return -1;
+ }
+
+ // TODO: check if CFI is valid?
+
+ // check if the cfi is in the location list
+ // var index = this.locations.indexOf(cfi);
+ var index = indexOfSorted(cfi, this.locations, this.epubcfi.compare);
+ if(index != -1) {
+ pg = this.pages[index];
+ } else {
+ // Otherwise add it to the list of locations
+ // Insert it in the correct position in the locations page
+ //index = EPUBJS.core.insert(cfi, this.locations, this.epubcfi.compare);
+ index = locationOf(cfi, this.locations, this.epubcfi.compare);
+ // Get the page at the location just before the new one, or return the first
+ pg = index-1 >= 0 ? this.pages[index-1] : this.pages[0];
+ if(pg !== undefined) {
+ // Add the new page in so that the locations and page array match up
+ //this.pages.splice(index, 0, pg);
+ } else {
+ pg = -1;
+ }
+
+ }
+ return pg;
+ }
+
+ /**
+ * Get an EpubCFI from a Page List Item
+ * @param {string | number} pg
+ * @return {string} cfi
+ */
+ cfiFromPage(pg){
+ var cfi = -1;
+ // check that pg is an int
+ if(typeof pg != "number"){
+ pg = parseInt(pg);
+ }
+
+ // check if the cfi is in the page list
+ // Pages could be unsorted.
+ var index = this.pages.indexOf(pg);
+ if(index != -1) {
+ cfi = this.locations[index];
+ }
+ // TODO: handle pages not in the list
+ return cfi;
+ }
+
+ /**
+ * Get a Page from Book percentage
+ * @param {number} percent
+ * @return {number} page
+ */
+ pageFromPercentage(percent){
+ var pg = Math.round(this.totalPages * percent);
+ return pg;
+ }
+
+ /**
+ * Returns a value between 0 - 1 corresponding to the location of a page
+ * @param {number} pg the page
+ * @return {number} percentage
+ */
+ percentageFromPage(pg){
+ var percentage = (pg - this.firstPage) / this.totalPages;
+ return Math.round(percentage * 1000) / 1000;
+ }
+
+ /**
+ * Returns a value between 0 - 1 corresponding to the location of a cfi
+ * @param {string} cfi EpubCFI String
+ * @return {number} percentage
+ */
+ percentageFromCfi(cfi){
+ var pg = this.pageFromCfi(cfi);
+ var percentage = this.percentageFromPage(pg);
+ return percentage;
+ }
+
+ /**
+ * Destroy
+ */
+ destroy() {
+ this.pages = undefined;
+ this.locations = undefined;
+ this.epubcfi = undefined;
+
+ this.pageList = undefined;
+
+ this.toc = undefined;
+ this.ncx = undefined;
+ }
+}
+
+export default PageList;
diff --git a/lib/epub.js/src/rendition.js b/lib/epub.js/src/rendition.js
new file mode 100644
index 0000000..f9ba53d
--- /dev/null
+++ b/lib/epub.js/src/rendition.js
@@ -0,0 +1,1064 @@
+import EventEmitter from "event-emitter";
+import { extend, defer, isFloat } from "./utils/core";
+import Hook from "./utils/hook";
+import EpubCFI from "./epubcfi";
+import Queue from "./utils/queue";
+import Layout from "./layout";
+// import Mapping from "./mapping";
+import Themes from "./themes";
+import Contents from "./contents";
+import Annotations from "./annotations";
+import { EVENTS, DOM_EVENTS } from "./utils/constants";
+
+// Default Views
+import IframeView from "./managers/views/iframe";
+
+// Default View Managers
+import DefaultViewManager from "./managers/default/index";
+import ContinuousViewManager from "./managers/continuous/index";
+
+/**
+ * Displays an Epub as a series of Views for each Section.
+ * Requires Manager and View class to handle specifics of rendering
+ * the section content.
+ * @class
+ * @param {Book} book
+ * @param {object} [options]
+ * @param {number} [options.width]
+ * @param {number} [options.height]
+ * @param {string} [options.ignoreClass] class for the cfi parser to ignore
+ * @param {string | function | object} [options.manager='default']
+ * @param {string | function} [options.view='iframe']
+ * @param {string} [options.layout] layout to force
+ * @param {string} [options.spread] force spread value
+ * @param {number} [options.minSpreadWidth] overridden by spread: none (never) / both (always)
+ * @param {string} [options.stylesheet] url of stylesheet to be injected
+ * @param {boolean} [options.resizeOnOrientationChange] false to disable orientation events
+ * @param {string} [options.script] url of script to be injected
+ * @param {boolean | object} [options.snap=false] use snap scrolling
+ */
+class Rendition {
+ constructor(book, options) {
+
+ this.settings = extend(this.settings || {}, {
+ width: null,
+ height: null,
+ ignoreClass: "",
+ manager: "default",
+ view: "iframe",
+ flow: null,
+ layout: null,
+ spread: null,
+ minSpreadWidth: 800,
+ stylesheet: null,
+ resizeOnOrientationChange: true,
+ script: null,
+ snap: false,
+ defaultDirection: "ltr"
+ });
+
+ extend(this.settings, options);
+
+ if (typeof(this.settings.manager) === "object") {
+ this.manager = this.settings.manager;
+ }
+
+ this.book = book;
+
+ /**
+ * Adds Hook methods to the Rendition prototype
+ * @member {object} hooks
+ * @property {Hook} hooks.content
+ * @memberof Rendition
+ */
+ this.hooks = {};
+ this.hooks.display = new Hook(this);
+ this.hooks.serialize = new Hook(this);
+ this.hooks.content = new Hook(this);
+ this.hooks.unloaded = new Hook(this);
+ this.hooks.layout = new Hook(this);
+ this.hooks.render = new Hook(this);
+ this.hooks.show = new Hook(this);
+
+ this.hooks.content.register(this.handleLinks.bind(this));
+ this.hooks.content.register(this.passEvents.bind(this));
+ this.hooks.content.register(this.adjustImages.bind(this));
+
+ this.book.spine.hooks.content.register(this.injectIdentifier.bind(this));
+
+ if (this.settings.stylesheet) {
+ this.book.spine.hooks.content.register(this.injectStylesheet.bind(this));
+ }
+
+ if (this.settings.script) {
+ this.book.spine.hooks.content.register(this.injectScript.bind(this));
+ }
+
+ /**
+ * @member {Themes} themes
+ * @memberof Rendition
+ */
+ this.themes = new Themes(this);
+
+ /**
+ * @member {Annotations} annotations
+ * @memberof Rendition
+ */
+ this.annotations = new Annotations(this);
+
+ this.epubcfi = new EpubCFI();
+
+ this.q = new Queue(this);
+
+ /**
+ * A Rendered Location Range
+ * @typedef location
+ * @type {Object}
+ * @property {object} start
+ * @property {string} start.index
+ * @property {string} start.href
+ * @property {object} start.displayed
+ * @property {EpubCFI} start.cfi
+ * @property {number} start.location
+ * @property {number} start.percentage
+ * @property {number} start.displayed.page
+ * @property {number} start.displayed.total
+ * @property {object} end
+ * @property {string} end.index
+ * @property {string} end.href
+ * @property {object} end.displayed
+ * @property {EpubCFI} end.cfi
+ * @property {number} end.location
+ * @property {number} end.percentage
+ * @property {number} end.displayed.page
+ * @property {number} end.displayed.total
+ * @property {boolean} atStart
+ * @property {boolean} atEnd
+ * @memberof Rendition
+ */
+ this.location = undefined;
+
+ // Hold queue until book is opened
+ this.q.enqueue(this.book.opened);
+
+ this.starting = new defer();
+ /**
+ * @member {promise} started returns after the rendition has started
+ * @memberof Rendition
+ */
+ this.started = this.starting.promise;
+
+ // Block the queue until rendering is started
+ this.q.enqueue(this.start);
+ }
+
+ /**
+ * Set the manager function
+ * @param {function} manager
+ */
+ setManager(manager) {
+ this.manager = manager;
+ }
+
+ /**
+ * Require the manager from passed string, or as a class function
+ * @param {string|object} manager [description]
+ * @return {method}
+ */
+ requireManager(manager) {
+ var viewManager;
+
+ // If manager is a string, try to load from imported managers
+ if (typeof manager === "string" && manager === "default") {
+ viewManager = DefaultViewManager;
+ } else if (typeof manager === "string" && manager === "continuous") {
+ viewManager = ContinuousViewManager;
+ } else {
+ // otherwise, assume we were passed a class function
+ viewManager = manager;
+ }
+
+ return viewManager;
+ }
+
+ /**
+ * Require the view from passed string, or as a class function
+ * @param {string|object} view
+ * @return {view}
+ */
+ requireView(view) {
+ var View;
+
+ // If view is a string, try to load from imported views,
+ if (typeof view == "string" && view === "iframe") {
+ View = IframeView;
+ } else {
+ // otherwise, assume we were passed a class function
+ View = view;
+ }
+
+ return View;
+ }
+
+ /**
+ * Start the rendering
+ * @return {Promise} rendering has started
+ */
+ start(){
+ if (!this.settings.layout && (this.book.package.metadata.layout === "pre-paginated" || this.book.displayOptions.fixedLayout === "true")) {
+ this.settings.layout = "pre-paginated";
+ }
+ switch(this.book.package.metadata.spread) {
+ case 'none':
+ this.settings.spread = 'none';
+ break;
+ case 'both':
+ this.settings.spread = true;
+ break;
+ }
+
+ if(!this.manager) {
+ this.ViewManager = this.requireManager(this.settings.manager);
+ this.View = this.requireView(this.settings.view);
+
+ this.manager = new this.ViewManager({
+ view: this.View,
+ queue: this.q,
+ request: this.book.load.bind(this.book),
+ settings: this.settings
+ });
+ }
+
+ this.direction(this.book.package.metadata.direction || this.settings.defaultDirection);
+
+ // Parse metadata to get layout props
+ this.settings.globalLayoutProperties = this.determineLayoutProperties(this.book.package.metadata);
+
+ this.flow(this.settings.globalLayoutProperties.flow);
+
+ this.layout(this.settings.globalLayoutProperties);
+
+ // Listen for displayed views
+ this.manager.on(EVENTS.MANAGERS.ADDED, this.afterDisplayed.bind(this));
+ this.manager.on(EVENTS.MANAGERS.REMOVED, this.afterRemoved.bind(this));
+
+ // Listen for resizing
+ this.manager.on(EVENTS.MANAGERS.RESIZED, this.onResized.bind(this));
+
+ // Listen for rotation
+ this.manager.on(EVENTS.MANAGERS.ORIENTATION_CHANGE, this.onOrientationChange.bind(this));
+
+ // Listen for scroll changes
+ this.manager.on(EVENTS.MANAGERS.SCROLLED, this.reportLocation.bind(this));
+
+ /**
+ * Emit that rendering has started
+ * @event started
+ * @memberof Rendition
+ */
+ this.emit(EVENTS.RENDITION.STARTED);
+
+ // Start processing queue
+ this.starting.resolve();
+ }
+
+ /**
+ * Call to attach the container to an element in the dom
+ * Container must be attached before rendering can begin
+ * @param {element} element to attach to
+ * @return {Promise}
+ */
+ attachTo(element){
+
+ return this.q.enqueue(function () {
+
+ // Start rendering
+ this.manager.render(element, {
+ "width" : this.settings.width,
+ "height" : this.settings.height
+ });
+
+ /**
+ * Emit that rendering has attached to an element
+ * @event attached
+ * @memberof Rendition
+ */
+ this.emit(EVENTS.RENDITION.ATTACHED);
+
+ }.bind(this));
+
+ }
+
+ /**
+ * Display a point in the book
+ * The request will be added to the rendering Queue,
+ * so it will wait until book is opened, rendering started
+ * and all other rendering tasks have finished to be called.
+ * @param {string} target Url or EpubCFI
+ * @return {Promise}
+ */
+ display(target){
+ if (this.displaying) {
+ this.displaying.resolve();
+ }
+ return this.q.enqueue(this._display, target);
+ }
+
+ /**
+ * Tells the manager what to display immediately
+ * @private
+ * @param {string} target Url or EpubCFI
+ * @return {Promise}
+ */
+ _display(target){
+ if (!this.book) {
+ return;
+ }
+ var isCfiString = this.epubcfi.isCfiString(target);
+ var displaying = new defer();
+ var displayed = displaying.promise;
+ var section;
+ var moveTo;
+
+ this.displaying = displaying;
+
+ // Check if this is a book percentage
+ if (this.book.locations.length() && isFloat(target)) {
+ target = this.book.locations.cfiFromPercentage(parseFloat(target));
+ }
+
+ section = this.book.spine.get(target);
+
+ if(!section){
+ displaying.reject(new Error("No Section Found"));
+ return displayed;
+ }
+
+ this.manager.display(section, target)
+ .then(() => {
+ displaying.resolve(section);
+ this.displaying = undefined;
+
+ /**
+ * Emit that a section has been displayed
+ * @event displayed
+ * @param {Section} section
+ * @memberof Rendition
+ */
+ this.emit(EVENTS.RENDITION.DISPLAYED, section);
+ this.reportLocation();
+ }, (err) => {
+ /**
+ * Emit that has been an error displaying
+ * @event displayError
+ * @param {Section} section
+ * @memberof Rendition
+ */
+ this.emit(EVENTS.RENDITION.DISPLAY_ERROR, err);
+ });
+
+ return displayed;
+ }
+
+ /*
+ render(view, show) {
+
+ // view.onLayout = this.layout.format.bind(this.layout);
+ view.create();
+
+ // Fit to size of the container, apply padding
+ this.manager.resizeView(view);
+
+ // Render Chain
+ return view.section.render(this.book.request)
+ .then(function(contents){
+ return view.load(contents);
+ }.bind(this))
+ .then(function(doc){
+ return this.hooks.content.trigger(view, this);
+ }.bind(this))
+ .then(function(){
+ this.layout.format(view.contents);
+ return this.hooks.layout.trigger(view, this);
+ }.bind(this))
+ .then(function(){
+ return view.display();
+ }.bind(this))
+ .then(function(){
+ return this.hooks.render.trigger(view, this);
+ }.bind(this))
+ .then(function(){
+ if(show !== false) {
+ this.q.enqueue(function(view){
+ view.show();
+ }, view);
+ }
+ // this.map = new Map(view, this.layout);
+ this.hooks.show.trigger(view, this);
+ this.trigger("rendered", view.section);
+
+ }.bind(this))
+ .catch(function(e){
+ this.trigger("loaderror", e);
+ }.bind(this));
+
+ }
+ */
+
+ /**
+ * Report what section has been displayed
+ * @private
+ * @param {*} view
+ */
+ afterDisplayed(view){
+
+ view.on(EVENTS.VIEWS.MARK_CLICKED, (cfiRange, data) => this.triggerMarkEvent(cfiRange, data, view.contents));
+
+ this.hooks.render.trigger(view, this)
+ .then(() => {
+ if (view.contents) {
+ this.hooks.content.trigger(view.contents, this).then(() => {
+ /**
+ * Emit that a section has been rendered
+ * @event rendered
+ * @param {Section} section
+ * @param {View} view
+ * @memberof Rendition
+ */
+ this.emit(EVENTS.RENDITION.RENDERED, view.section, view);
+ });
+ } else {
+ this.emit(EVENTS.RENDITION.RENDERED, view.section, view);
+ }
+ });
+
+ }
+
+ /**
+ * Report what has been removed
+ * @private
+ * @param {*} view
+ */
+ afterRemoved(view){
+ this.hooks.unloaded.trigger(view, this).then(() => {
+ /**
+ * Emit that a section has been removed
+ * @event removed
+ * @param {Section} section
+ * @param {View} view
+ * @memberof Rendition
+ */
+ this.emit(EVENTS.RENDITION.REMOVED, view.section, view);
+ });
+ }
+
+ /**
+ * Report resize events and display the last seen location
+ * @private
+ */
+ onResized(size, epubcfi){
+
+ /**
+ * Emit that the rendition has been resized
+ * @event resized
+ * @param {number} width
+ * @param {height} height
+ * @param {string} epubcfi (optional)
+ * @memberof Rendition
+ */
+ this.emit(EVENTS.RENDITION.RESIZED, {
+ width: size.width,
+ height: size.height
+ }, epubcfi);
+
+ if (this.location && this.location.start) {
+ this.display(epubcfi || this.location.start.cfi);
+ }
+
+ }
+
+ /**
+ * Report orientation events and display the last seen location
+ * @private
+ */
+ onOrientationChange(orientation){
+ /**
+ * Emit that the rendition has been rotated
+ * @event orientationchange
+ * @param {string} orientation
+ * @memberof Rendition
+ */
+ this.emit(EVENTS.RENDITION.ORIENTATION_CHANGE, orientation);
+ }
+
+ /**
+ * Move the Rendition to a specific offset
+ * Usually you would be better off calling display()
+ * @param {object} offset
+ */
+ moveTo(offset){
+ this.manager.moveTo(offset);
+ }
+
+ /**
+ * Trigger a resize of the views
+ * @param {number} [width]
+ * @param {number} [height]
+ * @param {string} [epubcfi] (optional)
+ */
+ resize(width, height, epubcfi){
+ if (width) {
+ this.settings.width = width;
+ }
+ if (height) {
+ this.settings.height = height;
+ }
+ this.manager.resize(width, height, epubcfi);
+ }
+
+ /**
+ * Clear all rendered views
+ */
+ clear(){
+ this.manager.clear();
+ }
+
+ /**
+ * Go to the next "page" in the rendition
+ * @return {Promise}
+ */
+ next(){
+ return this.q.enqueue(this.manager.next.bind(this.manager))
+ .then(this.reportLocation.bind(this));
+ }
+
+ /**
+ * Go to the previous "page" in the rendition
+ * @return {Promise}
+ */
+ prev(){
+ return this.q.enqueue(this.manager.prev.bind(this.manager))
+ .then(this.reportLocation.bind(this));
+ }
+
+ //-- http://www.idpf.org/epub/301/spec/epub-publications.html#meta-properties-rendering
+ /**
+ * Determine the Layout properties from metadata and settings
+ * @private
+ * @param {object} metadata
+ * @return {object} properties
+ */
+ determineLayoutProperties(metadata){
+ var properties;
+ var layout = this.settings.layout || metadata.layout || "reflowable";
+ var spread = this.settings.spread || metadata.spread || "auto";
+ var orientation = this.settings.orientation || metadata.orientation || "auto";
+ var flow = this.settings.flow || metadata.flow || "auto";
+ var viewport = metadata.viewport || "";
+ var minSpreadWidth = this.settings.minSpreadWidth || metadata.minSpreadWidth || 800;
+ var direction = this.settings.direction || metadata.direction || "ltr";
+
+ if ((this.settings.width === 0 || this.settings.width > 0) &&
+ (this.settings.height === 0 || this.settings.height > 0)) {
+ // viewport = "width="+this.settings.width+", height="+this.settings.height+"";
+ }
+
+ properties = {
+ layout : layout,
+ spread : spread,
+ orientation : orientation,
+ flow : flow,
+ viewport : viewport,
+ minSpreadWidth : minSpreadWidth,
+ direction: direction
+ };
+
+ return properties;
+ }
+
+ /**
+ * Adjust the flow of the rendition to paginated or scrolled
+ * (scrolled-continuous vs scrolled-doc are handled by different view managers)
+ * @param {string} flow
+ */
+ flow(flow){
+ var _flow = flow;
+ if (flow === "scrolled" ||
+ flow === "scrolled-doc" ||
+ flow === "scrolled-continuous") {
+ _flow = "scrolled";
+ }
+
+ if (flow === "auto" || flow === "paginated") {
+ _flow = "paginated";
+ }
+
+ this.settings.flow = flow;
+
+ if (this._layout) {
+ this._layout.flow(_flow);
+ }
+
+ if (this.manager && this._layout) {
+ this.manager.applyLayout(this._layout);
+ }
+
+ if (this.manager) {
+ this.manager.updateFlow(_flow);
+ }
+
+ if (this.manager && this.manager.isRendered() && this.location) {
+ this.manager.clear();
+ this.display(this.location.start.cfi);
+ }
+ }
+
+ /**
+ * Adjust the layout of the rendition to reflowable or pre-paginated
+ * @param {object} settings
+ */
+ layout(settings){
+ if (settings) {
+ this._layout = new Layout(settings);
+ this._layout.spread(settings.spread, this.settings.minSpreadWidth);
+
+ // this.mapping = new Mapping(this._layout.props);
+
+ this._layout.on(EVENTS.LAYOUT.UPDATED, (props, changed) => {
+ this.emit(EVENTS.RENDITION.LAYOUT, props, changed);
+ })
+ }
+
+ if (this.manager && this._layout) {
+ this.manager.applyLayout(this._layout);
+ }
+
+ return this._layout;
+ }
+
+ /**
+ * Adjust if the rendition uses spreads
+ * @param {string} spread none | auto (TODO: implement landscape, portrait, both)
+ * @param {int} [min] min width to use spreads at
+ */
+ spread(spread, min){
+
+ this.settings.spread = spread;
+
+ if (min) {
+ this.settings.minSpreadWidth = min;
+ }
+
+ if (this._layout) {
+ this._layout.spread(spread, min);
+ }
+
+ if (this.manager && this.manager.isRendered()) {
+ this.manager.updateLayout();
+ }
+ }
+
+ /**
+ * Adjust the direction of the rendition
+ * @param {string} dir
+ */
+ direction(dir){
+
+ this.settings.direction = dir || "ltr";
+
+ if (this.manager) {
+ this.manager.direction(this.settings.direction);
+ }
+
+ if (this.manager && this.manager.isRendered() && this.location) {
+ this.manager.clear();
+ this.display(this.location.start.cfi);
+ }
+ }
+
+ /**
+ * Report the current location
+ * @fires relocated
+ * @fires locationChanged
+ */
+ reportLocation(){
+ return this.q.enqueue(function reportedLocation(){
+ requestAnimationFrame(function reportedLocationAfterRAF() {
+ var location = this.manager.currentLocation();
+ if (location && location.then && typeof location.then === "function") {
+ location.then(function(result) {
+ let located = this.located(result);
+
+ if (!located || !located.start || !located.end) {
+ return;
+ }
+
+ this.location = located;
+
+ this.emit(EVENTS.RENDITION.LOCATION_CHANGED, {
+ index: this.location.start.index,
+ href: this.location.start.href,
+ start: this.location.start.cfi,
+ end: this.location.end.cfi,
+ percentage: this.location.start.percentage
+ });
+
+ this.emit(EVENTS.RENDITION.RELOCATED, this.location);
+ }.bind(this));
+ } else if (location) {
+ let located = this.located(location);
+
+ if (!located || !located.start || !located.end) {
+ return;
+ }
+
+ this.location = located;
+
+ /**
+ * @event locationChanged
+ * @deprecated
+ * @type {object}
+ * @property {number} index
+ * @property {string} href
+ * @property {EpubCFI} start
+ * @property {EpubCFI} end
+ * @property {number} percentage
+ * @memberof Rendition
+ */
+ this.emit(EVENTS.RENDITION.LOCATION_CHANGED, {
+ index: this.location.start.index,
+ href: this.location.start.href,
+ start: this.location.start.cfi,
+ end: this.location.end.cfi,
+ percentage: this.location.start.percentage
+ });
+
+ /**
+ * @event relocated
+ * @type {displayedLocation}
+ * @memberof Rendition
+ */
+ this.emit(EVENTS.RENDITION.RELOCATED, this.location);
+ }
+ }.bind(this));
+ }.bind(this));
+ }
+
+ /**
+ * Get the Current Location object
+ * @return {displayedLocation | promise} location (may be a promise)
+ */
+ currentLocation(){
+ var location = this.manager.currentLocation();
+ if (location && location.then && typeof location.then === "function") {
+ location.then(function(result) {
+ let located = this.located(result);
+ return located;
+ }.bind(this));
+ } else if (location) {
+ let located = this.located(location);
+ return located;
+ }
+ }
+
+ /**
+ * Creates a Rendition#locationRange from location
+ * passed by the Manager
+ * @returns {displayedLocation}
+ * @private
+ */
+ located(location){
+ if (!location.length) {
+ return {};
+ }
+ let start = location[0];
+ let end = location[location.length-1];
+
+ let located = {
+ start: {
+ index: start.index,
+ href: start.href,
+ cfi: start.mapping.start,
+ displayed: {
+ page: start.pages[0] || 1,
+ total: start.totalPages
+ }
+ },
+ end: {
+ index: end.index,
+ href: end.href,
+ cfi: end.mapping.end,
+ displayed: {
+ page: end.pages[end.pages.length-1] || 1,
+ total: end.totalPages
+ }
+ }
+ };
+
+ let locationStart = this.book.locations.locationFromCfi(start.mapping.start);
+ let locationEnd = this.book.locations.locationFromCfi(end.mapping.end);
+
+ if (locationStart != null) {
+ located.start.location = locationStart;
+ located.start.percentage = this.book.locations.percentageFromLocation(locationStart);
+ }
+ if (locationEnd != null) {
+ located.end.location = locationEnd;
+ located.end.percentage = this.book.locations.percentageFromLocation(locationEnd);
+ }
+
+ let pageStart = this.book.pageList.pageFromCfi(start.mapping.start);
+ let pageEnd = this.book.pageList.pageFromCfi(end.mapping.end);
+
+ if (pageStart != -1) {
+ located.start.page = pageStart;
+ }
+ if (pageEnd != -1) {
+ located.end.page = pageEnd;
+ }
+
+ if (end.index === this.book.spine.last().index &&
+ located.end.displayed.page >= located.end.displayed.total) {
+ located.atEnd = true;
+ }
+
+ if (start.index === this.book.spine.first().index &&
+ located.start.displayed.page === 1) {
+ located.atStart = true;
+ }
+
+ return located;
+ }
+
+ /**
+ * Remove and Clean Up the Rendition
+ */
+ destroy(){
+ // Clear the queue
+ // this.q.clear();
+ // this.q = undefined;
+
+ this.manager && this.manager.destroy();
+
+ this.book = undefined;
+
+ // this.views = null;
+
+ // this.hooks.display.clear();
+ // this.hooks.serialize.clear();
+ // this.hooks.content.clear();
+ // this.hooks.layout.clear();
+ // this.hooks.render.clear();
+ // this.hooks.show.clear();
+ // this.hooks = {};
+
+ // this.themes.destroy();
+ // this.themes = undefined;
+
+ // this.epubcfi = undefined;
+
+ // this.starting = undefined;
+ // this.started = undefined;
+
+
+ }
+
+ /**
+ * Pass the events from a view's Contents
+ * @private
+ * @param {Contents} view contents
+ */
+ passEvents(contents){
+ DOM_EVENTS.forEach((e) => {
+ contents.on(e, (ev) => this.triggerViewEvent(ev, contents));
+ });
+
+ contents.on(EVENTS.CONTENTS.SELECTED, (e) => this.triggerSelectedEvent(e, contents));
+ }
+
+ /**
+ * Emit events passed by a view
+ * @private
+ * @param {event} e
+ */
+ triggerViewEvent(e, contents){
+ this.emit(e.type, e, contents);
+ }
+
+ /**
+ * Emit a selection event's CFI Range passed from a a view
+ * @private
+ * @param {string} cfirange
+ */
+ triggerSelectedEvent(cfirange, contents){
+ /**
+ * Emit that a text selection has occured
+ * @event selected
+ * @param {string} cfirange
+ * @param {Contents} contents
+ * @memberof Rendition
+ */
+ this.emit(EVENTS.RENDITION.SELECTED, cfirange, contents);
+ }
+
+ /**
+ * Emit a markClicked event with the cfiRange and data from a mark
+ * @private
+ * @param {EpubCFI} cfirange
+ */
+ triggerMarkEvent(cfiRange, data, contents){
+ /**
+ * Emit that a mark was clicked
+ * @event markClicked
+ * @param {EpubCFI} cfirange
+ * @param {object} data
+ * @param {Contents} contents
+ * @memberof Rendition
+ */
+ this.emit(EVENTS.RENDITION.MARK_CLICKED, cfiRange, data, contents);
+ }
+
+ /**
+ * Get a Range from a Visible CFI
+ * @param {string} cfi EpubCfi String
+ * @param {string} ignoreClass
+ * @return {range}
+ */
+ getRange(cfi, ignoreClass){
+ var _cfi = new EpubCFI(cfi);
+ var found = this.manager.visible().filter(function (view) {
+ if(_cfi.spinePos === view.index) return true;
+ });
+
+ // Should only every return 1 item
+ if (found.length) {
+ return found[0].contents.range(_cfi, ignoreClass);
+ }
+ }
+
+ /**
+ * Hook to adjust images to fit in columns
+ * @param {Contents} contents
+ * @private
+ */
+ adjustImages(contents) {
+
+ if (this._layout.name === "pre-paginated") {
+ return new Promise(function(resolve){
+ resolve();
+ });
+ }
+
+ let computed = contents.window.getComputedStyle(contents.content, null);
+ let height = (contents.content.offsetHeight - (parseFloat(computed.paddingTop) + parseFloat(computed.paddingBottom))) * .95;
+ let horizontalPadding = parseFloat(computed.paddingLeft) + parseFloat(computed.paddingRight);
+
+ contents.addStylesheetRules({
+ "img" : {
+ "max-width": (this._layout.columnWidth ? (this._layout.columnWidth - horizontalPadding) + "px" : "100%") + "!important",
+ "max-height": height + "px" + "!important",
+ "object-fit": "contain",
+ "page-break-inside": "avoid",
+ "break-inside": "avoid",
+ "box-sizing": "border-box"
+ },
+ "svg" : {
+ "max-width": (this._layout.columnWidth ? (this._layout.columnWidth - horizontalPadding) + "px" : "100%") + "!important",
+ "max-height": height + "px" + "!important",
+ "page-break-inside": "avoid",
+ "break-inside": "avoid"
+ }
+ });
+
+ return new Promise(function(resolve, reject){
+ // Wait to apply
+ setTimeout(function() {
+ resolve();
+ }, 1);
+ });
+ }
+
+ /**
+ * Get the Contents object of each rendered view
+ * @returns {Contents[]}
+ */
+ getContents () {
+ return this.manager ? this.manager.getContents() : [];
+ }
+
+ /**
+ * Get the views member from the manager
+ * @returns {Views}
+ */
+ views () {
+ let views = this.manager ? this.manager.views : undefined;
+ return views || [];
+ }
+
+ /**
+ * Hook to handle link clicks in rendered content
+ * @param {Contents} contents
+ * @private
+ */
+ handleLinks(contents) {
+ if (contents) {
+ contents.on(EVENTS.CONTENTS.LINK_CLICKED, (href) => {
+ let relative = this.book.path.relative(href);
+ this.display(relative);
+ });
+ }
+ }
+
+ /**
+ * Hook to handle injecting stylesheet before
+ * a Section is serialized
+ * @param {document} doc
+ * @param {Section} section
+ * @private
+ */
+ injectStylesheet(doc, section) {
+ let style = doc.createElement("link");
+ style.setAttribute("type", "text/css");
+ style.setAttribute("rel", "stylesheet");
+ style.setAttribute("href", this.settings.stylesheet);
+ doc.getElementsByTagName("head")[0].appendChild(style);
+ }
+
+ /**
+ * Hook to handle injecting scripts before
+ * a Section is serialized
+ * @param {document} doc
+ * @param {Section} section
+ * @private
+ */
+ injectScript(doc, section) {
+ let script = doc.createElement("script");
+ script.setAttribute("type", "text/javascript");
+ script.setAttribute("src", this.settings.script);
+ script.textContent = " "; // Needed to prevent self closing tag
+ doc.getElementsByTagName("head")[0].appendChild(script);
+ }
+
+ /**
+ * Hook to handle the document identifier before
+ * a Section is serialized
+ * @param {document} doc
+ * @param {Section} section
+ * @private
+ */
+ injectIdentifier(doc, section) {
+ let ident = this.book.packaging.metadata.identifier;
+ let meta = doc.createElement("meta");
+ meta.setAttribute("name", "dc.relation.ispartof");
+ if (ident) {
+ meta.setAttribute("content", ident);
+ }
+ doc.getElementsByTagName("head")[0].appendChild(meta);
+ }
+
+}
+
+//-- Enable binding events to Renderer
+EventEmitter(Rendition.prototype);
+
+export default Rendition;
diff --git a/lib/epub.js/src/resources.js b/lib/epub.js/src/resources.js
new file mode 100644
index 0000000..b5d77f8
--- /dev/null
+++ b/lib/epub.js/src/resources.js
@@ -0,0 +1,320 @@
+import {substitute} from "./utils/replacements";
+import {createBase64Url, createBlobUrl, blob2base64} from "./utils/core";
+import Url from "./utils/url";
+import mime from "./utils/mime";
+import Path from "./utils/path";
+import path from "path-webpack";
+
+/**
+ * Handle Package Resources
+ * @class
+ * @param {Manifest} manifest
+ * @param {object} [options]
+ * @param {string} [options.replacements="base64"]
+ * @param {Archive} [options.archive]
+ * @param {method} [options.resolver]
+ */
+class Resources {
+ constructor(manifest, options) {
+ this.settings = {
+ replacements: (options && options.replacements) || "base64",
+ archive: (options && options.archive),
+ resolver: (options && options.resolver),
+ request: (options && options.request)
+ };
+
+ this.process(manifest);
+ }
+
+ /**
+ * Process resources
+ * @param {Manifest} manifest
+ */
+ process(manifest){
+ this.manifest = manifest;
+ this.resources = Object.keys(manifest).
+ map(function (key){
+ return manifest[key];
+ });
+
+ this.replacementUrls = [];
+
+ this.html = [];
+ this.assets = [];
+ this.css = [];
+
+ this.urls = [];
+ this.cssUrls = [];
+
+ this.split();
+ this.splitUrls();
+ }
+
+ /**
+ * Split resources by type
+ * @private
+ */
+ split(){
+
+ // HTML
+ this.html = this.resources.
+ filter(function (item){
+ if (item.type === "application/xhtml+xml" ||
+ item.type === "text/html") {
+ return true;
+ }
+ });
+
+ // Exclude HTML
+ this.assets = this.resources.
+ filter(function (item){
+ if (item.type !== "application/xhtml+xml" &&
+ item.type !== "text/html") {
+ return true;
+ }
+ });
+
+ // Only CSS
+ this.css = this.resources.
+ filter(function (item){
+ if (item.type === "text/css") {
+ return true;
+ }
+ });
+ }
+
+ /**
+ * Convert split resources into Urls
+ * @private
+ */
+ splitUrls(){
+
+ // All Assets Urls
+ this.urls = this.assets.
+ map(function(item) {
+ return item.href;
+ }.bind(this));
+
+ // Css Urls
+ this.cssUrls = this.css.map(function(item) {
+ return item.href;
+ });
+
+ }
+
+ /**
+ * Create a url to a resource
+ * @param {string} url
+ * @return {Promise<string>} Promise resolves with url string
+ */
+ createUrl (url) {
+ var parsedUrl = new Url(url);
+ var mimeType = mime.lookup(parsedUrl.filename);
+
+ if (this.settings.archive) {
+ return this.settings.archive.createUrl(url, {"base64": (this.settings.replacements === "base64")});
+ } else {
+ if (this.settings.replacements === "base64") {
+ return this.settings.request(url, 'blob')
+ .then((blob) => {
+ return blob2base64(blob);
+ })
+ .then((blob) => {
+ return createBase64Url(blob, mimeType);
+ });
+ } else {
+ return this.settings.request(url, 'blob').then((blob) => {
+ return createBlobUrl(blob, mimeType);
+ })
+ }
+ }
+ }
+
+ /**
+ * Create blob urls for all the assets
+ * @return {Promise} returns replacement urls
+ */
+ replacements(){
+ if (this.settings.replacements === "none") {
+ return new Promise(function(resolve) {
+ resolve(this.urls);
+ }.bind(this));
+ }
+
+ var replacements = this.urls.map( (url) => {
+ var absolute = this.settings.resolver(url);
+
+ return this.createUrl(absolute).
+ catch((err) => {
+ console.error(err);
+ return null;
+ });
+ });
+
+ return Promise.all(replacements)
+ .then( (replacementUrls) => {
+ this.replacementUrls = replacementUrls.filter((url) => {
+ return (typeof(url) === "string");
+ });
+ return replacementUrls;
+ });
+ }
+
+ /**
+ * Replace URLs in CSS resources
+ * @private
+ * @param {Archive} [archive]
+ * @param {method} [resolver]
+ * @return {Promise}
+ */
+ replaceCss(archive, resolver){
+ var replaced = [];
+ archive = archive || this.settings.archive;
+ resolver = resolver || this.settings.resolver;
+ this.cssUrls.forEach(function(href) {
+ var replacement = this.createCssFile(href, archive, resolver)
+ .then(function (replacementUrl) {
+ // switch the url in the replacementUrls
+ var indexInUrls = this.urls.indexOf(href);
+ if (indexInUrls > -1) {
+ this.replacementUrls[indexInUrls] = replacementUrl;
+ }
+ }.bind(this))
+
+
+ replaced.push(replacement);
+ }.bind(this));
+ return Promise.all(replaced);
+ }
+
+ /**
+ * Create a new CSS file with the replaced URLs
+ * @private
+ * @param {string} href the original css file
+ * @return {Promise} returns a BlobUrl to the new CSS file or a data url
+ */
+ createCssFile(href){
+ var newUrl;
+
+ if (path.isAbsolute(href)) {
+ return new Promise(function(resolve){
+ resolve();
+ });
+ }
+
+ var absolute = this.settings.resolver(href);
+
+ // Get the text of the css file from the archive
+ var textResponse;
+
+ if (this.settings.archive) {
+ textResponse = this.settings.archive.getText(absolute);
+ } else {
+ textResponse = this.settings.request(absolute, "text");
+ }
+
+ // Get asset links relative to css file
+ var relUrls = this.urls.map( (assetHref) => {
+ var resolved = this.settings.resolver(assetHref);
+ var relative = new Path(absolute).relative(resolved);
+
+ return relative;
+ });
+
+ if (!textResponse) {
+ // file not found, don't replace
+ return new Promise(function(resolve){
+ resolve();
+ });
+ }
+
+ return textResponse.then( (text) => {
+ // Replacements in the css text
+ text = substitute(text, relUrls, this.replacementUrls);
+
+ // Get the new url
+ if (this.settings.replacements === "base64") {
+ newUrl = createBase64Url(text, "text/css");
+ } else {
+ newUrl = createBlobUrl(text, "text/css");
+ }
+
+ return newUrl;
+ }, (err) => {
+ // handle response errors
+ return new Promise(function(resolve){
+ resolve();
+ });
+ });
+
+ }
+
+ /**
+ * Resolve all resources URLs relative to an absolute URL
+ * @param {string} absolute to be resolved to
+ * @param {resolver} [resolver]
+ * @return {string[]} array with relative Urls
+ */
+ relativeTo(absolute, resolver){
+ resolver = resolver || this.settings.resolver;
+
+ // Get Urls relative to current sections
+ return this.urls.
+ map(function(href) {
+ var resolved = resolver(href);
+ var relative = new Path(absolute).relative(resolved);
+ return relative;
+ }.bind(this));
+ }
+
+ /**
+ * Get a URL for a resource
+ * @param {string} path
+ * @return {string} url
+ */
+ get(path) {
+ var indexInUrls = this.urls.indexOf(path);
+ if (indexInUrls === -1) {
+ return;
+ }
+ if (this.replacementUrls.length) {
+ return new Promise(function(resolve, reject) {
+ resolve(this.replacementUrls[indexInUrls]);
+ }.bind(this));
+ } else {
+ return this.createUrl(path);
+ }
+ }
+
+ /**
+ * Substitute urls in content, with replacements,
+ * relative to a url if provided
+ * @param {string} content
+ * @param {string} [url] url to resolve to
+ * @return {string} content with urls substituted
+ */
+ substitute(content, url) {
+ var relUrls;
+ if (url) {
+ relUrls = this.relativeTo(url);
+ } else {
+ relUrls = this.urls;
+ }
+ return substitute(content, relUrls, this.replacementUrls);
+ }
+
+ destroy() {
+ this.settings = undefined;
+ this.manifest = undefined;
+ this.resources = undefined;
+ this.replacementUrls = undefined;
+ this.html = undefined;
+ this.assets = undefined;
+ this.css = undefined;
+
+ this.urls = undefined;
+ this.cssUrls = undefined;
+ }
+}
+
+export default Resources;
diff --git a/lib/epub.js/src/section.js b/lib/epub.js/src/section.js
new file mode 100644
index 0000000..89912d7
--- /dev/null
+++ b/lib/epub.js/src/section.js
@@ -0,0 +1,323 @@
+import { defer } from "./utils/core";
+import EpubCFI from "./epubcfi";
+import Hook from "./utils/hook";
+import { sprint } from "./utils/core";
+import { replaceBase } from "./utils/replacements";
+import Request from "./utils/request";
+import { XMLDOMParser as XMLDOMSerializer } from "xmldom";
+
+/**
+ * Represents a Section of the Book
+ *
+ * In most books this is equivelent to a Chapter
+ * @param {object} item The spine item representing the section
+ * @param {object} hooks hooks for serialize and content
+ */
+class Section {
+ constructor(item, hooks){
+ this.idref = item.idref;
+ this.linear = item.linear === "yes";
+ this.properties = item.properties;
+ this.index = item.index;
+ this.href = item.href;
+ this.url = item.url;
+ this.canonical = item.canonical;
+ this.next = item.next;
+ this.prev = item.prev;
+
+ this.cfiBase = item.cfiBase;
+
+ if (hooks) {
+ this.hooks = hooks;
+ } else {
+ this.hooks = {};
+ this.hooks.serialize = new Hook(this);
+ this.hooks.content = new Hook(this);
+ }
+
+ this.document = undefined;
+ this.contents = undefined;
+ this.output = undefined;
+ }
+
+ /**
+ * Load the section from its url
+ * @param {method} [_request] a request method to use for loading
+ * @return {document} a promise with the xml document
+ */
+ load(_request){
+ var request = _request || this.request || Request;
+ var loading = new defer();
+ var loaded = loading.promise;
+
+ if(this.contents) {
+ loading.resolve(this.contents);
+ } else {
+ request(this.url)
+ .then(function(xml){
+ // var directory = new Url(this.url).directory;
+
+ this.document = xml;
+ this.contents = xml.documentElement;
+
+ return this.hooks.content.trigger(this.document, this);
+ }.bind(this))
+ .then(function(){
+ loading.resolve(this.contents);
+ }.bind(this))
+ .catch(function(error){
+ loading.reject(error);
+ });
+ }
+
+ return loaded;
+ }
+
+ /**
+ * Adds a base tag for resolving urls in the section
+ * @private
+ */
+ base(){
+ return replaceBase(this.document, this);
+ }
+
+ /**
+ * Render the contents of a section
+ * @param {method} [_request] a request method to use for loading
+ * @return {string} output a serialized XML Document
+ */
+ render(_request){
+ var rendering = new defer();
+ var rendered = rendering.promise;
+ this.output; // TODO: better way to return this from hooks?
+
+ this.load(_request).
+ then(function(contents){
+ var userAgent = (typeof navigator !== 'undefined' && navigator.userAgent) || '';
+ var isIE = userAgent.indexOf('Trident') >= 0;
+ var Serializer;
+ if (typeof XMLSerializer === "undefined" || isIE) {
+ Serializer = XMLDOMSerializer;
+ } else {
+ Serializer = XMLSerializer;
+ }
+ var serializer = new Serializer();
+ this.output = serializer.serializeToString(contents);
+ return this.output;
+ }.bind(this)).
+ then(function(){
+ return this.hooks.serialize.trigger(this.output, this);
+ }.bind(this)).
+ then(function(){
+ rendering.resolve(this.output);
+ }.bind(this))
+ .catch(function(error){
+ rendering.reject(error);
+ });
+
+ return rendered;
+ }
+
+ /**
+ * Find a string in a section
+ * @param {string} _query The query string to find
+ * @return {object[]} A list of matches, with form {cfi, excerpt}
+ */
+ find(_query){
+ var section = this;
+ var matches = [];
+ var query = _query.toLowerCase();
+ var find = function(node){
+ var text = node.textContent.toLowerCase();
+ var range = section.document.createRange();
+ var cfi;
+ var pos;
+ var last = -1;
+ var excerpt;
+ var limit = 150;
+
+ while (pos != -1) {
+ // Search for the query
+ pos = text.indexOf(query, last + 1);
+
+ if (pos != -1) {
+ // We found it! Generate a CFI
+ range = section.document.createRange();
+ range.setStart(node, pos);
+ range.setEnd(node, pos + query.length);
+
+ cfi = section.cfiFromRange(range);
+
+ // Generate the excerpt
+ if (node.textContent.length < limit) {
+ excerpt = node.textContent;
+ }
+ else {
+ excerpt = node.textContent.substring(pos - limit/2, pos + limit/2);
+ excerpt = "..." + excerpt + "...";
+ }
+
+ // Add the CFI to the matches list
+ matches.push({
+ cfi: cfi,
+ excerpt: excerpt
+ });
+ }
+
+ last = pos;
+ }
+ };
+
+ sprint(section.document, function(node) {
+ find(node);
+ });
+
+ return matches;
+ };
+
+
+ /**
+ * Search a string in multiple sequential Element of the section. If the document.createTreeWalker api is missed(eg: IE8), use `find` as a fallback.
+ * @param {string} _query The query string to search
+ * @param {int} maxSeqEle The maximum number of Element that are combined for search, defualt value is 5.
+ * @return {object[]} A list of matches, with form {cfi, excerpt}
+ */
+ search(_query , maxSeqEle = 5){
+ if (typeof(document.createTreeWalker) == "undefined") {
+ return this.find(_query);
+ }
+ let matches = [];
+ const excerptLimit = 150;
+ const section = this;
+ const query = _query.toLowerCase();
+ const search = function(nodeList){
+ const textWithCase = nodeList.reduce((acc ,current)=>{
+ return acc + current.textContent;
+ },"");
+ const text = textWithCase.toLowerCase();
+ const pos = text.indexOf(query);
+ if (pos != -1){
+ const startNodeIndex = 0 , endPos = pos + query.length;
+ let endNodeIndex = 0 , l = 0;
+ if (pos < nodeList[startNodeIndex].length){
+ let cfi;
+ while( endNodeIndex < nodeList.length - 1 ){
+ l += nodeList[endNodeIndex].length;
+ if ( endPos <= l){
+ break;
+ }
+ endNodeIndex += 1;
+ }
+
+ let startNode = nodeList[startNodeIndex] , endNode = nodeList[endNodeIndex];
+ let range = section.document.createRange();
+ range.setStart(startNode,pos);
+ let beforeEndLengthCount = nodeList.slice(0, endNodeIndex).reduce((acc,current)=>{return acc+current.textContent.length;},0) ;
+ range.setEnd(endNode, beforeEndLengthCount > endPos ? endPos : endPos - beforeEndLengthCount );
+ cfi = section.cfiFromRange(range);
+
+ let excerpt = nodeList.slice(0, endNodeIndex+1).reduce((acc,current)=>{return acc+current.textContent ;},"");
+ if (excerpt.length > excerptLimit){
+ excerpt = excerpt.substring(pos - excerptLimit/2, pos + excerptLimit/2);
+ excerpt = "..." + excerpt + "...";
+ }
+ matches.push({
+ cfi: cfi,
+ excerpt: excerpt
+ });
+ }
+ }
+ }
+
+ const treeWalker = document.createTreeWalker(section.document, NodeFilter.SHOW_TEXT, null, false);
+ let node , nodeList = [];
+ while (node = treeWalker.nextNode()) {
+ nodeList.push(node);
+ if (nodeList.length == maxSeqEle){
+ search(nodeList.slice(0 , maxSeqEle));
+ nodeList = nodeList.slice(1, maxSeqEle);
+ }
+ }
+ if (nodeList.length > 0){
+ search(nodeList);
+ }
+ return matches;
+ }
+
+ /**
+ * Reconciles the current chapters layout properies with
+ * the global layout properities.
+ * @param {object} globalLayout The global layout settings object, chapter properties string
+ * @return {object} layoutProperties Object with layout properties
+ */
+ reconcileLayoutSettings(globalLayout){
+ //-- Get the global defaults
+ var settings = {
+ layout : globalLayout.layout,
+ spread : globalLayout.spread,
+ orientation : globalLayout.orientation
+ };
+
+ //-- Get the chapter's display type
+ this.properties.forEach(function(prop){
+ var rendition = prop.replace("rendition:", "");
+ var split = rendition.indexOf("-");
+ var property, value;
+
+ if(split != -1){
+ property = rendition.slice(0, split);
+ value = rendition.slice(split+1);
+
+ settings[property] = value;
+ }
+ });
+ return settings;
+ }
+
+ /**
+ * Get a CFI from a Range in the Section
+ * @param {range} _range
+ * @return {string} cfi an EpubCFI string
+ */
+ cfiFromRange(_range) {
+ return new EpubCFI(_range, this.cfiBase).toString();
+ }
+
+ /**
+ * Get a CFI from an Element in the Section
+ * @param {element} el
+ * @return {string} cfi an EpubCFI string
+ */
+ cfiFromElement(el) {
+ return new EpubCFI(el, this.cfiBase).toString();
+ }
+
+ /**
+ * Unload the section document
+ */
+ unload() {
+ this.document = undefined;
+ this.contents = undefined;
+ this.output = undefined;
+ }
+
+ destroy() {
+ this.unload();
+ this.hooks.serialize.clear();
+ this.hooks.content.clear();
+
+ this.hooks = undefined;
+ this.idref = undefined;
+ this.linear = undefined;
+ this.properties = undefined;
+ this.index = undefined;
+ this.href = undefined;
+ this.url = undefined;
+ this.next = undefined;
+ this.prev = undefined;
+
+ this.cfiBase = undefined;
+ }
+}
+
+export default Section;
diff --git a/lib/epub.js/src/spine.js b/lib/epub.js/src/spine.js
new file mode 100644
index 0000000..519b67c
--- /dev/null
+++ b/lib/epub.js/src/spine.js
@@ -0,0 +1,274 @@
+import EpubCFI from "./epubcfi";
+import Hook from "./utils/hook";
+import Section from "./section";
+import {replaceBase, replaceCanonical, replaceMeta} from "./utils/replacements";
+
+/**
+ * A collection of Spine Items
+ */
+class Spine {
+ constructor() {
+ this.spineItems = [];
+ this.spineByHref = {};
+ this.spineById = {};
+
+ this.hooks = {};
+ this.hooks.serialize = new Hook();
+ this.hooks.content = new Hook();
+
+ // Register replacements
+ this.hooks.content.register(replaceBase);
+ this.hooks.content.register(replaceCanonical);
+ this.hooks.content.register(replaceMeta);
+
+ this.epubcfi = new EpubCFI();
+
+ this.loaded = false;
+
+ this.items = undefined;
+ this.manifest = undefined;
+ this.spineNodeIndex = undefined;
+ this.baseUrl = undefined;
+ this.length = undefined;
+ }
+
+ /**
+ * Unpack items from a opf into spine items
+ * @param {Packaging} _package
+ * @param {method} resolver URL resolver
+ * @param {method} canonical Resolve canonical url
+ */
+ unpack(_package, resolver, canonical) {
+
+ this.items = _package.spine;
+ this.manifest = _package.manifest;
+ this.spineNodeIndex = _package.spineNodeIndex;
+ this.baseUrl = _package.baseUrl || _package.basePath || "";
+ this.length = this.items.length;
+
+ this.items.forEach( (item, index) => {
+ var manifestItem = this.manifest[item.idref];
+ var spineItem;
+
+ item.index = index;
+ item.cfiBase = this.epubcfi.generateChapterComponent(this.spineNodeIndex, item.index, item.idref);
+
+ if (item.href) {
+ item.url = resolver(item.href, true);
+ item.canonical = canonical(item.href);
+ }
+
+ if(manifestItem) {
+ item.href = manifestItem.href;
+ item.url = resolver(item.href, true);
+ item.canonical = canonical(item.href);
+
+ if(manifestItem.properties.length){
+ item.properties.push.apply(item.properties, manifestItem.properties);
+ }
+ }
+
+ if (item.linear === "yes") {
+ item.prev = function() {
+ let prevIndex = item.index;
+ while (prevIndex > 0) {
+ let prev = this.get(prevIndex-1);
+ if (prev && prev.linear) {
+ return prev;
+ }
+ prevIndex -= 1;
+ }
+ return;
+ }.bind(this);
+ item.next = function() {
+ let nextIndex = item.index;
+ while (nextIndex < this.spineItems.length-1) {
+ let next = this.get(nextIndex+1);
+ if (next && next.linear) {
+ return next;
+ }
+ nextIndex += 1;
+ }
+ return;
+ }.bind(this);
+ } else {
+ item.prev = function() {
+ return;
+ }
+ item.next = function() {
+ return;
+ }
+ }
+
+
+ spineItem = new Section(item, this.hooks);
+
+ this.append(spineItem);
+
+
+ });
+
+ this.loaded = true;
+ }
+
+ /**
+ * Get an item from the spine
+ * @param {string|number} [target]
+ * @return {Section} section
+ * @example spine.get();
+ * @example spine.get(1);
+ * @example spine.get("chap1.html");
+ * @example spine.get("#id1234");
+ */
+ get(target) {
+ var index = 0;
+
+ if (typeof target === "undefined") {
+ while (index < this.spineItems.length) {
+ let next = this.spineItems[index];
+ if (next && next.linear) {
+ break;
+ }
+ index += 1;
+ }
+ } else if(this.epubcfi.isCfiString(target)) {
+ let cfi = new EpubCFI(target);
+ index = cfi.spinePos;
+ } else if(typeof target === "number" || isNaN(target) === false){
+ index = target;
+ } else if(typeof target === "string" && target.indexOf("#") === 0) {
+ index = this.spineById[target.substring(1)];
+ } else if(typeof target === "string") {
+ // Remove fragments
+ target = target.split("#")[0];
+ index = this.spineByHref[target] || this.spineByHref[encodeURI(target)];
+ }
+
+ return this.spineItems[index] || null;
+ }
+
+ /**
+ * Append a Section to the Spine
+ * @private
+ * @param {Section} section
+ */
+ append(section) {
+ var index = this.spineItems.length;
+ section.index = index;
+
+ this.spineItems.push(section);
+
+ // Encode and Decode href lookups
+ // see pr for details: https://github.com/futurepress/epub.js/pull/358
+ this.spineByHref[decodeURI(section.href)] = index;
+ this.spineByHref[encodeURI(section.href)] = index;
+ this.spineByHref[section.href] = index;
+
+ this.spineById[section.idref] = index;
+
+ return index;
+ }
+
+ /**
+ * Prepend a Section to the Spine
+ * @private
+ * @param {Section} section
+ */
+ prepend(section) {
+ // var index = this.spineItems.unshift(section);
+ this.spineByHref[section.href] = 0;
+ this.spineById[section.idref] = 0;
+
+ // Re-index
+ this.spineItems.forEach(function(item, index){
+ item.index = index;
+ });
+
+ return 0;
+ }
+
+ // insert(section, index) {
+ //
+ // };
+
+ /**
+ * Remove a Section from the Spine
+ * @private
+ * @param {Section} section
+ */
+ remove(section) {
+ var index = this.spineItems.indexOf(section);
+
+ if(index > -1) {
+ delete this.spineByHref[section.href];
+ delete this.spineById[section.idref];
+
+ return this.spineItems.splice(index, 1);
+ }
+ }
+
+ /**
+ * Loop over the Sections in the Spine
+ * @return {method} forEach
+ */
+ each() {
+ return this.spineItems.forEach.apply(this.spineItems, arguments);
+ }
+
+ /**
+ * Find the first Section in the Spine
+ * @return {Section} first section
+ */
+ first() {
+ let index = 0;
+
+ do {
+ let next = this.get(index);
+
+ if (next && next.linear) {
+ return next;
+ }
+ index += 1;
+ } while (index < this.spineItems.length) ;
+ }
+
+ /**
+ * Find the last Section in the Spine
+ * @return {Section} last section
+ */
+ last() {
+ let index = this.spineItems.length-1;
+
+ do {
+ let prev = this.get(index);
+ if (prev && prev.linear) {
+ return prev;
+ }
+ index -= 1;
+ } while (index >= 0);
+ }
+
+ destroy() {
+ this.each((section) => section.destroy());
+
+ this.spineItems = undefined
+ this.spineByHref = undefined
+ this.spineById = undefined
+
+ this.hooks.serialize.clear();
+ this.hooks.content.clear();
+ this.hooks = undefined;
+
+ this.epubcfi = undefined;
+
+ this.loaded = false;
+
+ this.items = undefined;
+ this.manifest = undefined;
+ this.spineNodeIndex = undefined;
+ this.baseUrl = undefined;
+ this.length = undefined;
+ }
+}
+
+export default Spine;
diff --git a/lib/epub.js/src/store.js b/lib/epub.js/src/store.js
new file mode 100644
index 0000000..0d12103
--- /dev/null
+++ b/lib/epub.js/src/store.js
@@ -0,0 +1,384 @@
+import {defer, isXml, parse} from "./utils/core";
+import httpRequest from "./utils/request";
+import mime from "./utils/mime";
+import Path from "./utils/path";
+import EventEmitter from "event-emitter";
+import localforage from "localforage";
+
+/**
+ * Handles saving and requesting files from local storage
+ * @class
+ * @param {string} name This should be the name of the application for modals
+ * @param {function} [requester]
+ * @param {function} [resolver]
+ */
+class Store {
+
+ constructor(name, requester, resolver) {
+ this.urlCache = {};
+
+ this.storage = undefined;
+
+ this.name = name;
+ this.requester = requester || httpRequest;
+ this.resolver = resolver;
+
+ this.online = true;
+
+ this.checkRequirements();
+
+ this.addListeners();
+ }
+
+ /**
+ * Checks to see if localForage exists in global namspace,
+ * Requires localForage if it isn't there
+ * @private
+ */
+ checkRequirements(){
+ try {
+ let store;
+ if (typeof localforage === "undefined") {
+ store = localforage;
+ }
+ this.storage = store.createInstance({
+ name: this.name
+ });
+ } catch (e) {
+ throw new Error("localForage lib not loaded");
+ }
+ }
+
+ /**
+ * Add online and offline event listeners
+ * @private
+ */
+ addListeners() {
+ this._status = this.status.bind(this);
+ window.addEventListener('online', this._status);
+ window.addEventListener('offline', this._status);
+ }
+
+ /**
+ * Remove online and offline event listeners
+ * @private
+ */
+ removeListeners() {
+ window.removeEventListener('online', this._status);
+ window.removeEventListener('offline', this._status);
+ this._status = undefined;
+ }
+
+ /**
+ * Update the online / offline status
+ * @private
+ */
+ status(event) {
+ let online = navigator.onLine;
+ this.online = online;
+ if (online) {
+ this.emit("online", this);
+ } else {
+ this.emit("offline", this);
+ }
+ }
+
+ /**
+ * Add all of a book resources to the store
+ * @param {Resources} resources book resources
+ * @param {boolean} [force] force resaving resources
+ * @return {Promise<object>} store objects
+ */
+ add(resources, force) {
+ let mapped = resources.resources.map((item) => {
+ let { href } = item;
+ let url = this.resolver(href);
+ let encodedUrl = window.encodeURIComponent(url);
+
+ return this.storage.getItem(encodedUrl).then((item) => {
+ if (!item || force) {
+ return this.requester(url, "binary")
+ .then((data) => {
+ return this.storage.setItem(encodedUrl, data);
+ });
+ } else {
+ return item;
+ }
+ });
+
+ });
+ return Promise.all(mapped);
+ }
+
+ /**
+ * Put binary data from a url to storage
+ * @param {string} url a url to request from storage
+ * @param {boolean} [withCredentials]
+ * @param {object} [headers]
+ * @return {Promise<Blob>}
+ */
+ put(url, withCredentials, headers) {
+ let encodedUrl = window.encodeURIComponent(url);
+
+ return this.storage.getItem(encodedUrl).then((result) => {
+ if (!result) {
+ return this.requester(url, "binary", withCredentials, headers).then((data) => {
+ return this.storage.setItem(encodedUrl, data);
+ });
+ }
+ return result;
+ });
+ }
+
+ /**
+ * Request a url
+ * @param {string} url a url to request from storage
+ * @param {string} [type] specify the type of the returned result
+ * @param {boolean} [withCredentials]
+ * @param {object} [headers]
+ * @return {Promise<Blob | string | JSON | Document | XMLDocument>}
+ */
+ request(url, type, withCredentials, headers){
+ if (this.online) {
+ // From network
+ return this.requester(url, type, withCredentials, headers).then((data) => {
+ // save to store if not present
+ this.put(url);
+ return data;
+ })
+ } else {
+ // From store
+ return this.retrieve(url, type);
+ }
+
+ }
+
+ /**
+ * Request a url from storage
+ * @param {string} url a url to request from storage
+ * @param {string} [type] specify the type of the returned result
+ * @return {Promise<Blob | string | JSON | Document | XMLDocument>}
+ */
+ retrieve(url, type) {
+ var deferred = new defer();
+ var response;
+ var path = new Path(url);
+
+ // If type isn't set, determine it from the file extension
+ if(!type) {
+ type = path.extension;
+ }
+
+ if(type == "blob"){
+ response = this.getBlob(url);
+ } else {
+ response = this.getText(url);
+ }
+
+
+ return response.then((r) => {
+ var deferred = new defer();
+ var result;
+ if (r) {
+ result = this.handleResponse(r, type);
+ deferred.resolve(result);
+ } else {
+ deferred.reject({
+ message : "File not found in storage: " + url,
+ stack : new Error().stack
+ });
+ }
+ return deferred.promise;
+ });
+ }
+
+ /**
+ * Handle the response from request
+ * @private
+ * @param {any} response
+ * @param {string} [type]
+ * @return {any} the parsed result
+ */
+ handleResponse(response, type){
+ var r;
+
+ if(type == "json") {
+ r = JSON.parse(response);
+ }
+ else
+ if(isXml(type)) {
+ r = parse(response, "text/xml");
+ }
+ else
+ if(type == "xhtml") {
+ r = parse(response, "application/xhtml+xml");
+ }
+ else
+ if(type == "html" || type == "htm") {
+ r = parse(response, "text/html");
+ } else {
+ r = response;
+ }
+
+ return r;
+ }
+
+ /**
+ * Get a Blob from Storage by Url
+ * @param {string} url
+ * @param {string} [mimeType]
+ * @return {Blob}
+ */
+ getBlob(url, mimeType){
+ let encodedUrl = window.encodeURIComponent(url);
+
+ return this.storage.getItem(encodedUrl).then(function(uint8array) {
+ if(!uint8array) return;
+
+ mimeType = mimeType || mime.lookup(url);
+
+ return new Blob([uint8array], {type : mimeType});
+ });
+
+ }
+
+ /**
+ * Get Text from Storage by Url
+ * @param {string} url
+ * @param {string} [mimeType]
+ * @return {string}
+ */
+ getText(url, mimeType){
+ let encodedUrl = window.encodeURIComponent(url);
+
+ mimeType = mimeType || mime.lookup(url);
+
+ return this.storage.getItem(encodedUrl).then(function(uint8array) {
+ var deferred = new defer();
+ var reader = new FileReader();
+ var blob;
+
+ if(!uint8array) return;
+
+ blob = new Blob([uint8array], {type : mimeType});
+
+ reader.addEventListener("loadend", () => {
+ deferred.resolve(reader.result);
+ });
+
+ reader.readAsText(blob, mimeType);
+
+ return deferred.promise;
+ });
+ }
+
+ /**
+ * Get a base64 encoded result from Storage by Url
+ * @param {string} url
+ * @param {string} [mimeType]
+ * @return {string} base64 encoded
+ */
+ getBase64(url, mimeType){
+ let encodedUrl = window.encodeURIComponent(url);
+
+ mimeType = mimeType || mime.lookup(url);
+
+ return this.storage.getItem(encodedUrl).then((uint8array) => {
+ var deferred = new defer();
+ var reader = new FileReader();
+ var blob;
+
+ if(!uint8array) return;
+
+ blob = new Blob([uint8array], {type : mimeType});
+
+ reader.addEventListener("loadend", () => {
+ deferred.resolve(reader.result);
+ });
+ reader.readAsDataURL(blob, mimeType);
+
+ return deferred.promise;
+ });
+ }
+
+ /**
+ * Create a Url from a stored item
+ * @param {string} url
+ * @param {object} [options.base64] use base64 encoding or blob url
+ * @return {Promise} url promise with Url string
+ */
+ createUrl(url, options){
+ var deferred = new defer();
+ var _URL = window.URL || window.webkitURL || window.mozURL;
+ var tempUrl;
+ var response;
+ var useBase64 = options && options.base64;
+
+ if(url in this.urlCache) {
+ deferred.resolve(this.urlCache[url]);
+ return deferred.promise;
+ }
+
+ if (useBase64) {
+ response = this.getBase64(url);
+
+ if (response) {
+ response.then(function(tempUrl) {
+
+ this.urlCache[url] = tempUrl;
+ deferred.resolve(tempUrl);
+
+ }.bind(this));
+
+ }
+
+ } else {
+
+ response = this.getBlob(url);
+
+ if (response) {
+ response.then(function(blob) {
+
+ tempUrl = _URL.createObjectURL(blob);
+ this.urlCache[url] = tempUrl;
+ deferred.resolve(tempUrl);
+
+ }.bind(this));
+
+ }
+ }
+
+
+ if (!response) {
+ deferred.reject({
+ message : "File not found in storage: " + url,
+ stack : new Error().stack
+ });
+ }
+
+ return deferred.promise;
+ }
+
+ /**
+ * Revoke Temp Url for a achive item
+ * @param {string} url url of the item in the store
+ */
+ revokeUrl(url){
+ var _URL = window.URL || window.webkitURL || window.mozURL;
+ var fromCache = this.urlCache[url];
+ if(fromCache) _URL.revokeObjectURL(fromCache);
+ }
+
+ destroy() {
+ var _URL = window.URL || window.webkitURL || window.mozURL;
+ for (let fromCache in this.urlCache) {
+ _URL.revokeObjectURL(fromCache);
+ }
+ this.urlCache = {};
+ this.removeListeners();
+ }
+}
+
+EventEmitter(Store.prototype);
+
+export default Store;
diff --git a/lib/epub.js/src/themes.js b/lib/epub.js/src/themes.js
new file mode 100644
index 0000000..6e37f1d
--- /dev/null
+++ b/lib/epub.js/src/themes.js
@@ -0,0 +1,268 @@
+import Url from "./utils/url";
+
+/**
+ * Themes to apply to displayed content
+ * @class
+ * @param {Rendition} rendition
+ */
+class Themes {
+ constructor(rendition) {
+ this.rendition = rendition;
+ this._themes = {
+ "default" : {
+ "rules" : {},
+ "url" : "",
+ "serialized" : ""
+ }
+ };
+ this._overrides = {};
+ this._current = "default";
+ this._injected = [];
+ this.rendition.hooks.content.register(this.inject.bind(this));
+ this.rendition.hooks.content.register(this.overrides.bind(this));
+
+ }
+
+ /**
+ * Add themes to be used by a rendition
+ * @param {object | Array<object> | string}
+ * @example themes.register("light", "http://example.com/light.css")
+ * @example themes.register("light", { "body": { "color": "purple"}})
+ * @example themes.register({ "light" : {...}, "dark" : {...}})
+ */
+ register () {
+ if (arguments.length === 0) {
+ return;
+ }
+ if (arguments.length === 1 && typeof(arguments[0]) === "object") {
+ return this.registerThemes(arguments[0]);
+ }
+ if (arguments.length === 1 && typeof(arguments[0]) === "string") {
+ return this.default(arguments[0]);
+ }
+ if (arguments.length === 2 && typeof(arguments[1]) === "string") {
+ return this.registerUrl(arguments[0], arguments[1]);
+ }
+ if (arguments.length === 2 && typeof(arguments[1]) === "object") {
+ return this.registerRules(arguments[0], arguments[1]);
+ }
+ }
+
+ /**
+ * Add a default theme to be used by a rendition
+ * @param {object | string} theme
+ * @example themes.register("http://example.com/default.css")
+ * @example themes.register({ "body": { "color": "purple"}})
+ */
+ default (theme) {
+ if (!theme) {
+ return;
+ }
+ if (typeof(theme) === "string") {
+ return this.registerUrl("default", theme);
+ }
+ if (typeof(theme) === "object") {
+ return this.registerRules("default", theme);
+ }
+ }
+
+ /**
+ * Register themes object
+ * @param {object} themes
+ */
+ registerThemes (themes) {
+ for (var theme in themes) {
+ if (themes.hasOwnProperty(theme)) {
+ if (typeof(themes[theme]) === "string") {
+ this.registerUrl(theme, themes[theme]);
+ } else {
+ this.registerRules(theme, themes[theme]);
+ }
+ }
+ }
+ }
+
+ /**
+ * Register a theme by passing its css as string
+ * @param {string} name
+ * @param {string} css
+ */
+ registerCss (name, css) {
+ this._themes[name] = { "serialized" : css };
+ if (this._injected[name] || name == 'default') {
+ this.update(name);
+ }
+ }
+
+ /**
+ * Register a url
+ * @param {string} name
+ * @param {string} input
+ */
+ registerUrl (name, input) {
+ var url = new Url(input);
+ this._themes[name] = { "url": url.toString() };
+ if (this._injected[name] || name == 'default') {
+ this.update(name);
+ }
+ }
+
+ /**
+ * Register rule
+ * @param {string} name
+ * @param {object} rules
+ */
+ registerRules (name, rules) {
+ this._themes[name] = { "rules": rules };
+ // TODO: serialize css rules
+ if (this._injected[name] || name == 'default') {
+ this.update(name);
+ }
+ }
+
+ /**
+ * Select a theme
+ * @param {string} name
+ */
+ select (name) {
+ var prev = this._current;
+ var contents;
+
+ this._current = name;
+ this.update(name);
+
+ contents = this.rendition.getContents();
+ contents.forEach( (content) => {
+ content.removeClass(prev);
+ content.addClass(name);
+ });
+ }
+
+ /**
+ * Update a theme
+ * @param {string} name
+ */
+ update (name) {
+ var contents = this.rendition.getContents();
+ contents.forEach( (content) => {
+ this.add(name, content);
+ });
+ }
+
+ /**
+ * Inject all themes into contents
+ * @param {Contents} contents
+ */
+ inject (contents) {
+ var links = [];
+ var themes = this._themes;
+ var theme;
+
+ for (var name in themes) {
+ if (themes.hasOwnProperty(name) && (name === this._current || name === "default")) {
+ theme = themes[name];
+ if((theme.rules && Object.keys(theme.rules).length > 0) || (theme.url && links.indexOf(theme.url) === -1)) {
+ this.add(name, contents);
+ }
+ this._injected.push(name);
+ }
+ }
+
+ if(this._current != "default") {
+ contents.addClass(this._current);
+ }
+ }
+
+ /**
+ * Add Theme to contents
+ * @param {string} name
+ * @param {Contents} contents
+ */
+ add (name, contents) {
+ var theme = this._themes[name];
+
+ if (!theme || !contents) {
+ return;
+ }
+
+ if (theme.url) {
+ contents.addStylesheet(theme.url);
+ } else if (theme.serialized) {
+ contents.addStylesheetCss(theme.serialized, name);
+ theme.injected = true;
+ } else if (theme.rules) {
+ contents.addStylesheetRules(theme.rules, name);
+ theme.injected = true;
+ }
+ }
+
+ /**
+ * Add override
+ * @param {string} name
+ * @param {string} value
+ * @param {boolean} priority
+ */
+ override (name, value, priority) {
+ var contents = this.rendition.getContents();
+
+ this._overrides[name] = {
+ value: value,
+ priority: priority === true
+ };
+
+ contents.forEach( (content) => {
+ content.css(name, this._overrides[name].value, this._overrides[name].priority);
+ });
+ }
+
+ removeOverride (name) {
+ var contents = this.rendition.getContents();
+
+ delete this._overrides[name];
+
+ contents.forEach( (content) => {
+ content.css(name);
+ });
+ }
+
+ /**
+ * Add all overrides
+ * @param {Content} content
+ */
+ overrides (contents) {
+ var overrides = this._overrides;
+
+ for (var rule in overrides) {
+ if (overrides.hasOwnProperty(rule)) {
+ contents.css(rule, overrides[rule].value, overrides[rule].priority);
+ }
+ }
+ }
+
+ /**
+ * Adjust the font size of a rendition
+ * @param {number} size
+ */
+ fontSize (size) {
+ this.override("font-size", size);
+ }
+
+ /**
+ * Adjust the font-family of a rendition
+ * @param {string} f
+ */
+ font (f) {
+ this.override("font-family", f, true);
+ }
+
+ destroy() {
+ this.rendition = undefined;
+ this._themes = undefined;
+ this._overrides = undefined;
+ this._current = undefined;
+ this._injected = undefined;
+ }
+
+}
+
+export default Themes;
diff --git a/lib/epub.js/src/utils/constants.js b/lib/epub.js/src/utils/constants.js
new file mode 100644
index 0000000..ac0a268
--- /dev/null
+++ b/lib/epub.js/src/utils/constants.js
@@ -0,0 +1,62 @@
+export const EPUBJS_VERSION = "0.3";
+
+// Dom events to listen for
+export const DOM_EVENTS = ["keydown", "keyup", "keypressed", "mouseup", "mousedown", "mousemove", "click", "touchend", "touchstart", "touchmove"];
+
+export const EVENTS = {
+ BOOK : {
+ OPEN_FAILED : "openFailed"
+ },
+ CONTENTS : {
+ EXPAND : "expand",
+ RESIZE : "resize",
+ SELECTED : "selected",
+ SELECTED_RANGE : "selectedRange",
+ LINK_CLICKED : "linkClicked"
+ },
+ LOCATIONS : {
+ CHANGED : "changed"
+ },
+ MANAGERS : {
+ RESIZE : "resize",
+ RESIZED : "resized",
+ ORIENTATION_CHANGE : "orientationchange",
+ ADDED : "added",
+ SCROLL : "scroll",
+ SCROLLED : "scrolled",
+ REMOVED : "removed",
+ },
+ VIEWS : {
+ AXIS: "axis",
+ WRITING_MODE: "writingMode",
+ LOAD_ERROR : "loaderror",
+ RENDERED : "rendered",
+ RESIZED : "resized",
+ DISPLAYED : "displayed",
+ SHOWN : "shown",
+ HIDDEN : "hidden",
+ MARK_CLICKED : "markClicked"
+ },
+ RENDITION : {
+ STARTED : "started",
+ ATTACHED : "attached",
+ DISPLAYED : "displayed",
+ DISPLAY_ERROR : "displayerror",
+ RENDERED : "rendered",
+ REMOVED : "removed",
+ RESIZED : "resized",
+ ORIENTATION_CHANGE : "orientationchange",
+ LOCATION_CHANGED : "locationChanged",
+ RELOCATED : "relocated",
+ MARK_CLICKED : "markClicked",
+ SELECTED : "selected",
+ LAYOUT: "layout"
+ },
+ LAYOUT : {
+ UPDATED : "updated"
+ },
+ ANNOTATION : {
+ ATTACH : "attach",
+ DETACH : "detach"
+ }
+}
diff --git a/lib/epub.js/src/utils/core.js b/lib/epub.js/src/utils/core.js
new file mode 100644
index 0000000..5c83944
--- /dev/null
+++ b/lib/epub.js/src/utils/core.js
@@ -0,0 +1,876 @@
+/**
+ * Core Utilities and Helpers
+ * @module Core
+*/
+import { DOMParser as XMLDOMParser } from "xmldom";
+
+/**
+ * Vendor prefixed requestAnimationFrame
+ * @returns {function} requestAnimationFrame
+ * @memberof Core
+ */
+export const requestAnimationFrame = (typeof window != "undefined") ? (window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame) : false;
+const ELEMENT_NODE = 1;
+const TEXT_NODE = 3;
+const COMMENT_NODE = 8;
+const DOCUMENT_NODE = 9;
+const _URL = typeof URL != "undefined" ? URL : (typeof window != "undefined" ? (window.URL || window.webkitURL || window.mozURL) : undefined);
+
+/**
+ * Generates a UUID
+ * based on: http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript
+ * @returns {string} uuid
+ * @memberof Core
+ */
+export function uuid() {
+ var d = new Date().getTime();
+ var uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
+ var r = (d + Math.random()*16)%16 | 0;
+ d = Math.floor(d/16);
+ return (c=="x" ? r : (r&0x7|0x8)).toString(16);
+ });
+ return uuid;
+}
+
+/**
+ * Gets the height of a document
+ * @returns {number} height
+ * @memberof Core
+ */
+export function documentHeight() {
+ return Math.max(
+ document.documentElement.clientHeight,
+ document.body.scrollHeight,
+ document.documentElement.scrollHeight,
+ document.body.offsetHeight,
+ document.documentElement.offsetHeight
+ );
+}
+
+/**
+ * Checks if a node is an element
+ * @param {object} obj
+ * @returns {boolean}
+ * @memberof Core
+ */
+export function isElement(obj) {
+ return !!(obj && obj.nodeType == 1);
+}
+
+/**
+ * @param {any} n
+ * @returns {boolean}
+ * @memberof Core
+ */
+export function isNumber(n) {
+ return !isNaN(parseFloat(n)) && isFinite(n);
+}
+
+/**
+ * @param {any} n
+ * @returns {boolean}
+ * @memberof Core
+ */
+export function isFloat(n) {
+ let f = parseFloat(n);
+
+ if (isNumber(n) === false) {
+ return false;
+ }
+
+ if (typeof n === "string" && n.indexOf(".") > -1) {
+ return true;
+ }
+
+ return Math.floor(f) !== f;
+}
+
+/**
+ * Get a prefixed css property
+ * @param {string} unprefixed
+ * @returns {string}
+ * @memberof Core
+ */
+export function prefixed(unprefixed) {
+ var vendors = ["Webkit", "webkit", "Moz", "O", "ms" ];
+ var prefixes = ["-webkit-", "-webkit-", "-moz-", "-o-", "-ms-"];
+ var lower = unprefixed.toLowerCase();
+ var length = vendors.length;
+
+ if (typeof(document) === "undefined" || typeof(document.body.style[lower]) != "undefined") {
+ return unprefixed;
+ }
+
+ for (var i = 0; i < length; i++) {
+ if (typeof(document.body.style[prefixes[i] + lower]) != "undefined") {
+ return prefixes[i] + lower;
+ }
+ }
+
+ return unprefixed;
+}
+
+/**
+ * Apply defaults to an object
+ * @param {object} obj
+ * @returns {object}
+ * @memberof Core
+ */
+export function defaults(obj) {
+ for (var i = 1, length = arguments.length; i < length; i++) {
+ var source = arguments[i];
+ for (var prop in source) {
+ if (obj[prop] === void 0) obj[prop] = source[prop];
+ }
+ }
+ return obj;
+}
+
+/**
+ * Extend properties of an object
+ * @param {object} target
+ * @returns {object}
+ * @memberof Core
+ */
+export function extend(target) {
+ var sources = [].slice.call(arguments, 1);
+ sources.forEach(function (source) {
+ if(!source) return;
+ Object.getOwnPropertyNames(source).forEach(function(propName) {
+ Object.defineProperty(target, propName, Object.getOwnPropertyDescriptor(source, propName));
+ });
+ });
+ return target;
+}
+
+/**
+ * Fast quicksort insert for sorted array -- based on:
+ * http://stackoverflow.com/questions/1344500/efficient-way-to-insert-a-number-into-a-sorted-array-of-numbers
+ * @param {any} item
+ * @param {array} array
+ * @param {function} [compareFunction]
+ * @returns {number} location (in array)
+ * @memberof Core
+ */
+export function insert(item, array, compareFunction) {
+ var location = locationOf(item, array, compareFunction);
+ array.splice(location, 0, item);
+
+ return location;
+}
+
+/**
+ * Finds where something would fit into a sorted array
+ * @param {any} item
+ * @param {array} array
+ * @param {function} [compareFunction]
+ * @param {function} [_start]
+ * @param {function} [_end]
+ * @returns {number} location (in array)
+ * @memberof Core
+ */
+export function locationOf(item, array, compareFunction, _start, _end) {
+ var start = _start || 0;
+ var end = _end || array.length;
+ var pivot = parseInt(start + (end - start) / 2);
+ var compared;
+ if(!compareFunction){
+ compareFunction = function(a, b) {
+ if(a > b) return 1;
+ if(a < b) return -1;
+ if(a == b) return 0;
+ };
+ }
+ if(end-start <= 0) {
+ return pivot;
+ }
+
+ compared = compareFunction(array[pivot], item);
+ if(end-start === 1) {
+ return compared >= 0 ? pivot : pivot + 1;
+ }
+ if(compared === 0) {
+ return pivot;
+ }
+ if(compared === -1) {
+ return locationOf(item, array, compareFunction, pivot, end);
+ } else{
+ return locationOf(item, array, compareFunction, start, pivot);
+ }
+}
+
+/**
+ * Finds index of something in a sorted array
+ * Returns -1 if not found
+ * @param {any} item
+ * @param {array} array
+ * @param {function} [compareFunction]
+ * @param {function} [_start]
+ * @param {function} [_end]
+ * @returns {number} index (in array) or -1
+ * @memberof Core
+ */
+export function indexOfSorted(item, array, compareFunction, _start, _end) {
+ var start = _start || 0;
+ var end = _end || array.length;
+ var pivot = parseInt(start + (end - start) / 2);
+ var compared;
+ if(!compareFunction){
+ compareFunction = function(a, b) {
+ if(a > b) return 1;
+ if(a < b) return -1;
+ if(a == b) return 0;
+ };
+ }
+ if(end-start <= 0) {
+ return -1; // Not found
+ }
+
+ compared = compareFunction(array[pivot], item);
+ if(end-start === 1) {
+ return compared === 0 ? pivot : -1;
+ }
+ if(compared === 0) {
+ return pivot; // Found
+ }
+ if(compared === -1) {
+ return indexOfSorted(item, array, compareFunction, pivot, end);
+ } else{
+ return indexOfSorted(item, array, compareFunction, start, pivot);
+ }
+}
+/**
+ * Find the bounds of an element
+ * taking padding and margin into account
+ * @param {element} el
+ * @returns {{ width: Number, height: Number}}
+ * @memberof Core
+ */
+export function bounds(el) {
+
+ var style = window.getComputedStyle(el);
+ var widthProps = ["width", "paddingRight", "paddingLeft", "marginRight", "marginLeft", "borderRightWidth", "borderLeftWidth"];
+ var heightProps = ["height", "paddingTop", "paddingBottom", "marginTop", "marginBottom", "borderTopWidth", "borderBottomWidth"];
+
+ var width = 0;
+ var height = 0;
+
+ widthProps.forEach(function(prop){
+ width += parseFloat(style[prop]) || 0;
+ });
+
+ heightProps.forEach(function(prop){
+ height += parseFloat(style[prop]) || 0;
+ });
+
+ return {
+ height: height,
+ width: width
+ };
+
+}
+
+/**
+ * Find the bounds of an element
+ * taking padding, margin and borders into account
+ * @param {element} el
+ * @returns {{ width: Number, height: Number}}
+ * @memberof Core
+ */
+export function borders(el) {
+
+ var style = window.getComputedStyle(el);
+ var widthProps = ["paddingRight", "paddingLeft", "marginRight", "marginLeft", "borderRightWidth", "borderLeftWidth"];
+ var heightProps = ["paddingTop", "paddingBottom", "marginTop", "marginBottom", "borderTopWidth", "borderBottomWidth"];
+
+ var width = 0;
+ var height = 0;
+
+ widthProps.forEach(function(prop){
+ width += parseFloat(style[prop]) || 0;
+ });
+
+ heightProps.forEach(function(prop){
+ height += parseFloat(style[prop]) || 0;
+ });
+
+ return {
+ height: height,
+ width: width
+ };
+
+}
+
+/**
+ * Find the bounds of any node
+ * allows for getting bounds of text nodes by wrapping them in a range
+ * @param {node} node
+ * @returns {BoundingClientRect}
+ * @memberof Core
+ */
+export function nodeBounds(node) {
+ let elPos;
+ let doc = node.ownerDocument;
+ if(node.nodeType == Node.TEXT_NODE){
+ let elRange = doc.createRange();
+ elRange.selectNodeContents(node);
+ elPos = elRange.getBoundingClientRect();
+ } else {
+ elPos = node.getBoundingClientRect();
+ }
+ return elPos;
+}
+
+/**
+ * Find the equivelent of getBoundingClientRect of a browser window
+ * @returns {{ width: Number, height: Number, top: Number, left: Number, right: Number, bottom: Number }}
+ * @memberof Core
+ */
+export function windowBounds() {
+
+ var width = window.innerWidth;
+ var height = window.innerHeight;
+
+ return {
+ top: 0,
+ left: 0,
+ right: width,
+ bottom: height,
+ width: width,
+ height: height
+ };
+
+}
+
+/**
+ * Gets the index of a node in its parent
+ * @param {Node} node
+ * @param {string} typeId
+ * @return {number} index
+ * @memberof Core
+ */
+export function indexOfNode(node, typeId) {
+ var parent = node.parentNode;
+ var children = parent.childNodes;
+ var sib;
+ var index = -1;
+ for (var i = 0; i < children.length; i++) {
+ sib = children[i];
+ if (sib.nodeType === typeId) {
+ index++;
+ }
+ if (sib == node) break;
+ }
+
+ return index;
+}
+
+/**
+ * Gets the index of a text node in its parent
+ * @param {node} textNode
+ * @returns {number} index
+ * @memberof Core
+ */
+export function indexOfTextNode(textNode) {
+ return indexOfNode(textNode, TEXT_NODE);
+}
+
+/**
+ * Gets the index of an element node in its parent
+ * @param {element} elementNode
+ * @returns {number} index
+ * @memberof Core
+ */
+export function indexOfElementNode(elementNode) {
+ return indexOfNode(elementNode, ELEMENT_NODE);
+}
+
+/**
+ * Check if extension is xml
+ * @param {string} ext
+ * @returns {boolean}
+ * @memberof Core
+ */
+export function isXml(ext) {
+ return ["xml", "opf", "ncx"].indexOf(ext) > -1;
+}
+
+/**
+ * Create a new blob
+ * @param {any} content
+ * @param {string} mime
+ * @returns {Blob}
+ * @memberof Core
+ */
+export function createBlob(content, mime){
+ return new Blob([content], {type : mime });
+}
+
+/**
+ * Create a new blob url
+ * @param {any} content
+ * @param {string} mime
+ * @returns {string} url
+ * @memberof Core
+ */
+export function createBlobUrl(content, mime){
+ var tempUrl;
+ var blob = createBlob(content, mime);
+
+ tempUrl = _URL.createObjectURL(blob);
+
+ return tempUrl;
+}
+
+/**
+ * Remove a blob url
+ * @param {string} url
+ * @memberof Core
+ */
+export function revokeBlobUrl(url){
+ return _URL.revokeObjectURL(url);
+}
+
+/**
+ * Create a new base64 encoded url
+ * @param {any} content
+ * @param {string} mime
+ * @returns {string} url
+ * @memberof Core
+ */
+export function createBase64Url(content, mime){
+ var data;
+ var datauri;
+
+ if (typeof(content) !== "string") {
+ // Only handles strings
+ return;
+ }
+
+ data = btoa(encodeURIComponent(content));
+
+ datauri = "data:" + mime + ";base64," + data;
+
+ return datauri;
+}
+
+/**
+ * Get type of an object
+ * @param {object} obj
+ * @returns {string} type
+ * @memberof Core
+ */
+export function type(obj){
+ return Object.prototype.toString.call(obj).slice(8, -1);
+}
+
+/**
+ * Parse xml (or html) markup
+ * @param {string} markup
+ * @param {string} mime
+ * @param {boolean} forceXMLDom force using xmlDom to parse instead of native parser
+ * @returns {document} document
+ * @memberof Core
+ */
+export function parse(markup, mime, forceXMLDom) {
+ var doc;
+ var Parser;
+
+ if (typeof DOMParser === "undefined" || forceXMLDom) {
+ Parser = XMLDOMParser;
+ } else {
+ Parser = DOMParser;
+ }
+
+ // Remove byte order mark before parsing
+ // https://www.w3.org/International/questions/qa-byte-order-mark
+ if(markup.charCodeAt(0) === 0xFEFF) {
+ markup = markup.slice(1);
+ }
+
+ doc = new Parser().parseFromString(markup, mime);
+
+ return doc;
+}
+
+/**
+ * querySelector polyfill
+ * @param {element} el
+ * @param {string} sel selector string
+ * @returns {element} element
+ * @memberof Core
+ */
+export function qs(el, sel) {
+ var elements;
+ if (!el) {
+ throw new Error("No Element Provided");
+ }
+
+ if (typeof el.querySelector != "undefined") {
+ return el.querySelector(sel);
+ } else {
+ elements = el.getElementsByTagName(sel);
+ if (elements.length) {
+ return elements[0];
+ }
+ }
+}
+
+/**
+ * querySelectorAll polyfill
+ * @param {element} el
+ * @param {string} sel selector string
+ * @returns {element[]} elements
+ * @memberof Core
+ */
+export function qsa(el, sel) {
+
+ if (typeof el.querySelector != "undefined") {
+ return el.querySelectorAll(sel);
+ } else {
+ return el.getElementsByTagName(sel);
+ }
+}
+
+/**
+ * querySelector by property
+ * @param {element} el
+ * @param {string} sel selector string
+ * @param {object[]} props
+ * @returns {element[]} elements
+ * @memberof Core
+ */
+export function qsp(el, sel, props) {
+ var q, filtered;
+ if (typeof el.querySelector != "undefined") {
+ sel += "[";
+ for (var prop in props) {
+ sel += prop + "~='" + props[prop] + "'";
+ }
+ sel += "]";
+ return el.querySelector(sel);
+ } else {
+ q = el.getElementsByTagName(sel);
+ filtered = Array.prototype.slice.call(q, 0).filter(function(el) {
+ for (var prop in props) {
+ if(el.getAttribute(prop) === props[prop]){
+ return true;
+ }
+ }
+ return false;
+ });
+
+ if (filtered) {
+ return filtered[0];
+ }
+ }
+}
+
+/**
+ * Sprint through all text nodes in a document
+ * @memberof Core
+ * @param {element} root element to start with
+ * @param {function} func function to run on each element
+ */
+export function sprint(root, func) {
+ var doc = root.ownerDocument || root;
+ if (typeof(doc.createTreeWalker) !== "undefined") {
+ treeWalker(root, func, NodeFilter.SHOW_TEXT);
+ } else {
+ walk(root, function(node) {
+ if (node && node.nodeType === 3) { // Node.TEXT_NODE
+ func(node);
+ }
+ }, true);
+ }
+}
+
+/**
+ * Create a treeWalker
+ * @memberof Core
+ * @param {element} root element to start with
+ * @param {function} func function to run on each element
+ * @param {function | object} filter funtion or object to filter with
+ */
+export function treeWalker(root, func, filter) {
+ var treeWalker = document.createTreeWalker(root, filter, null, false);
+ let node;
+ while ((node = treeWalker.nextNode())) {
+ func(node);
+ }
+}
+
+/**
+ * @memberof Core
+ * @param {node} node
+ * @param {callback} return false for continue,true for break inside callback
+ */
+export function walk(node,callback){
+ if(callback(node)){
+ return true;
+ }
+ node = node.firstChild;
+ if(node){
+ do{
+ let walked = walk(node,callback);
+ if(walked){
+ return true;
+ }
+ node = node.nextSibling;
+ } while(node);
+ }
+}
+
+/**
+ * Convert a blob to a base64 encoded string
+ * @param {Blog} blob
+ * @returns {string}
+ * @memberof Core
+ */
+export function blob2base64(blob) {
+ return new Promise(function(resolve, reject) {
+ var reader = new FileReader();
+ reader.readAsDataURL(blob);
+ reader.onloadend = function() {
+ resolve(reader.result);
+ };
+ });
+}
+
+
+/**
+ * Creates a new pending promise and provides methods to resolve or reject it.
+ * From: https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/Promise.jsm/Deferred#backwards_forwards_compatible
+ * @memberof Core
+ */
+export function defer() {
+ /* A method to resolve the associated Promise with the value passed.
+ * If the promise is already settled it does nothing.
+ *
+ * @param {anything} value : This value is used to resolve the promise
+ * If the value is a Promise then the associated promise assumes the state
+ * of Promise passed as value.
+ */
+ this.resolve = null;
+
+ /* A method to reject the assocaited Promise with the value passed.
+ * If the promise is already settled it does nothing.
+ *
+ * @param {anything} reason: The reason for the rejection of the Promise.
+ * Generally its an Error object. If however a Promise is passed, then the Promise
+ * itself will be the reason for rejection no matter the state of the Promise.
+ */
+ this.reject = null;
+
+ this.id = uuid();
+
+ /* A newly created Pomise object.
+ * Initially in pending state.
+ */
+ this.promise = new Promise((resolve, reject) => {
+ this.resolve = resolve;
+ this.reject = reject;
+ });
+ Object.freeze(this);
+}
+
+/**
+ * querySelector with filter by epub type
+ * @param {element} html
+ * @param {string} element element type to find
+ * @param {string} type epub type to find
+ * @returns {element[]} elements
+ * @memberof Core
+ */
+export function querySelectorByType(html, element, type){
+ var query;
+ if (typeof html.querySelector != "undefined") {
+ query = html.querySelector(`${element}[*|type="${type}"]`);
+ }
+ // Handle IE not supporting namespaced epub:type in querySelector
+ if(!query || query.length === 0) {
+ query = qsa(html, element);
+ for (var i = 0; i < query.length; i++) {
+ if(query[i].getAttributeNS("http://www.idpf.org/2007/ops", "type") === type ||
+ query[i].getAttribute("epub:type") === type) {
+ return query[i];
+ }
+ }
+ } else {
+ return query;
+ }
+}
+
+/**
+ * Find direct decendents of an element
+ * @param {element} el
+ * @returns {element[]} children
+ * @memberof Core
+ */
+export function findChildren(el) {
+ var result = [];
+ var childNodes = el.childNodes;
+ for (var i = 0; i < childNodes.length; i++) {
+ let node = childNodes[i];
+ if (node.nodeType === 1) {
+ result.push(node);
+ }
+ }
+ return result;
+}
+
+/**
+ * Find all parents (ancestors) of an element
+ * @param {element} node
+ * @returns {element[]} parents
+ * @memberof Core
+ */
+export function parents(node) {
+ var nodes = [node];
+ for (; node; node = node.parentNode) {
+ nodes.unshift(node);
+ }
+ return nodes
+}
+
+/**
+ * Find all direct decendents of a specific type
+ * @param {element} el
+ * @param {string} nodeName
+ * @param {boolean} [single]
+ * @returns {element[]} children
+ * @memberof Core
+ */
+export function filterChildren(el, nodeName, single) {
+ var result = [];
+ var childNodes = el.childNodes;
+ for (var i = 0; i < childNodes.length; i++) {
+ let node = childNodes[i];
+ if (node.nodeType === 1 && node.nodeName.toLowerCase() === nodeName) {
+ if (single) {
+ return node;
+ } else {
+ result.push(node);
+ }
+ }
+ }
+ if (!single) {
+ return result;
+ }
+}
+
+/**
+ * Filter all parents (ancestors) with tag name
+ * @param {element} node
+ * @param {string} tagname
+ * @returns {element[]} parents
+ * @memberof Core
+ */
+export function getParentByTagName(node, tagname) {
+ let parent;
+ if (node === null || tagname === '') return;
+ parent = node.parentNode;
+ while (parent.nodeType === 1) {
+ if (parent.tagName.toLowerCase() === tagname) {
+ return parent;
+ }
+ parent = parent.parentNode;
+ }
+}
+
+/**
+ * Lightweight Polyfill for DOM Range
+ * @class
+ * @memberof Core
+ */
+export class RangeObject {
+ constructor() {
+ this.collapsed = false;
+ this.commonAncestorContainer = undefined;
+ this.endContainer = undefined;
+ this.endOffset = undefined;
+ this.startContainer = undefined;
+ this.startOffset = undefined;
+ }
+
+ setStart(startNode, startOffset) {
+ this.startContainer = startNode;
+ this.startOffset = startOffset;
+
+ if (!this.endContainer) {
+ this.collapse(true);
+ } else {
+ this.commonAncestorContainer = this._commonAncestorContainer();
+ }
+
+ this._checkCollapsed();
+ }
+
+ setEnd(endNode, endOffset) {
+ this.endContainer = endNode;
+ this.endOffset = endOffset;
+
+ if (!this.startContainer) {
+ this.collapse(false);
+ } else {
+ this.collapsed = false;
+ this.commonAncestorContainer = this._commonAncestorContainer();
+ }
+
+ this._checkCollapsed();
+ }
+
+ collapse(toStart) {
+ this.collapsed = true;
+ if (toStart) {
+ this.endContainer = this.startContainer;
+ this.endOffset = this.startOffset;
+ this.commonAncestorContainer = this.startContainer.parentNode;
+ } else {
+ this.startContainer = this.endContainer;
+ this.startOffset = this.endOffset;
+ this.commonAncestorContainer = this.endOffset.parentNode;
+ }
+ }
+
+ selectNode(referenceNode) {
+ let parent = referenceNode.parentNode;
+ let index = Array.prototype.indexOf.call(parent.childNodes, referenceNode);
+ this.setStart(parent, index);
+ this.setEnd(parent, index + 1);
+ }
+
+ selectNodeContents(referenceNode) {
+ let end = referenceNode.childNodes[referenceNode.childNodes - 1];
+ let endIndex = (referenceNode.nodeType === 3) ?
+ referenceNode.textContent.length : parent.childNodes.length;
+ this.setStart(referenceNode, 0);
+ this.setEnd(referenceNode, endIndex);
+ }
+
+ _commonAncestorContainer(startContainer, endContainer) {
+ var startParents = parents(startContainer || this.startContainer);
+ var endParents = parents(endContainer || this.endContainer);
+
+ if (startParents[0] != endParents[0]) return undefined;
+
+ for (var i = 0; i < startParents.length; i++) {
+ if (startParents[i] != endParents[i]) {
+ return startParents[i - 1];
+ }
+ }
+ }
+
+ _checkCollapsed() {
+ if (this.startContainer === this.endContainer &&
+ this.startOffset === this.endOffset) {
+ this.collapsed = true;
+ } else {
+ this.collapsed = false;
+ }
+ }
+
+ toString() {
+ // TODO: implement walking between start and end to find text
+ }
+}
diff --git a/lib/epub.js/src/utils/hook.js b/lib/epub.js/src/utils/hook.js
new file mode 100644
index 0000000..ea1b901
--- /dev/null
+++ b/lib/epub.js/src/utils/hook.js
@@ -0,0 +1,82 @@
+/**
+ * Hooks allow for injecting functions that must all complete in order before finishing
+ * They will execute in parallel but all must finish before continuing
+ * Functions may return a promise if they are asycn.
+ * @param {any} context scope of this
+ * @example this.content = new EPUBJS.Hook(this);
+ */
+class Hook {
+ constructor(context){
+ this.context = context || this;
+ this.hooks = [];
+ }
+
+ /**
+ * Adds a function to be run before a hook completes
+ * @example this.content.register(function(){...});
+ */
+ register(){
+ for(var i = 0; i < arguments.length; ++i) {
+ if (typeof arguments[i] === "function") {
+ this.hooks.push(arguments[i]);
+ } else {
+ // unpack array
+ for(var j = 0; j < arguments[i].length; ++j) {
+ this.hooks.push(arguments[i][j]);
+ }
+ }
+ }
+ }
+
+ /**
+ * Removes a function
+ * @example this.content.deregister(function(){...});
+ */
+ deregister(func){
+ let hook;
+ for (let i = 0; i < this.hooks.length; i++) {
+ hook = this.hooks[i];
+ if (hook === func) {
+ this.hooks.splice(i, 1);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Triggers a hook to run all functions
+ * @example this.content.trigger(args).then(function(){...});
+ */
+ trigger(){
+ var args = arguments;
+ var context = this.context;
+ var promises = [];
+
+ this.hooks.forEach(function(task) {
+ try {
+ var executing = task.apply(context, args);
+ } catch (err) {
+ console.log(err);
+ }
+
+ if(executing && typeof executing["then"] === "function") {
+ // Task is a function that returns a promise
+ promises.push(executing);
+ }
+ // Otherwise Task resolves immediately, continue
+ });
+
+
+ return Promise.all(promises);
+ }
+
+ // Adds a function to be run before a hook completes
+ list(){
+ return this.hooks;
+ }
+
+ clear(){
+ return this.hooks = [];
+ }
+}
+export default Hook;
diff --git a/lib/epub.js/src/utils/mime.js b/lib/epub.js/src/utils/mime.js
new file mode 100644
index 0000000..8f4cca3
--- /dev/null
+++ b/lib/epub.js/src/utils/mime.js
@@ -0,0 +1,169 @@
+/*
+ From Zip.js, by Gildas Lormeau
+edited down
+ */
+
+var table = {
+ "application" : {
+ "ecmascript" : [ "es", "ecma" ],
+ "javascript" : "js",
+ "ogg" : "ogx",
+ "pdf" : "pdf",
+ "postscript" : [ "ps", "ai", "eps", "epsi", "epsf", "eps2", "eps3" ],
+ "rdf+xml" : "rdf",
+ "smil" : [ "smi", "smil" ],
+ "xhtml+xml" : [ "xhtml", "xht" ],
+ "xml" : [ "xml", "xsl", "xsd", "opf", "ncx" ],
+ "zip" : "zip",
+ "x-httpd-eruby" : "rhtml",
+ "x-latex" : "latex",
+ "x-maker" : [ "frm", "maker", "frame", "fm", "fb", "book", "fbdoc" ],
+ "x-object" : "o",
+ "x-shockwave-flash" : [ "swf", "swfl" ],
+ "x-silverlight" : "scr",
+ "epub+zip" : "epub",
+ "font-tdpfr" : "pfr",
+ "inkml+xml" : [ "ink", "inkml" ],
+ "json" : "json",
+ "jsonml+json" : "jsonml",
+ "mathml+xml" : "mathml",
+ "metalink+xml" : "metalink",
+ "mp4" : "mp4s",
+ // "oebps-package+xml" : "opf",
+ "omdoc+xml" : "omdoc",
+ "oxps" : "oxps",
+ "vnd.amazon.ebook" : "azw",
+ "widget" : "wgt",
+ // "x-dtbncx+xml" : "ncx",
+ "x-dtbook+xml" : "dtb",
+ "x-dtbresource+xml" : "res",
+ "x-font-bdf" : "bdf",
+ "x-font-ghostscript" : "gsf",
+ "x-font-linux-psf" : "psf",
+ "x-font-otf" : "otf",
+ "x-font-pcf" : "pcf",
+ "x-font-snf" : "snf",
+ "x-font-ttf" : [ "ttf", "ttc" ],
+ "x-font-type1" : [ "pfa", "pfb", "pfm", "afm" ],
+ "x-font-woff" : "woff",
+ "x-mobipocket-ebook" : [ "prc", "mobi" ],
+ "x-mspublisher" : "pub",
+ "x-nzb" : "nzb",
+ "x-tgif" : "obj",
+ "xaml+xml" : "xaml",
+ "xml-dtd" : "dtd",
+ "xproc+xml" : "xpl",
+ "xslt+xml" : "xslt",
+ "internet-property-stream" : "acx",
+ "x-compress" : "z",
+ "x-compressed" : "tgz",
+ "x-gzip" : "gz",
+ },
+ "audio" : {
+ "flac" : "flac",
+ "midi" : [ "mid", "midi", "kar", "rmi" ],
+ "mpeg" : [ "mpga", "mpega", "mp2", "mp3", "m4a", "mp2a", "m2a", "m3a" ],
+ "mpegurl" : "m3u",
+ "ogg" : [ "oga", "ogg", "spx" ],
+ "x-aiff" : [ "aif", "aiff", "aifc" ],
+ "x-ms-wma" : "wma",
+ "x-wav" : "wav",
+ "adpcm" : "adp",
+ "mp4" : "mp4a",
+ "webm" : "weba",
+ "x-aac" : "aac",
+ "x-caf" : "caf",
+ "x-matroska" : "mka",
+ "x-pn-realaudio-plugin" : "rmp",
+ "xm" : "xm",
+ "mid" : [ "mid", "rmi" ]
+ },
+ "image" : {
+ "gif" : "gif",
+ "ief" : "ief",
+ "jpeg" : [ "jpeg", "jpg", "jpe" ],
+ "pcx" : "pcx",
+ "png" : "png",
+ "svg+xml" : [ "svg", "svgz" ],
+ "tiff" : [ "tiff", "tif" ],
+ "x-icon" : "ico",
+ "bmp" : "bmp",
+ "webp" : "webp",
+ "x-pict" : [ "pic", "pct" ],
+ "x-tga" : "tga",
+ "cis-cod" : "cod"
+ },
+ "text" : {
+ "cache-manifest" : [ "manifest", "appcache" ],
+ "css" : "css",
+ "csv" : "csv",
+ "html" : [ "html", "htm", "shtml", "stm" ],
+ "mathml" : "mml",
+ "plain" : [ "txt", "text", "brf", "conf", "def", "list", "log", "in", "bas" ],
+ "richtext" : "rtx",
+ "tab-separated-values" : "tsv",
+ "x-bibtex" : "bib"
+ },
+ "video" : {
+ "mpeg" : [ "mpeg", "mpg", "mpe", "m1v", "m2v", "mp2", "mpa", "mpv2" ],
+ "mp4" : [ "mp4", "mp4v", "mpg4" ],
+ "quicktime" : [ "qt", "mov" ],
+ "ogg" : "ogv",
+ "vnd.mpegurl" : [ "mxu", "m4u" ],
+ "x-flv" : "flv",
+ "x-la-asf" : [ "lsf", "lsx" ],
+ "x-mng" : "mng",
+ "x-ms-asf" : [ "asf", "asx", "asr" ],
+ "x-ms-wm" : "wm",
+ "x-ms-wmv" : "wmv",
+ "x-ms-wmx" : "wmx",
+ "x-ms-wvx" : "wvx",
+ "x-msvideo" : "avi",
+ "x-sgi-movie" : "movie",
+ "x-matroska" : [ "mpv", "mkv", "mk3d", "mks" ],
+ "3gpp2" : "3g2",
+ "h261" : "h261",
+ "h263" : "h263",
+ "h264" : "h264",
+ "jpeg" : "jpgv",
+ "jpm" : [ "jpm", "jpgm" ],
+ "mj2" : [ "mj2", "mjp2" ],
+ "vnd.ms-playready.media.pyv" : "pyv",
+ "vnd.uvvu.mp4" : [ "uvu", "uvvu" ],
+ "vnd.vivo" : "viv",
+ "webm" : "webm",
+ "x-f4v" : "f4v",
+ "x-m4v" : "m4v",
+ "x-ms-vob" : "vob",
+ "x-smv" : "smv"
+ }
+};
+
+var mimeTypes = (function() {
+ var type, subtype, val, index, mimeTypes = {};
+ for (type in table) {
+ if (table.hasOwnProperty(type)) {
+ for (subtype in table[type]) {
+ if (table[type].hasOwnProperty(subtype)) {
+ val = table[type][subtype];
+ if (typeof val == "string") {
+ mimeTypes[val] = type + "/" + subtype;
+ } else {
+ for (index = 0; index < val.length; index++) {
+ mimeTypes[val[index]] = type + "/" + subtype;
+ }
+ }
+ }
+ }
+ }
+ }
+ return mimeTypes;
+})();
+
+var defaultValue = "text/plain";//"application/octet-stream";
+
+function lookup(filename) {
+ return filename && mimeTypes[filename.split(".").pop().toLowerCase()] || defaultValue;
+};
+
+export default { lookup };
diff --git a/lib/epub.js/src/utils/path.js b/lib/epub.js/src/utils/path.js
new file mode 100644
index 0000000..6a060cb
--- /dev/null
+++ b/lib/epub.js/src/utils/path.js
@@ -0,0 +1,102 @@
+import path from "path-webpack";
+
+/**
+ * Creates a Path object for parsing and manipulation of a path strings
+ *
+ * Uses a polyfill for Nodejs path: https://nodejs.org/api/path.html
+ * @param {string} pathString a url string (relative or absolute)
+ * @class
+ */
+class Path {
+ constructor(pathString) {
+ var protocol;
+ var parsed;
+
+ protocol = pathString.indexOf("://");
+ if (protocol > -1) {
+ pathString = new URL(pathString).pathname;
+ }
+
+ parsed = this.parse(pathString);
+
+ this.path = pathString;
+
+ if (this.isDirectory(pathString)) {
+ this.directory = pathString;
+ } else {
+ this.directory = parsed.dir + "/";
+ }
+
+ this.filename = parsed.base;
+ this.extension = parsed.ext.slice(1);
+
+ }
+
+ /**
+ * Parse the path: https://nodejs.org/api/path.html#path_path_parse_path
+ * @param {string} what
+ * @returns {object}
+ */
+ parse (what) {
+ return path.parse(what);
+ }
+
+ /**
+ * @param {string} what
+ * @returns {boolean}
+ */
+ isAbsolute (what) {
+ return path.isAbsolute(what || this.path);
+ }
+
+ /**
+ * Check if path ends with a directory
+ * @param {string} what
+ * @returns {boolean}
+ */
+ isDirectory (what) {
+ return (what.charAt(what.length-1) === "/");
+ }
+
+ /**
+ * Resolve a path against the directory of the Path
+ *
+ * https://nodejs.org/api/path.html#path_path_resolve_paths
+ * @param {string} what
+ * @returns {string} resolved
+ */
+ resolve (what) {
+ return path.resolve(this.directory, what);
+ }
+
+ /**
+ * Resolve a path relative to the directory of the Path
+ *
+ * https://nodejs.org/api/path.html#path_path_relative_from_to
+ * @param {string} what
+ * @returns {string} relative
+ */
+ relative (what) {
+ var isAbsolute = what && (what.indexOf("://") > -1);
+
+ if (isAbsolute) {
+ return what;
+ }
+
+ return path.relative(this.directory, what);
+ }
+
+ splitPath(filename) {
+ return this.splitPathRe.exec(filename).slice(1);
+ }
+
+ /**
+ * Return the path string
+ * @returns {string} path
+ */
+ toString () {
+ return this.path;
+ }
+}
+
+export default Path;
diff --git a/lib/epub.js/src/utils/queue.js b/lib/epub.js/src/utils/queue.js
new file mode 100644
index 0000000..1f8a18a
--- /dev/null
+++ b/lib/epub.js/src/utils/queue.js
@@ -0,0 +1,246 @@
+import {defer, requestAnimationFrame} from "./core";
+
+/**
+ * Queue for handling tasks one at a time
+ * @class
+ * @param {scope} context what this will resolve to in the tasks
+ */
+class Queue {
+ constructor(context){
+ this._q = [];
+ this.context = context;
+ this.tick = requestAnimationFrame;
+ this.running = false;
+ this.paused = false;
+ }
+
+ /**
+ * Add an item to the queue
+ * @return {Promise}
+ */
+ enqueue() {
+ var deferred, promise;
+ var queued;
+ var task = [].shift.call(arguments);
+ var args = arguments;
+
+ // Handle single args without context
+ // if(args && !Array.isArray(args)) {
+ // args = [args];
+ // }
+ if(!task) {
+ throw new Error("No Task Provided");
+ }
+
+ if(typeof task === "function"){
+
+ deferred = new defer();
+ promise = deferred.promise;
+
+ queued = {
+ "task" : task,
+ "args" : args,
+ //"context" : context,
+ "deferred" : deferred,
+ "promise" : promise
+ };
+
+ } else {
+ // Task is a promise
+ queued = {
+ "promise" : task
+ };
+
+ }
+
+ this._q.push(queued);
+
+ // Wait to start queue flush
+ if (this.paused == false && !this.running) {
+ // setTimeout(this.flush.bind(this), 0);
+ // this.tick.call(window, this.run.bind(this));
+ this.run();
+ }
+
+ return queued.promise;
+ }
+
+ /**
+ * Run one item
+ * @return {Promise}
+ */
+ dequeue(){
+ var inwait, task, result;
+
+ if(this._q.length && !this.paused) {
+ inwait = this._q.shift();
+ task = inwait.task;
+ if(task){
+ // console.log(task)
+
+ result = task.apply(this.context, inwait.args);
+
+ if(result && typeof result["then"] === "function") {
+ // Task is a function that returns a promise
+ return result.then(function(){
+ inwait.deferred.resolve.apply(this.context, arguments);
+ }.bind(this), function() {
+ inwait.deferred.reject.apply(this.context, arguments);
+ }.bind(this));
+ } else {
+ // Task resolves immediately
+ inwait.deferred.resolve.apply(this.context, result);
+ return inwait.promise;
+ }
+
+
+
+ } else if(inwait.promise) {
+ // Task is a promise
+ return inwait.promise;
+ }
+
+ } else {
+ inwait = new defer();
+ inwait.deferred.resolve();
+ return inwait.promise;
+ }
+
+ }
+
+ // Run All Immediately
+ dump(){
+ while(this._q.length) {
+ this.dequeue();
+ }
+ }
+
+ /**
+ * Run all tasks sequentially, at convince
+ * @return {Promise}
+ */
+ run(){
+
+ if(!this.running){
+ this.running = true;
+ this.defered = new defer();
+ }
+
+ this.tick.call(window, () => {
+
+ if(this._q.length) {
+
+ this.dequeue()
+ .then(function(){
+ this.run();
+ }.bind(this));
+
+ } else {
+ this.defered.resolve();
+ this.running = undefined;
+ }
+
+ });
+
+ // Unpause
+ if(this.paused == true) {
+ this.paused = false;
+ }
+
+ return this.defered.promise;
+ }
+
+ /**
+ * Flush all, as quickly as possible
+ * @return {Promise}
+ */
+ flush(){
+
+ if(this.running){
+ return this.running;
+ }
+
+ if(this._q.length) {
+ this.running = this.dequeue()
+ .then(function(){
+ this.running = undefined;
+ return this.flush();
+ }.bind(this));
+
+ return this.running;
+ }
+
+ }
+
+ /**
+ * Clear all items in wait
+ */
+ clear(){
+ this._q = [];
+ }
+
+ /**
+ * Get the number of tasks in the queue
+ * @return {number} tasks
+ */
+ length(){
+ return this._q.length;
+ }
+
+ /**
+ * Pause a running queue
+ */
+ pause(){
+ this.paused = true;
+ }
+
+ /**
+ * End the queue
+ */
+ stop(){
+ this._q = [];
+ this.running = false;
+ this.paused = true;
+ }
+}
+
+
+/**
+ * Create a new task from a callback
+ * @class
+ * @private
+ * @param {function} task
+ * @param {array} args
+ * @param {scope} context
+ * @return {function} task
+ */
+class Task {
+ constructor(task, args, context){
+
+ return function(){
+ var toApply = arguments || [];
+
+ return new Promise( (resolve, reject) => {
+ var callback = function(value, err){
+ if (!value && err) {
+ reject(err);
+ } else {
+ resolve(value);
+ }
+ };
+ // Add the callback to the arguments list
+ toApply.push(callback);
+
+ // Apply all arguments to the functions
+ task.apply(context || this, toApply);
+
+ });
+
+ };
+
+ }
+}
+
+
+export default Queue;
+export { Task };
diff --git a/lib/epub.js/src/utils/replacements.js b/lib/epub.js/src/utils/replacements.js
new file mode 100644
index 0000000..a271088
--- /dev/null
+++ b/lib/epub.js/src/utils/replacements.js
@@ -0,0 +1,138 @@
+import { qs, qsa } from "./core";
+import Url from "./url";
+import Path from "./path";
+
+export function replaceBase(doc, section){
+ var base;
+ var head;
+ var url = section.url;
+ var absolute = (url.indexOf("://") > -1);
+
+ if(!doc){
+ return;
+ }
+
+ head = qs(doc, "head");
+ base = qs(head, "base");
+
+ if(!base) {
+ base = doc.createElement("base");
+ head.insertBefore(base, head.firstChild);
+ }
+
+ // Fix for Safari crashing if the url doesn't have an origin
+ if (!absolute && window && window.location) {
+ url = window.location.origin + url;
+ }
+
+ base.setAttribute("href", url);
+}
+
+export function replaceCanonical(doc, section){
+ var head;
+ var link;
+ var url = section.canonical;
+
+ if(!doc){
+ return;
+ }
+
+ head = qs(doc, "head");
+ link = qs(head, "link[rel='canonical']");
+
+ if (link) {
+ link.setAttribute("href", url);
+ } else {
+ link = doc.createElement("link");
+ link.setAttribute("rel", "canonical");
+ link.setAttribute("href", url);
+ head.appendChild(link);
+ }
+}
+
+export function replaceMeta(doc, section){
+ var head;
+ var meta;
+ var id = section.idref;
+ if(!doc){
+ return;
+ }
+
+ head = qs(doc, "head");
+ meta = qs(head, "link[property='dc.identifier']");
+
+ if (meta) {
+ meta.setAttribute("content", id);
+ } else {
+ meta = doc.createElement("meta");
+ meta.setAttribute("name", "dc.identifier");
+ meta.setAttribute("content", id);
+ head.appendChild(meta);
+ }
+}
+
+// TODO: move me to Contents
+export function replaceLinks(contents, fn) {
+
+ var links = contents.querySelectorAll("a[href]");
+
+ if (!links.length) {
+ return;
+ }
+
+ var base = qs(contents.ownerDocument, "base");
+ var location = base ? base.getAttribute("href") : undefined;
+ var replaceLink = function(link){
+ var href = link.getAttribute("href");
+
+ if(href.indexOf("mailto:") === 0){
+ return;
+ }
+
+ var absolute = (href.indexOf("://") > -1);
+
+ if(absolute){
+
+ link.setAttribute("target", "_blank");
+
+ }else{
+ var linkUrl;
+ try {
+ linkUrl = new Url(href, location);
+ } catch(error) {
+ // NOOP
+ }
+
+ link.onclick = function(){
+
+ if(linkUrl && linkUrl.hash) {
+ fn(linkUrl.Path.path + linkUrl.hash);
+ } else if(linkUrl){
+ fn(linkUrl.Path.path);
+ } else {
+ fn(href);
+ }
+
+ return false;
+ };
+ }
+ }.bind(this);
+
+ for (var i = 0; i < links.length; i++) {
+ replaceLink(links[i]);
+ }
+
+
+}
+
+export function substitute(content, urls, replacements) {
+ urls.forEach(function(url, i){
+ if (url && replacements[i]) {
+ // Account for special characters in the file name.
+ // See https://stackoverflow.com/a/6318729.
+ url = url.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
+ content = content.replace(new RegExp(url, "g"), replacements[i]);
+ }
+ });
+ return content;
+}
diff --git a/lib/epub.js/src/utils/request.js b/lib/epub.js/src/utils/request.js
new file mode 100644
index 0000000..0de3b86
--- /dev/null
+++ b/lib/epub.js/src/utils/request.js
@@ -0,0 +1,150 @@
+import {defer, isXml, parse} from "./core";
+import Path from "./path";
+
+function request(url, type, withCredentials, headers) {
+ var supportsURL = (typeof window != "undefined") ? window.URL : false; // TODO: fallback for url if window isn't defined
+ var BLOB_RESPONSE = supportsURL ? "blob" : "arraybuffer";
+
+ var deferred = new defer();
+
+ var xhr = new XMLHttpRequest();
+
+ //-- Check from PDF.js:
+ // https://github.com/mozilla/pdf.js/blob/master/web/compatibility.js
+ var xhrPrototype = XMLHttpRequest.prototype;
+
+ var header;
+
+ if (!("overrideMimeType" in xhrPrototype)) {
+ // IE10 might have response, but not overrideMimeType
+ Object.defineProperty(xhrPrototype, "overrideMimeType", {
+ value: function xmlHttpRequestOverrideMimeType() {}
+ });
+ }
+
+ if(withCredentials) {
+ xhr.withCredentials = true;
+ }
+
+ xhr.onreadystatechange = handler;
+ xhr.onerror = err;
+
+ xhr.open("GET", url, true);
+
+ for(header in headers) {
+ xhr.setRequestHeader(header, headers[header]);
+ }
+
+ if(type == "json") {
+ xhr.setRequestHeader("Accept", "application/json");
+ }
+
+ // If type isn"t set, determine it from the file extension
+ if(!type) {
+ type = new Path(url).extension;
+ }
+
+ if(type == "blob"){
+ xhr.responseType = BLOB_RESPONSE;
+ }
+
+
+ if(isXml(type)) {
+ // xhr.responseType = "document";
+ xhr.overrideMimeType("text/xml"); // for OPF parsing
+ }
+
+ if(type == "xhtml") {
+ // xhr.responseType = "document";
+ }
+
+ if(type == "html" || type == "htm") {
+ // xhr.responseType = "document";
+ }
+
+ if(type == "binary") {
+ xhr.responseType = "arraybuffer";
+ }
+
+ xhr.send();
+
+ function err(e) {
+ deferred.reject(e);
+ }
+
+ function handler() {
+ if (this.readyState === XMLHttpRequest.DONE) {
+ var responseXML = false;
+
+ if(this.responseType === "" || this.responseType === "document") {
+ responseXML = this.responseXML;
+ }
+
+ if (this.status === 200 || this.status === 0 || responseXML) { //-- Firefox is reporting 0 for blob urls
+ var r;
+
+ if (!this.response && !responseXML) {
+ deferred.reject({
+ status: this.status,
+ message : "Empty Response",
+ stack : new Error().stack
+ });
+ return deferred.promise;
+ }
+
+ if (this.status === 403) {
+ deferred.reject({
+ status: this.status,
+ response: this.response,
+ message : "Forbidden",
+ stack : new Error().stack
+ });
+ return deferred.promise;
+ }
+ if(responseXML){
+ r = this.responseXML;
+ } else
+ if(isXml(type)){
+ // xhr.overrideMimeType("text/xml"); // for OPF parsing
+ // If this.responseXML wasn't set, try to parse using a DOMParser from text
+ r = parse(this.response, "text/xml");
+ }else
+ if(type == "xhtml"){
+ r = parse(this.response, "application/xhtml+xml");
+ }else
+ if(type == "html" || type == "htm"){
+ r = parse(this.response, "text/html");
+ }else
+ if(type == "json"){
+ r = JSON.parse(this.response);
+ }else
+ if(type == "blob"){
+
+ if(supportsURL) {
+ r = this.response;
+ } else {
+ //-- Safari doesn't support responseType blob, so create a blob from arraybuffer
+ r = new Blob([this.response]);
+ }
+
+ }else{
+ r = this.response;
+ }
+
+ deferred.resolve(r);
+ } else {
+
+ deferred.reject({
+ status: this.status,
+ message : this.response,
+ stack : new Error().stack
+ });
+
+ }
+ }
+ }
+
+ return deferred.promise;
+}
+
+export default request;
diff --git a/lib/epub.js/src/utils/scrolltype.js b/lib/epub.js/src/utils/scrolltype.js
new file mode 100644
index 0000000..7d2e47b
--- /dev/null
+++ b/lib/epub.js/src/utils/scrolltype.js
@@ -0,0 +1,55 @@
+// Detect RTL scroll type
+// Based on https://github.com/othree/jquery.rtl-scroll-type/blob/master/src/jquery.rtl-scroll.js
+export default function scrollType() {
+ var type = "reverse";
+ var definer = createDefiner();
+ document.body.appendChild(definer);
+
+ if (definer.scrollLeft > 0) {
+ type = "default";
+ } else {
+ if (typeof Element !== 'undefined' && Element.prototype.scrollIntoView) {
+ definer.children[0].children[1].scrollIntoView();
+ if (definer.scrollLeft < 0) {
+ type = "negative";
+ }
+ } else {
+ definer.scrollLeft = 1;
+ if (definer.scrollLeft === 0) {
+ type = "negative";
+ }
+ }
+ }
+
+ document.body.removeChild(definer);
+ return type;
+}
+
+export function createDefiner() {
+ var definer = document.createElement('div');
+ definer.dir="rtl";
+
+ definer.style.position = "fixed";
+ definer.style.width = "1px";
+ definer.style.height = "1px";
+ definer.style.top = "0px";
+ definer.style.left = "0px";
+ definer.style.overflow = "hidden";
+
+ var innerDiv = document.createElement('div');
+ innerDiv.style.width = "2px";
+
+ var spanA = document.createElement('span');
+ spanA.style.width = "1px";
+ spanA.style.display = "inline-block";
+
+ var spanB = document.createElement('span');
+ spanB.style.width = "1px";
+ spanB.style.display = "inline-block";
+
+ innerDiv.appendChild(spanA);
+ innerDiv.appendChild(spanB);
+ definer.appendChild(innerDiv);
+
+ return definer;
+}
diff --git a/lib/epub.js/src/utils/url.js b/lib/epub.js/src/utils/url.js
new file mode 100644
index 0000000..3cc8c04
--- /dev/null
+++ b/lib/epub.js/src/utils/url.js
@@ -0,0 +1,108 @@
+import Path from "./path";
+import path from "path-webpack";
+
+/**
+ * creates a Url object for parsing and manipulation of a url string
+ * @param {string} urlString a url string (relative or absolute)
+ * @param {string} [baseString] optional base for the url,
+ * default to window.location.href
+ */
+class Url {
+ constructor(urlString, baseString) {
+ var absolute = (urlString.indexOf("://") > -1);
+ var pathname = urlString;
+ var basePath;
+
+ this.Url = undefined;
+ this.href = urlString;
+ this.protocol = "";
+ this.origin = "";
+ this.hash = "";
+ this.hash = "";
+ this.search = "";
+ this.base = baseString;
+
+ if (!absolute &&
+ baseString !== false &&
+ typeof(baseString) !== "string" &&
+ window && window.location) {
+ this.base = window.location.href;
+ }
+
+ // URL Polyfill doesn't throw an error if base is empty
+ if (absolute || this.base) {
+ try {
+ if (this.base) { // Safari doesn't like an undefined base
+ this.Url = new URL(urlString, this.base);
+ } else {
+ this.Url = new URL(urlString);
+ }
+ this.href = this.Url.href;
+
+ this.protocol = this.Url.protocol;
+ this.origin = this.Url.origin;
+ this.hash = this.Url.hash;
+ this.search = this.Url.search;
+
+ pathname = this.Url.pathname + (this.Url.search ? this.Url.search : '');
+ } catch (e) {
+ // Skip URL parsing
+ this.Url = undefined;
+ // resolve the pathname from the base
+ if (this.base) {
+ basePath = new Path(this.base);
+ pathname = basePath.resolve(pathname);
+ }
+ }
+ }
+
+ this.Path = new Path(pathname);
+
+ this.directory = this.Path.directory;
+ this.filename = this.Path.filename;
+ this.extension = this.Path.extension;
+
+ }
+
+ /**
+ * @returns {Path}
+ */
+ path () {
+ return this.Path;
+ }
+
+ /**
+ * Resolves a relative path to a absolute url
+ * @param {string} what
+ * @returns {string} url
+ */
+ resolve (what) {
+ var isAbsolute = (what.indexOf("://") > -1);
+ var fullpath;
+
+ if (isAbsolute) {
+ return what;
+ }
+
+ fullpath = path.resolve(this.directory, what);
+ return this.origin + fullpath;
+ }
+
+ /**
+ * Resolve a path relative to the url
+ * @param {string} what
+ * @returns {string} path
+ */
+ relative (what) {
+ return path.relative(what, this.directory);
+ }
+
+ /**
+ * @returns {string}
+ */
+ toString () {
+ return this.href;
+ }
+}
+
+export default Url;