epubjs
Version:
Parse and Render Epubs
507 lines (403 loc) • 12.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _core = require("./utils/core");
var _queue = _interopRequireDefault(require("./utils/queue"));
var _epubcfi = _interopRequireDefault(require("./epubcfi"));
var _constants = require("./utils/constants");
var _eventEmitter = _interopRequireDefault(require("event-emitter"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/**
* 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.default(this);
this.epubcfi = new _epubcfi.default();
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 {Promise<Array<string>>} 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 _core.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 = (0, _core.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.default(range, cfiBase).toString();
locations.push(cfi);
counter = 0;
}
}
prev = node;
};
(0, _core.sprint)(body, parser.bind(this)); // Close remaining
if (range && range.startContainer && prev) {
range.endContainer = prev;
range.endOffset = prev.length;
let cfi = new _epubcfi.default(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.default(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 _core.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 = (0, _core.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.default(node, cfiBase);
locations.push({
cfi: cfi.toString(),
wordCount: this._wordCounter
});
this._wordCounter = 0;
}
}
prev = node;
};
(0, _core.sprint)(body, parser.bind(this));
return locations;
}
/**
* Get a location from an EpubCFI
* @param {EpubCFI} cfi
* @return {number}
*/
locationFromCfi(cfi) {
let loc;
if (_epubcfi.default.prototype.isCfiString(cfi)) {
cfi = new _epubcfi.default(cfi);
} // Check if the location has not been set yet
if (this._locations.length === 0) {
return -1;
}
loc = (0, _core.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.default(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(_constants.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);
}
}
(0, _eventEmitter.default)(Locations.prototype);
var _default = Locations;
exports.default = _default;