From 4fd9b8f2b5a98bfcde57970b48fed2488a80f356 Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Fri, 17 Sep 2021 21:53:37 +0300 Subject: add in master snapshot of epubjs --- lib/epub.js/src/locations.js | 501 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 501 insertions(+) create mode 100644 lib/epub.js/src/locations.js (limited to 'lib/epub.js/src/locations.js') 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; -- cgit v1.2.3