UNPKG

node-webodf

Version:

WebODF - JavaScript Document Engine http://webodf.org/

519 lines (497 loc) 19.5 kB
/** * Copyright (C) 2012 KO GmbH <copyright@kogmbh.com> * * @licstart * This file is part of WebODF. * * WebODF is free software: you can redistribute it and/or modify it * under the terms of the GNU Affero General Public License (GNU AGPL) * as published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. * * WebODF is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with WebODF. If not, see <http://www.gnu.org/licenses/>. * @licend * * @source: http://www.webodf.org/ * @source: https://github.com/kogmbh/WebODF/ */ /*global Node, NodeFilter, runtime, core*/ /** * An iterator that iterators through positions in a DOM tree. * Positions in the DOM tree are places between nodes and between characters. * Undesired positions can be avoided by passing a filter to constructor of the * PositionIterator. * In the following example '|' designates positions that an unfiltered * PositionIterator would visit. * * <a>|<b>|<c>|a|b|</c>|a|<a>|</a>|</b>|</a> * * Certain positions are considered equivalent in by the PositionIterator. * Position 0 in a Text node is the same as the position preceding the Text node * in the parent node. The last position in a Text node is considered equal to * the subsequent position in the parent node. As such, these two Text node * positions are ommitted from the PositionIterator's traversal throught the * DOM. If the PositionIterator is set to a first or last position in a Text * node, it is instead set the equivalent position in the parent node. * Omitting the first and last Text node positions serves two functions: * - It ensures that the number of iterated steps is independent of how * characters are split up over text nodes. * - The iterator avoids positions that not distinguised by the API for * range and selections purposes. * * * @constructor * @param {!Node} root * @param {!number=} whatToShow * @param {!NodeFilter=} filter * @param {!boolean=} expandEntityReferences */ core.PositionIterator = function PositionIterator(root, whatToShow, filter, expandEntityReferences) { "use strict"; /* * Implementation notes. * The position of the positioniterator is defined by two internal * variables: walker and currentPos. The walker is an instance of TreeWalker * which has a member called currentNode of type Node. * Since the implementation uses a Node and an offset, it is comparable to * the parameters that go into Range and Selection related functions. * If the currentNode is a Text node, the variable currentPos gives the * offset in the node. * If the currentNode is an Element node, the variable currentPos can only * have the values 0 or 1. The value 0 means that the iterator is at the * position just before the currentNode. A value of 1 means that the * iterator is at the last position inside the currentNode. */ var self = this, /**@type{!TreeWalker}*/ walker, /**@type{!number}*/ currentPos, /**@type{!function(?Node):!number}*/ nodeFilter, TEXT_NODE = Node.TEXT_NODE, ELEMENT_NODE = Node.ELEMENT_NODE, FILTER_ACCEPT = NodeFilter.FILTER_ACCEPT, FILTER_REJECT = NodeFilter.FILTER_REJECT; /** * Empty text nodes are not considered to be a valid position for the * positioniterator. They should be filtered out in all cases. * @constructor * @implements NodeFilter */ function EmptyTextNodeFilter() { /** * @param {?Node} node * @return {!number} */ this.acceptNode = function (node) { var text = /**@type{!Text}*/(node); if (!node || (node.nodeType === TEXT_NODE && text.length === 0)) { return FILTER_REJECT; } return FILTER_ACCEPT; }; } /** * @constructor * @implements NodeFilter * @param {!NodeFilter} filter */ function FilteredEmptyTextNodeFilter(filter) { /** * @param {?Node} node * @return {!number} */ this.acceptNode = function (node) { var text = /**@type{!Text}*/(node); if (!node || (node.nodeType === TEXT_NODE && text.length === 0)) { return FILTER_REJECT; } return filter.acceptNode(node); }; } /** * @return {!boolean} */ this.nextPosition = function () { var currentNode = walker.currentNode, nodeType = currentNode.nodeType, text = /**@type{!Text}*/(currentNode); if (currentNode === root) { return false; } if (currentPos === 0 && nodeType === ELEMENT_NODE) { // step inside an element if (walker.firstChild() === null) { currentPos = 1; } } else if (nodeType === TEXT_NODE && currentPos + 1 < text.length) { // advance inside a text node currentPos += 1; } else { if (walker.nextSibling() !== null) { currentPos = 0; } else if (walker.parentNode()) { currentPos = 1; } else { return false; } } return true; }; function setAtEnd() { var text = /**@type{!Text}*/(walker.currentNode), type = text.nodeType; if (type === TEXT_NODE) { currentPos = text.length - 1; } else { currentPos = (type === ELEMENT_NODE) ? 1 : 0; } } /** * @return {!boolean} */ function previousNode() { if (walker.previousSibling() === null) { if (!walker.parentNode() || walker.currentNode === root) { walker.firstChild(); return false; } currentPos = 0; } else { setAtEnd(); } return true; } /** * Move the iterator to the previous position. * If the iterator is already at the first position, it is not moved and * false is returned instead of true. * @return {!boolean} */ this.previousPosition = function () { var moved = true, currentNode = walker.currentNode; if (currentPos === 0) { moved = previousNode(); } else if (currentNode.nodeType === TEXT_NODE) { currentPos -= 1; } else if (walker.lastChild() !== null) { setAtEnd(); } else if (currentNode === root) { moved = false; } else { currentPos = 0; } return moved; }; /** * This function exposes class internals and should be avoided. */ this.previousNode = previousNode; /** * Return the container for the current position. * @return {!Element|!Text} */ this.container = function () { var n = /**@type{!Element|!Text}*/(walker.currentNode), t = n.nodeType; if (currentPos === 0 && t !== TEXT_NODE) { n = /**@type{!Element|!Text}*/(n.parentNode); } return n; }; /** * Return the node to the right of the current iterator position. * If the iterator is placed between two characters in a text node, * the text node will be returned. * If there is no right neighbor in the container node, then null is * returned. * Only filtered nodes will be returned. * @return {?Node} */ this.rightNode = function () { var n = walker.currentNode, text = /**@type{!Text}*/(n), nodeType = n.nodeType; if (nodeType === TEXT_NODE && currentPos === text.length) { n = n.nextSibling; while (n && nodeFilter(n) !== FILTER_ACCEPT) { n = n.nextSibling; } } else if (nodeType === ELEMENT_NODE && currentPos === 1) { n = null; } return n; }; /** * Return the node to the left of the current iterator position. * See rightNode(). * @return {?Node} */ this.leftNode = function () { var n = walker.currentNode; if (currentPos === 0) { n = n.previousSibling; while (n && nodeFilter(n) !== FILTER_ACCEPT) { n = n.previousSibling; } } else if (n.nodeType === ELEMENT_NODE) { n = n.lastChild; while (n && nodeFilter(n) !== FILTER_ACCEPT) { n = n.previousSibling; } } return n; }; /** * This function exposes class internals and should be avoided. * @return {!Element|!Text} */ this.getCurrentNode = function () { var n = /**@type{!Element|!Text}*/(walker.currentNode); return n; }; /** * Returns the current position within the container of the iterator. * This function is useful for communication iterator position with * components that do not use a filter. * @return {!number} */ this.unfilteredDomOffset = function () { if (walker.currentNode.nodeType === TEXT_NODE) { return currentPos; } var c = 0, n = walker.currentNode; if (currentPos === 1) { n = n.lastChild; } else { n = n.previousSibling; } while (n) { c += 1; n = n.previousSibling; } return c; }; /** * Return the previous sibling of the current node * @return {Node} */ this.getPreviousSibling = function () { var currentNode = walker.currentNode, sibling = walker.previousSibling(); walker.currentNode = currentNode; return sibling; }; /** * Return the next sibling of the current node * @return {Node} */ this.getNextSibling = function () { var currentNode = walker.currentNode, sibling = walker.nextSibling(); walker.currentNode = currentNode; return sibling; }; /** * Advance the walker to the first node that is accepted by the node filter * (i.e., nodeFilter(node) === FILTER_ACCEPT) * * @return {!boolean} Returns true if the walker found an accepted node. Otherwise * returns false. */ function moveToAcceptedNode() { var node = walker.currentNode, filterResult, moveResult; // Ensure currentNode is not within a rejected subtree by crawling each parent node // up to the root and verifying it is either accepted or skipped by the nodeFilter. // NOTE: The root is deliberately not checked as it is the container iteration happens within. filterResult = nodeFilter(node); if (node !== root) { node = node.parentNode; while (node && node !== root) { if (nodeFilter(node) === FILTER_REJECT) { walker.currentNode = node; filterResult = FILTER_REJECT; } node = node.parentNode; } } if (filterResult === FILTER_REJECT) { // Set currentPos to be 1 (or text data.length), so nextPosition will jump to the next sibling or parent currentPos = walker.currentNode.nodeType === TEXT_NODE ? /**@type{!Text}*/(node).length : 1; moveResult = self.nextPosition(); } else if (filterResult === FILTER_ACCEPT) { moveResult = true; } else { // filterResult === FILTER_SKIP // FILTER_SKIP indicates children of the current node are acceptable. // currentPos is left unchanged as nextPosition can advance to an accepted child inside the node moveResult = self.nextPosition(); } if (moveResult) { runtime.assert(nodeFilter(walker.currentNode) === FILTER_ACCEPT, "moveToAcceptedNode did not result in walker being on an accepted node"); } return moveResult; } /** * Set the current position of the iterator to just before the supplied element. * * Querying the iterator then will return the container of the element and the offset * of the element within it's container (assuming the supplied element is accepted by * the nodeFilter). * * E.g., * p1.setPositionBeforeElement(span); * p1.container() === span.parentNode * p1.unfilteredDomOffset === positionInParent(span) * * If the element is not accepted by the nodeFilter, the iterator will immediately * move to the next accepted node. * * @param {!Element} element * @return {!boolean} Returns true if the iterator was set to a valid position * (i.e., is currently on a node that is accepted by the nodeFilter) */ this.setPositionBeforeElement = function (element) { runtime.assert(Boolean(element), "setPositionBeforeElement called without element"); walker.currentNode = element; currentPos = 0; return moveToAcceptedNode(); }; /** * Set the current position of the iterator to the specified container + offset. * * Querying the iterator will then return the supplied container + offset * (assuming the supplied element is accepted by the nodeFilter). * * E.g., * p2.setUnfilteredPosition(container, offset); * p2.container() === container * p2.unfilteredDomOffset() === offset; * * If the container is not accepted by the nodeFilter, the iterator will immediately * move to the next accepted node. * * @param {!Node} container * @param {!number} offset offset in unfiltered DOM world. Will immediately advance * the iterator to the numbered child node of the provided container. * @return {!boolean} Returns true if the iterator was set to a valid position * (i.e., is currently on a node that is accepted by the nodeFilter) */ this.setUnfilteredPosition = function (container, offset) { var text; runtime.assert(Boolean(container), "PositionIterator.setUnfilteredPosition called without container"); walker.currentNode = container; if (container.nodeType === TEXT_NODE) { currentPos = offset; text = /**@type{!Text}*/(container); runtime.assert(offset <= text.length, "Error in setPosition: " + offset + " > " + text.length); runtime.assert(offset >= 0, "Error in setPosition: " + offset + " < 0"); if (offset === text.length) { if (walker.nextSibling()) { currentPos = 0; } else if (walker.parentNode()) { currentPos = 1; } else { runtime.assert(false, "Error in setUnfilteredPosition: position not valid."); } } } else if (offset < container.childNodes.length) { // Immediately advance to the child node at that offset to begin iteration. // This is necessary in order to satisfy the most frequent use case where developer will // store the (container, unfilteredDomOffset) from a previous position iterator, and use // this value to resume iteration at the specified point. If we didn't immediately advance // to the next position, the first call to nextPosition would return the input container+offset. walker.currentNode = /**@type{!Node}*/(container.childNodes.item(offset)); currentPos = 0; } else { // Either the node has no children or offset === childNodes.length // Set currentPos to 1 to indicate iteration on the currentNode is complete. // This will cause the next call to self.nextPosition() to jump to the next // available sibling or parent currentPos = 1; } return moveToAcceptedNode(); }; /** * Move the iterator to its last possible position. * This is at the last position in the root node if the iterator. * @return {undefined} */ this.moveToEnd = function () { walker.currentNode = root; currentPos = 1; }; /** * Places the iterator at the last position inside the given node. * @param {!Node} node * @return {undefined} */ this.moveToEndOfNode = function (node) { var text; if (node.nodeType === TEXT_NODE) { text = /**@type{!Text}*/(node); self.setUnfilteredPosition(text, text.length); } else { walker.currentNode = node; currentPos = 1; } }; /** * Returns true if the iterator is just to the left of a node. In this position, * calls to container() will return the parent of the node, and unfilteredDomOffset * will return the position of the node within the parent container. * * Calls to unfilteredDomOffset are extremely slow when the iterator is just before a * node, so querying this method can provide warning when a slow offset is necessary. * @return {!boolean} */ this.isBeforeNode = function() { return currentPos === 0; }; /** * Return the filter that is used in this iterator. * @return {!function(?Node):!number} */ this.getNodeFilter = function () { return nodeFilter; }; function init() { var f; // a position can never be near an empty TextNode. A NodeFilter is the // easiest way of filtering out these nodes. if (filter) { f = new FilteredEmptyTextNodeFilter(filter); } else { f = new EmptyTextNodeFilter(); } // workaround for versions of createTreeWalker that need a function // instead of an object with a function such as IE 9 and older webkits nodeFilter = /**@type {!function(?Node):!number}*/(f.acceptNode); nodeFilter.acceptNode = nodeFilter; whatToShow = whatToShow || NodeFilter.SHOW_ALL; runtime.assert(root.nodeType !== Node.TEXT_NODE, "Internet Explorer doesn't allow tree walker roots to be text nodes"); walker = root.ownerDocument.createTreeWalker(root, whatToShow, nodeFilter, expandEntityReferences); currentPos = 0; if (walker.firstChild() === null) { currentPos = 1; } } init(); };