src/TreeWalker.js

/**
 * Generalization and simplification of the
 * {@link https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker TreeWalker web API} for the
 * normal and worker contexts.
 */
class TreeWalker {
  /**
   * Create a tree walker.
   *
   * @param {Node|external:Node} root Node that limits where the tree walker can go within this
   *   document's tree: only the root node and its descendants.
   * @param {Function} [acceptNode] Function that returns `true` if the tree walker should accept
   *   the node and `false` if it should reject.
   * @param {boolean} [onlyElementNodes=false] Walk only on element nodes, ignoring nodes of other
   *   types.
   * @param {Node|external:Node} [startNode=root] Node to set as a current node.
   */
  constructor(root, acceptNode, onlyElementNodes = false, startNode = root) {
    this.acceptNode = acceptNode;
    this.root = root;
    this.currentNode = startNode;

    if (onlyElementNodes) {
      this.firstChildProp = 'firstElementChild';
      this.lastChildProp = 'lastElementChild';
      this.previousSiblingProp = 'previousElementSibling';
      this.nextSiblingProp = 'nextElementSibling';
    } else {
      this.firstChildProp = 'firstChild';
      this.lastChildProp = 'lastChild';
      this.previousSiblingProp = 'previousSibling';
      this.nextSiblingProp = 'nextSibling';
    }
  }

  /**
   * Try changing the current node to a node specified by the property.
   *
   * @param {string} prop
   * @returns {?Node}
   * @protected
   */
  tryMove(prop) {
    let node = this.currentNode;
    if (node === this.root && !prop.includes('Child')) {
      return null;
    }
    do {
      node = node[prop];
    } while (node && this.acceptNode && !this.acceptNode(node));
    if (node) {
      this.currentNode = node;
    }
    return node || null;
  }

  /**
   * Go to the parent node.
   *
   * @returns {?Node}
   */
  parentNode() {
    return this.tryMove('parentNode');
  }

  /**
   * Go to the first child node.
   *
   * @returns {?Node}
   */
  firstChild() {
    return this.tryMove(this.firstChildProp);
  }

  /**
   * Go to the last child node.
   *
   * @returns {?Node}
   */
  lastChild() {
    return this.tryMove(this.lastChildProp);
  }

  /**
   * Go to the previous sibling node.
   *
   * @returns {?Node}
   */
  previousSibling() {
    return this.tryMove(this.previousSiblingProp);
  }

  /**
   * Go to the next sibling node.
   *
   * @returns {?Node}
   */
  nextSibling() {
    return this.tryMove(this.nextSiblingProp);
  }

  /**
   * Go to the next node (don't confuse with the next sibling).
   *
   * @returns {?Node}
   */
  nextNode() {
    let node = this.currentNode;
    do {
      if (node[this.firstChildProp]) {
        node = node[this.firstChildProp];
      } else {
        while (node && !node[this.nextSiblingProp] && node.parentNode !== this.root) {
          node = node.parentNode;
        }
        node &&= node[this.nextSiblingProp];
      }
    } while (node && this.acceptNode && !this.acceptNode(node));
    if (node) {
      this.currentNode = node;
    }
    return node;
  }

  /**
   * Go to the previous node (don't confuse with the previous sibling).
   *
   * @returns {?Node}
   */
  previousNode() {
    let node = this.currentNode;
    if (node === this.root) {
      return null;
    }
    do {
      if (node[this.previousSiblingProp]) {
        node = node[this.previousSiblingProp];
        while (node[this.lastChildProp]) {
          node = node[this.lastChildProp];
        }
      } else {
        node = node.parentNode;
      }
    } while (node && this.acceptNode && !this.acceptNode(node));
    if (node) {
      this.currentNode = node;
    }
    return node;
  }
}

export default TreeWalker;