mirror of
https://github.com/penpot/penpot.git
synced 2025-12-11 22:14:05 +01:00
251 lines
5.5 KiB
JavaScript
251 lines
5.5 KiB
JavaScript
/**
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
*
|
|
* Copyright (c) KALEIDOS INC
|
|
*/
|
|
|
|
/**
|
|
* Iterator direction.
|
|
*
|
|
* @enum {number}
|
|
*/
|
|
export const TextNodeIteratorDirection = {
|
|
FORWARD: 1,
|
|
BACKWARD: 0,
|
|
};
|
|
|
|
/**
|
|
* TextNodeIterator
|
|
*/
|
|
export class TextNodeIterator {
|
|
/**
|
|
* Returns if a specific node is a text node.
|
|
*
|
|
* @param {Node} node
|
|
* @returns {boolean}
|
|
*/
|
|
static isTextNode(node) {
|
|
return (
|
|
node.nodeType === Node.TEXT_NODE ||
|
|
(node.nodeType === Node.ELEMENT_NODE && node.nodeName === "BR")
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns if a specific node is a container node.
|
|
*
|
|
* @param {Node} node
|
|
* @returns {boolean}
|
|
*/
|
|
static isContainerNode(node) {
|
|
return node.nodeType === Node.ELEMENT_NODE && node.nodeName !== "BR";
|
|
}
|
|
|
|
/**
|
|
* Finds a node from an initial node and down the tree.
|
|
*
|
|
* @param {Node} startNode
|
|
* @param {Node} rootNode
|
|
* @param {Set<Node>} skipNodes
|
|
* @param {number} direction
|
|
* @returns {Node}
|
|
*/
|
|
static findDown(
|
|
startNode,
|
|
rootNode,
|
|
skipNodes = new Set(),
|
|
direction = TextNodeIteratorDirection.FORWARD
|
|
) {
|
|
if (startNode === rootNode) {
|
|
return TextNodeIterator.findDown(
|
|
direction === TextNodeIteratorDirection.FORWARD
|
|
? startNode.firstChild
|
|
: startNode.lastChild,
|
|
rootNode,
|
|
skipNodes,
|
|
direction
|
|
);
|
|
}
|
|
|
|
// NOTE: This should not use the SafeGuard
|
|
// module.
|
|
let safeGuard = Date.now();
|
|
let currentNode = startNode;
|
|
while (currentNode) {
|
|
if (Date.now() - safeGuard >= 1000) {
|
|
throw new Error("Iteration timeout");
|
|
}
|
|
if (skipNodes.has(currentNode)) {
|
|
currentNode =
|
|
direction === TextNodeIteratorDirection.FORWARD
|
|
? currentNode.nextSibling
|
|
: currentNode.previousSibling;
|
|
continue;
|
|
}
|
|
if (TextNodeIterator.isTextNode(currentNode)) {
|
|
return currentNode;
|
|
} else if (TextNodeIterator.isContainerNode(currentNode)) {
|
|
return TextNodeIterator.findDown(
|
|
direction === TextNodeIteratorDirection.FORWARD
|
|
? currentNode.firstChild
|
|
: currentNode.lastChild,
|
|
rootNode,
|
|
skipNodes,
|
|
direction
|
|
);
|
|
}
|
|
currentNode =
|
|
direction === TextNodeIteratorDirection.FORWARD
|
|
? currentNode.nextSibling
|
|
: currentNode.previousSibling;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Finds a node from an initial node and up the tree.
|
|
*
|
|
* @param {Node} startNode
|
|
* @param {Node} rootNode
|
|
* @param {Set} backTrack
|
|
* @param {number} direction
|
|
* @returns {Node}
|
|
*/
|
|
static findUp(
|
|
startNode,
|
|
rootNode,
|
|
backTrack = new Set(),
|
|
direction = TextNodeIteratorDirection.FORWARD
|
|
) {
|
|
backTrack.add(startNode);
|
|
if (TextNodeIterator.isTextNode(startNode)) {
|
|
return TextNodeIterator.findUp(
|
|
startNode.parentNode,
|
|
rootNode,
|
|
backTrack,
|
|
direction
|
|
);
|
|
} else if (TextNodeIterator.isContainerNode(startNode)) {
|
|
const found = TextNodeIterator.findDown(
|
|
startNode,
|
|
rootNode,
|
|
backTrack,
|
|
direction
|
|
);
|
|
if (found) {
|
|
return found;
|
|
}
|
|
if (startNode !== rootNode) {
|
|
return TextNodeIterator.findUp(
|
|
startNode.parentNode,
|
|
rootNode,
|
|
backTrack,
|
|
direction
|
|
);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* This is the root text node.
|
|
*
|
|
* @type {HTMLElement}
|
|
*/
|
|
#rootNode = null;
|
|
|
|
/**
|
|
* This is the current text node.
|
|
*
|
|
* @type {Text|null}
|
|
*/
|
|
#currentNode = null;
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param {HTMLElement} rootNode
|
|
*/
|
|
constructor(rootNode) {
|
|
if (!(rootNode instanceof HTMLElement)) {
|
|
throw new TypeError("Invalid root node");
|
|
}
|
|
this.#rootNode = rootNode;
|
|
this.#currentNode = TextNodeIterator.findDown(rootNode, rootNode);
|
|
}
|
|
|
|
/**
|
|
* Current node we're into.
|
|
*
|
|
* @type {TextNode|HTMLBRElement}
|
|
*/
|
|
get currentNode() {
|
|
return this.#currentNode;
|
|
}
|
|
|
|
set currentNode(newCurrentNode) {
|
|
const isContained =
|
|
(newCurrentNode.compareDocumentPosition(this.#rootNode) &
|
|
Node.DOCUMENT_POSITION_CONTAINS) ===
|
|
Node.DOCUMENT_POSITION_CONTAINS;
|
|
if (
|
|
!(newCurrentNode instanceof Node) ||
|
|
!TextNodeIterator.isTextNode(newCurrentNode) ||
|
|
!isContained
|
|
) {
|
|
throw new TypeError("Invalid new current node");
|
|
}
|
|
this.#currentNode = newCurrentNode;
|
|
}
|
|
|
|
/**
|
|
* Returns the next Text node or <br> element or null if there are.
|
|
*
|
|
* @returns {Text|HTMLBRElement}
|
|
*/
|
|
nextNode() {
|
|
if (!this.#currentNode) return null;
|
|
|
|
const nextNode = TextNodeIterator.findUp(
|
|
this.#currentNode,
|
|
this.#rootNode,
|
|
new Set(),
|
|
TextNodeIteratorDirection.FORWARD
|
|
);
|
|
|
|
if (!nextNode) {
|
|
return null;
|
|
}
|
|
|
|
this.#currentNode = nextNode;
|
|
return this.#currentNode;
|
|
}
|
|
|
|
/**
|
|
* Returns the previous Text node or <br> element or null.
|
|
*
|
|
* @returns {Text|HTMLBRElement}
|
|
*/
|
|
previousNode() {
|
|
if (!this.#currentNode) return null;
|
|
|
|
const previousNode = TextNodeIterator.findUp(
|
|
this.#currentNode,
|
|
this.#rootNode,
|
|
new Set(),
|
|
TextNodeIteratorDirection.BACKWARD
|
|
);
|
|
|
|
if (!previousNode) {
|
|
return null;
|
|
}
|
|
|
|
this.#currentNode = previousNode;
|
|
return this.#currentNode;
|
|
}
|
|
}
|
|
|
|
export default TextNodeIterator;
|