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;