diff options
Diffstat (limited to 'lib/epub.js/src/utils')
-rw-r--r-- | lib/epub.js/src/utils/constants.js | 62 | ||||
-rw-r--r-- | lib/epub.js/src/utils/core.js | 876 | ||||
-rw-r--r-- | lib/epub.js/src/utils/hook.js | 82 | ||||
-rw-r--r-- | lib/epub.js/src/utils/mime.js | 169 | ||||
-rw-r--r-- | lib/epub.js/src/utils/path.js | 102 | ||||
-rw-r--r-- | lib/epub.js/src/utils/queue.js | 246 | ||||
-rw-r--r-- | lib/epub.js/src/utils/replacements.js | 138 | ||||
-rw-r--r-- | lib/epub.js/src/utils/request.js | 150 | ||||
-rw-r--r-- | lib/epub.js/src/utils/scrolltype.js | 55 | ||||
-rw-r--r-- | lib/epub.js/src/utils/url.js | 108 |
10 files changed, 1988 insertions, 0 deletions
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; |