epubjs
Version:
Parse and Render Epubs
351 lines (288 loc) • 9.61 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _core = require("./utils/core");
var _epubcfi = _interopRequireDefault(require("./epubcfi"));
var _hook = _interopRequireDefault(require("./utils/hook"));
var _replacements = require("./utils/replacements");
var _request2 = _interopRequireDefault(require("./utils/request"));
var _xmldom = require("@xmldom/xmldom");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/**
* Represents a Section of the Book
*
* In most books this is equivalent to a Chapter
* @param {object} item The spine item representing the section
* @param {object} hooks hooks for serialize and content
*/
class Section {
constructor(item, hooks) {
this.idref = item.idref;
this.linear = item.linear === "yes";
this.properties = item.properties;
this.index = item.index;
this.href = item.href;
this.url = item.url;
this.canonical = item.canonical;
this.next = item.next;
this.prev = item.prev;
this.cfiBase = item.cfiBase;
if (hooks) {
this.hooks = hooks;
} else {
this.hooks = {};
this.hooks.serialize = new _hook.default(this);
this.hooks.content = new _hook.default(this);
}
this.document = undefined;
this.contents = undefined;
this.output = undefined;
}
/**
* Load the section from its url
* @param {method} [_request] a request method to use for loading
* @return {document} a promise with the xml document
*/
load(_request) {
var request = _request || this.request || _request2.default;
var loading = new _core.defer();
var loaded = loading.promise;
if (this.contents) {
loading.resolve(this.contents);
} else {
request(this.url).then(function (xml) {
// var directory = new Url(this.url).directory;
this.document = xml;
this.contents = xml.documentElement;
return this.hooks.content.trigger(this.document, this);
}.bind(this)).then(function () {
loading.resolve(this.contents);
}.bind(this)).catch(function (error) {
loading.reject(error);
});
}
return loaded;
}
/**
* Adds a base tag for resolving urls in the section
* @private
*/
base() {
return (0, _replacements.replaceBase)(this.document, this);
}
/**
* Render the contents of a section
* @param {method} [_request] a request method to use for loading
* @return {string} output a serialized XML Document
*/
render(_request) {
var rendering = new _core.defer();
var rendered = rendering.promise;
this.output; // TODO: better way to return this from hooks?
this.load(_request).then(function (contents) {
var userAgent = typeof navigator !== 'undefined' && navigator.userAgent || '';
var isIE = userAgent.indexOf('Trident') >= 0;
var Serializer;
if (typeof XMLSerializer === "undefined" || isIE) {
Serializer = _xmldom.DOMParser;
} else {
Serializer = XMLSerializer;
}
var serializer = new Serializer();
this.output = serializer.serializeToString(contents);
return this.output;
}.bind(this)).then(function () {
return this.hooks.serialize.trigger(this.output, this);
}.bind(this)).then(function () {
rendering.resolve(this.output);
}.bind(this)).catch(function (error) {
rendering.reject(error);
});
return rendered;
}
/**
* Find a string in a section
* @param {string} _query The query string to find
* @return {object[]} A list of matches, with form {cfi, excerpt}
*/
find(_query) {
var section = this;
var matches = [];
var query = _query.toLowerCase();
var find = function (node) {
var text = node.textContent.toLowerCase();
var range = section.document.createRange();
var cfi;
var pos;
var last = -1;
var excerpt;
var limit = 150;
while (pos != -1) {
// Search for the query
pos = text.indexOf(query, last + 1);
if (pos != -1) {
// We found it! Generate a CFI
range = section.document.createRange();
range.setStart(node, pos);
range.setEnd(node, pos + query.length);
cfi = section.cfiFromRange(range); // Generate the excerpt
if (node.textContent.length < limit) {
excerpt = node.textContent;
} else {
excerpt = node.textContent.substring(pos - limit / 2, pos + limit / 2);
excerpt = "..." + excerpt + "...";
} // Add the CFI to the matches list
matches.push({
cfi: cfi,
excerpt: excerpt
});
}
last = pos;
}
};
(0, _core.sprint)(section.document, function (node) {
find(node);
});
return matches;
}
/**
* Search a string in multiple sequential Element of the section. If the document.createTreeWalker api is missed(eg: IE8), use `find` as a fallback.
* @param {string} _query The query string to search
* @param {int} maxSeqEle The maximum number of Element that are combined for search, default value is 5.
* @return {object[]} A list of matches, with form {cfi, excerpt}
*/
search(_query, maxSeqEle = 5) {
if (typeof document.createTreeWalker == "undefined") {
return this.find(_query);
}
let matches = [];
const excerptLimit = 150;
const section = this;
const query = _query.toLowerCase();
const search = function (nodeList) {
const textWithCase = nodeList.reduce((acc, current) => {
return acc + current.textContent;
}, "");
const text = textWithCase.toLowerCase();
const pos = text.indexOf(query);
if (pos != -1) {
const startNodeIndex = 0,
endPos = pos + query.length;
let endNodeIndex = 0,
l = 0;
if (pos < nodeList[startNodeIndex].length) {
let cfi;
while (endNodeIndex < nodeList.length - 1) {
l += nodeList[endNodeIndex].length;
if (endPos <= l) {
break;
}
endNodeIndex += 1;
}
let startNode = nodeList[startNodeIndex],
endNode = nodeList[endNodeIndex];
let range = section.document.createRange();
range.setStart(startNode, pos);
let beforeEndLengthCount = nodeList.slice(0, endNodeIndex).reduce((acc, current) => {
return acc + current.textContent.length;
}, 0);
range.setEnd(endNode, beforeEndLengthCount > endPos ? endPos : endPos - beforeEndLengthCount);
cfi = section.cfiFromRange(range);
let excerpt = nodeList.slice(0, endNodeIndex + 1).reduce((acc, current) => {
return acc + current.textContent;
}, "");
if (excerpt.length > excerptLimit) {
excerpt = excerpt.substring(pos - excerptLimit / 2, pos + excerptLimit / 2);
excerpt = "..." + excerpt + "...";
}
matches.push({
cfi: cfi,
excerpt: excerpt
});
}
}
};
const treeWalker = document.createTreeWalker(section.document, NodeFilter.SHOW_TEXT, null, false);
let node,
nodeList = [];
while (node = treeWalker.nextNode()) {
nodeList.push(node);
if (nodeList.length == maxSeqEle) {
search(nodeList.slice(0, maxSeqEle));
nodeList = nodeList.slice(1, maxSeqEle);
}
}
if (nodeList.length > 0) {
search(nodeList);
}
return matches;
}
/**
* Reconciles the current chapters layout properties with
* the global layout properties.
* @param {object} globalLayout The global layout settings object, chapter properties string
* @return {object} layoutProperties Object with layout properties
*/
reconcileLayoutSettings(globalLayout) {
//-- Get the global defaults
var settings = {
layout: globalLayout.layout,
spread: globalLayout.spread,
orientation: globalLayout.orientation
}; //-- Get the chapter's display type
this.properties.forEach(function (prop) {
var rendition = prop.replace("rendition:", "");
var split = rendition.indexOf("-");
var property, value;
if (split != -1) {
property = rendition.slice(0, split);
value = rendition.slice(split + 1);
settings[property] = value;
}
});
return settings;
}
/**
* Get a CFI from a Range in the Section
* @param {range} _range
* @return {string} cfi an EpubCFI string
*/
cfiFromRange(_range) {
return new _epubcfi.default(_range, this.cfiBase).toString();
}
/**
* Get a CFI from an Element in the Section
* @param {element} el
* @return {string} cfi an EpubCFI string
*/
cfiFromElement(el) {
return new _epubcfi.default(el, this.cfiBase).toString();
}
/**
* Unload the section document
*/
unload() {
this.document = undefined;
this.contents = undefined;
this.output = undefined;
}
destroy() {
this.unload();
this.hooks.serialize.clear();
this.hooks.content.clear();
this.hooks = undefined;
this.idref = undefined;
this.linear = undefined;
this.properties = undefined;
this.index = undefined;
this.href = undefined;
this.url = undefined;
this.next = undefined;
this.prev = undefined;
this.cfiBase = undefined;
}
}
var _default = Section;
exports.default = _default;