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/mapping.js | 511 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 511 insertions(+) create mode 100644 lib/epub.js/src/mapping.js (limited to 'lib/epub.js/src/mapping.js') diff --git a/lib/epub.js/src/mapping.js b/lib/epub.js/src/mapping.js new file mode 100644 index 0000000..4216204 --- /dev/null +++ b/lib/epub.js/src/mapping.js @@ -0,0 +1,511 @@ +import EpubCFI from "./epubcfi"; +import { nodeBounds } from "./utils/core"; + +/** + * Map text locations to CFI ranges + * @class + * @param {Layout} layout Layout to apply + * @param {string} [direction="ltr"] Text direction + * @param {string} [axis="horizontal"] vertical or horizontal axis + * @param {boolean} [dev] toggle developer highlighting + */ +class Mapping { + constructor(layout, direction, axis, dev=false) { + this.layout = layout; + this.horizontal = (axis === "horizontal") ? true : false; + this.direction = direction || "ltr"; + this._dev = dev; + } + + /** + * Find CFI pairs for entire section at once + */ + section(view) { + var ranges = this.findRanges(view); + var map = this.rangeListToCfiList(view.section.cfiBase, ranges); + + return map; + } + + /** + * Find CFI pairs for a page + * @param {Contents} contents Contents from view + * @param {string} cfiBase string of the base for a cfi + * @param {number} start position to start at + * @param {number} end position to end at + */ + page(contents, cfiBase, start, end) { + var root = contents && contents.document ? contents.document.body : false; + var result; + + if (!root) { + return; + } + + result = this.rangePairToCfiPair(cfiBase, { + start: this.findStart(root, start, end), + end: this.findEnd(root, start, end) + }); + + if (this._dev === true) { + let doc = contents.document; + let startRange = new EpubCFI(result.start).toRange(doc); + let endRange = new EpubCFI(result.end).toRange(doc); + + let selection = doc.defaultView.getSelection(); + let r = doc.createRange(); + selection.removeAllRanges(); + r.setStart(startRange.startContainer, startRange.startOffset); + r.setEnd(endRange.endContainer, endRange.endOffset); + selection.addRange(r); + } + + return result; + } + + /** + * Walk a node, preforming a function on each node it finds + * @private + * @param {Node} root Node to walkToNode + * @param {function} func walk function + * @return {*} returns the result of the walk function + */ + walk(root, func) { + // IE11 has strange issue, if root is text node IE throws exception on + // calling treeWalker.nextNode(), saying + // Unexpected call to method or property access instead of returing null value + if(root && root.nodeType === Node.TEXT_NODE) { + return; + } + // safeFilter is required so that it can work in IE as filter is a function for IE + // and for other browser filter is an object. + var filter = { + acceptNode: function(node) { + if (node.data.trim().length > 0) { + return NodeFilter.FILTER_ACCEPT; + } else { + return NodeFilter.FILTER_REJECT; + } + } + }; + var safeFilter = filter.acceptNode; + safeFilter.acceptNode = filter.acceptNode; + + var treeWalker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, safeFilter, false); + var node; + var result; + while ((node = treeWalker.nextNode())) { + result = func(node); + if(result) break; + } + + return result; + } + + findRanges(view){ + var columns = []; + var scrollWidth = view.contents.scrollWidth(); + var spreads = Math.ceil( scrollWidth / this.layout.spreadWidth); + var count = spreads * this.layout.divisor; + var columnWidth = this.layout.columnWidth; + var gap = this.layout.gap; + var start, end; + + for (var i = 0; i < count.pages; i++) { + start = (columnWidth + gap) * i; + end = (columnWidth * (i+1)) + (gap * i); + columns.push({ + start: this.findStart(view.document.body, start, end), + end: this.findEnd(view.document.body, start, end) + }); + } + + return columns; + } + + /** + * Find Start Range + * @private + * @param {Node} root root node + * @param {number} start position to start at + * @param {number} end position to end at + * @return {Range} + */ + findStart(root, start, end){ + var stack = [root]; + var $el; + var found; + var $prev = root; + + while (stack.length) { + + $el = stack.shift(); + + found = this.walk($el, (node) => { + var left, right, top, bottom; + var elPos; + var elRange; + + + elPos = nodeBounds(node); + + if (this.horizontal && this.direction === "ltr") { + + left = this.horizontal ? elPos.left : elPos.top; + right = this.horizontal ? elPos.right : elPos.bottom; + + if( left >= start && left <= end ) { + return node; + } else if (right > start) { + return node; + } else { + $prev = node; + stack.push(node); + } + + } else if (this.horizontal && this.direction === "rtl") { + + left = elPos.left; + right = elPos.right; + + if( right <= end && right >= start ) { + return node; + } else if (left < end) { + return node; + } else { + $prev = node; + stack.push(node); + } + + } else { + + top = elPos.top; + bottom = elPos.bottom; + + if( top >= start && top <= end ) { + return node; + } else if (bottom > start) { + return node; + } else { + $prev = node; + stack.push(node); + } + + } + + + }); + + if(found) { + return this.findTextStartRange(found, start, end); + } + + } + + // Return last element + return this.findTextStartRange($prev, start, end); + } + + /** + * Find End Range + * @private + * @param {Node} root root node + * @param {number} start position to start at + * @param {number} end position to end at + * @return {Range} + */ + findEnd(root, start, end){ + var stack = [root]; + var $el; + var $prev = root; + var found; + + while (stack.length) { + + $el = stack.shift(); + + found = this.walk($el, (node) => { + + var left, right, top, bottom; + var elPos; + var elRange; + + elPos = nodeBounds(node); + + if (this.horizontal && this.direction === "ltr") { + + left = Math.round(elPos.left); + right = Math.round(elPos.right); + + if(left > end && $prev) { + return $prev; + } else if(right > end) { + return node; + } else { + $prev = node; + stack.push(node); + } + + } else if (this.horizontal && this.direction === "rtl") { + + left = Math.round(this.horizontal ? elPos.left : elPos.top); + right = Math.round(this.horizontal ? elPos.right : elPos.bottom); + + if(right < start && $prev) { + return $prev; + } else if(left < start) { + return node; + } else { + $prev = node; + stack.push(node); + } + + } else { + + top = Math.round(elPos.top); + bottom = Math.round(elPos.bottom); + + if(top > end && $prev) { + return $prev; + } else if(bottom > end) { + return node; + } else { + $prev = node; + stack.push(node); + } + + } + + }); + + + if(found){ + return this.findTextEndRange(found, start, end); + } + + } + + // end of chapter + return this.findTextEndRange($prev, start, end); + } + + /** + * Find Text Start Range + * @private + * @param {Node} root root node + * @param {number} start position to start at + * @param {number} end position to end at + * @return {Range} + */ + findTextStartRange(node, start, end){ + var ranges = this.splitTextNodeIntoRanges(node); + var range; + var pos; + var left, top, right; + + for (var i = 0; i < ranges.length; i++) { + range = ranges[i]; + + pos = range.getBoundingClientRect(); + + if (this.horizontal && this.direction === "ltr") { + + left = pos.left; + if( left >= start ) { + return range; + } + + } else if (this.horizontal && this.direction === "rtl") { + + right = pos.right; + if( right <= end ) { + return range; + } + + } else { + + top = pos.top; + if( top >= start ) { + return range; + } + + } + + // prev = range; + + } + + return ranges[0]; + } + + /** + * Find Text End Range + * @private + * @param {Node} root root node + * @param {number} start position to start at + * @param {number} end position to end at + * @return {Range} + */ + findTextEndRange(node, start, end){ + var ranges = this.splitTextNodeIntoRanges(node); + var prev; + var range; + var pos; + var left, right, top, bottom; + + for (var i = 0; i < ranges.length; i++) { + range = ranges[i]; + + pos = range.getBoundingClientRect(); + + if (this.horizontal && this.direction === "ltr") { + + left = pos.left; + right = pos.right; + + if(left > end && prev) { + return prev; + } else if(right > end) { + return range; + } + + } else if (this.horizontal && this.direction === "rtl") { + + left = pos.left + right = pos.right; + + if(right < start && prev) { + return prev; + } else if(left < start) { + return range; + } + + } else { + + top = pos.top; + bottom = pos.bottom; + + if(top > end && prev) { + return prev; + } else if(bottom > end) { + return range; + } + + } + + + prev = range; + + } + + // Ends before limit + return ranges[ranges.length-1]; + + } + + /** + * Split up a text node into ranges for each word + * @private + * @param {Node} root root node + * @param {string} [_splitter] what to split on + * @return {Range[]} + */ + splitTextNodeIntoRanges(node, _splitter){ + var ranges = []; + var textContent = node.textContent || ""; + var text = textContent.trim(); + var range; + var doc = node.ownerDocument; + var splitter = _splitter || " "; + + var pos = text.indexOf(splitter); + + if(pos === -1 || node.nodeType != Node.TEXT_NODE) { + range = doc.createRange(); + range.selectNodeContents(node); + return [range]; + } + + range = doc.createRange(); + range.setStart(node, 0); + range.setEnd(node, pos); + ranges.push(range); + range = false; + + while ( pos != -1 ) { + + pos = text.indexOf(splitter, pos + 1); + if(pos > 0) { + + if(range) { + range.setEnd(node, pos); + ranges.push(range); + } + + range = doc.createRange(); + range.setStart(node, pos+1); + } + } + + if(range) { + range.setEnd(node, text.length); + ranges.push(range); + } + + return ranges; + } + + + /** + * Turn a pair of ranges into a pair of CFIs + * @private + * @param {string} cfiBase base string for an EpubCFI + * @param {object} rangePair { start: Range, end: Range } + * @return {object} { start: "epubcfi(...)", end: "epubcfi(...)" } + */ + rangePairToCfiPair(cfiBase, rangePair){ + + var startRange = rangePair.start; + var endRange = rangePair.end; + + startRange.collapse(true); + endRange.collapse(false); + + let startCfi = new EpubCFI(startRange, cfiBase).toString(); + let endCfi = new EpubCFI(endRange, cfiBase).toString(); + + return { + start: startCfi, + end: endCfi + }; + + } + + rangeListToCfiList(cfiBase, columns){ + var map = []; + var cifPair; + + for (var i = 0; i < columns.length; i++) { + cifPair = this.rangePairToCfiPair(cfiBase, columns[i]); + + map.push(cifPair); + + } + + return map; + } + + /** + * Set the axis for mapping + * @param {string} axis horizontal | vertical + * @return {boolean} is it horizontal? + */ + axis(axis) { + if (axis) { + this.horizontal = (axis === "horizontal") ? true : false; + } + return this.horizontal; + } +} + +export default Mapping; -- cgit v1.2.3