/* 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/. */ "use strict"; loader.lazyRequireGetter( this, "nodeFilterConstants", "resource://devtools/shared/dom-node-filter-constants.js" ); loader.lazyRequireGetter( this, "standardTreeWalkerFilter", "resource://devtools/server/actors/inspector/utils.js", true ); // SKIP_TO_* arguments are used with the DocumentWalker, driving the strategy to use if // the starting node is incompatible with the filter function of the walker. const SKIP_TO_PARENT = "SKIP_TO_PARENT"; const SKIP_TO_SIBLING = "SKIP_TO_SIBLING"; class DocumentWalker { /** * Wrapper for inDeepTreeWalker. Adds filtering to the traversal methods. * See inDeepTreeWalker for more information about the methods. * * @param {DOMNode} node * @param {Window} rootWin * @param {Object} * - {Function} filter * A custom filter function Taking in a DOMNode and returning an Int. See * WalkerActor.nodeFilter for an example. * - {String} skipTo * Either SKIP_TO_PARENT or SKIP_TO_SIBLING. If the provided node is not * compatible with the filter function for this walker, try to find a compatible * one either in the parents or in the siblings of the node. * - {Boolean} showAnonymousContent * Pass true to let the walker return and traverse anonymous content. * When navigating host elements to which shadow DOM is attached, the light tree * will be visible only to a walker with showAnonymousContent=false. The shadow * tree will only be visible to a walker with showAnonymousContent=true. */ constructor( node, rootWin, { filter = standardTreeWalkerFilter, skipTo = SKIP_TO_PARENT, showAnonymousContent = true, } = {} ) { if (Cu.isDeadWrapper(rootWin) || !rootWin.location) { throw new Error("Got an invalid root window in DocumentWalker"); } this.walker = Cc[ "@mozilla.org/inspector/deep-tree-walker;1" ].createInstance(Ci.inIDeepTreeWalker); this.walker.showAnonymousContent = showAnonymousContent; this.walker.showSubDocuments = true; this.walker.showDocumentsAsNodes = true; this.walker.init(rootWin.document); this.filter = filter; // Make sure that the walker knows about the initial node (which could // be skipped due to a filter). this.walker.currentNode = this.getStartingNode(node, skipTo); } get currentNode() { return this.walker.currentNode; } set currentNode(val) { this.walker.currentNode = val; } parentNode() { return this.walker.parentNode(); } nextNode() { const node = this.walker.currentNode; if (!node) { return null; } let nextNode = this.walker.nextNode(); while (nextNode && this.isSkippedNode(nextNode)) { nextNode = this.walker.nextNode(); } return nextNode; } firstChild() { if (!this.walker.currentNode) { return null; } let firstChild = this.walker.firstChild(); while (firstChild && this.isSkippedNode(firstChild)) { firstChild = this.walker.nextSibling(); } return firstChild; } lastChild() { if (!this.walker.currentNode) { return null; } let lastChild = this.walker.lastChild(); while (lastChild && this.isSkippedNode(lastChild)) { lastChild = this.walker.previousSibling(); } return lastChild; } previousSibling() { let node = this.walker.previousSibling(); while (node && this.isSkippedNode(node)) { node = this.walker.previousSibling(); } return node; } nextSibling() { let node = this.walker.nextSibling(); while (node && this.isSkippedNode(node)) { node = this.walker.nextSibling(); } return node; } getStartingNode(node, skipTo) { // Keep a reference on the starting node in case we can't find a node compatible with // the filter. const startingNode = node; if (skipTo === SKIP_TO_PARENT) { while (node && this.isSkippedNode(node)) { node = node.parentNode; } } else if (skipTo === SKIP_TO_SIBLING) { node = this.getClosestAcceptedSibling(node); } return node || startingNode; } /** * Loop on all of the provided node siblings until finding one that is compliant with * the filter function. */ getClosestAcceptedSibling(node) { if (this.filter(node) === nodeFilterConstants.FILTER_ACCEPT) { // node is already valid, return immediately. return node; } // Loop on starting node siblings. let previous = node; let next = node; while (previous || next) { previous = previous?.previousSibling; next = next?.nextSibling; if ( previous && this.filter(previous) === nodeFilterConstants.FILTER_ACCEPT ) { // A valid node was found in the previous siblings of the node. return previous; } if (next && this.filter(next) === nodeFilterConstants.FILTER_ACCEPT) { // A valid node was found in the next siblings of the node. return next; } } return null; } isSkippedNode(node) { return this.filter(node) === nodeFilterConstants.FILTER_SKIP; } } exports.DocumentWalker = DocumentWalker; exports.SKIP_TO_PARENT = SKIP_TO_PARENT; exports.SKIP_TO_SIBLING = SKIP_TO_SIBLING;