text: /**
   * 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
  	}
  }
  