From 763ce8802121e41cfd981439186ebdf7d092c581 Mon Sep 17 00:00:00 2001 From: Ed Sanders Date: Mon, 16 Nov 2020 00:13:55 +0000 Subject: [PATCH] Base TreeWalker implementation on PHPDOM For consistency with other DOM implementations. Change-Id: I20447d880ccd3b70b6694b36ea2f63dd0c42fa84 --- includes/TreeWalker.php | 166 +++++++++++++++++++++------------------- 1 file changed, 89 insertions(+), 77 deletions(-) diff --git a/includes/TreeWalker.php b/includes/TreeWalker.php index e546c9b81..b10984a80 100644 --- a/includes/TreeWalker.php +++ b/includes/TreeWalker.php @@ -4,6 +4,7 @@ namespace MediaWiki\Extension\DiscussionTools; use DOMNode; use Exception; +use Throwable; /** * Partial implementation of W3 DOM4 TreeWalker interface. @@ -11,7 +12,7 @@ use Exception; * See also: * - https://dom.spec.whatwg.org/#interface-treewalker * - * Adapted from https://github.com/Krinkle/dom-TreeWalker-polyfill/blob/master/src/TreeWalker-polyfill.js + * Ported from https://github.com/TRowbotham/PHPDOM (MIT) */ class TreeWalker { @@ -20,83 +21,24 @@ class TreeWalker { public $currentNode; public $filter; - /** - * See https://dom.spec.whatwg.org/#concept-node-filter - * - * @param TreeWalker $tw - * @param DOMNode $node - * @return int Constant NodeFilter::FILTER_ACCEPT, - * NodeFilter::FILTER_REJECT or NodeFilter::FILTER_SKIP. - */ - private function nodeFilter( TreeWalker $tw, DOMNode $node ) { - // Maps nodeType to whatToShow - if ( !( ( ( 1 << ( $node->nodeType - 1 ) ) & $tw->whatToShow ) ) ) { - return NodeFilter::FILTER_SKIP; - } - - if ( $tw->filter === null ) { - return NodeFilter::FILTER_ACCEPT; - } - - return $tw->filter->acceptNode( $node ); - } - - /** - * Based on WebKit's NodeTraversal::nextSkippingChildren - * https://trac.webkit.org/browser/trunk/Source/WebCore/dom/NodeTraversal.h?rev=137221#L103 - * - * @param DOMNode $node - * @param DOMNode $stayWithin - * @return DOMNode|null - */ - private function nextSkippingChildren( DOMNode $node, DOMNode $stayWithin ) : ?DOMNode { - if ( $node === $stayWithin ) { - return null; - } - if ( $node->nextSibling !== null ) { - return $node->nextSibling; - } - - /** - * Based on WebKit's NodeTraversal::nextAncestorSibling - * https://trac.webkit.org/browser/trunk/Source/WebCore/dom/NodeTraversal.cpp?rev=137221#L43 - */ - while ( $node->parentNode !== null ) { - $node = $node->parentNode; - if ( $node === $stayWithin ) { - return null; - } - if ( $node->nextSibling !== null ) { - return $node->nextSibling; - } - } - return null; - } + private $isActive = false; /** * See https://dom.spec.whatwg.org/#interface-treewalker * * @param DOMNode $root - * @param int|null $whatToShow + * @param int $whatToShow * @param callable|null $filter - * @throws Exception */ - public function __construct( DOMNode $root, $whatToShow = null, callable $filter = null ) { - if ( !$root->nodeType ) { - throw new Exception( 'DOMException: NOT_SUPPORTED_ERR' ); - } - - $this->root = $root; - $this->whatToShow = (int)$whatToShow ?: 0; - + public function __construct( + DOMNode $root, + int $whatToShow = NodeFilter::SHOW_ALL, + callable $filter = null + ) { $this->currentNode = $root; - - if ( !$filter ) { - $this->filter = null; - } else { - $this->filter = new NodeFilter(); - $this->filter->filter = $filter; - } + $this->filter = $filter; + $this->root = $root; + $this->whatToShow = $whatToShow; } /** @@ -111,23 +53,93 @@ class TreeWalker { while ( true ) { while ( $result !== NodeFilter::FILTER_REJECT && $node->firstChild !== null ) { $node = $node->firstChild; - $result = $this->nodeFilter( $this, $node ); + $result = $this->filterNode( $node ); if ( $result === NodeFilter::FILTER_ACCEPT ) { $this->currentNode = $node; return $node; } } - $following = $this->nextSkippingChildren( $node, $this->root ); - if ( $following !== null ) { - $node = $following; - } else { - return null; + + $sibling = null; + $temp = $node; + while ( $temp !== null ) { + if ( $temp === $this->root ) { + return null; + } + + $sibling = $temp->nextSibling; + + if ( $sibling !== null ) { + $node = $sibling; + + break; + } + + $temp = $temp->parentNode; } - $result = $this->nodeFilter( $this, $node ); + + $result = $this->filterNode( $node ); + if ( $result === NodeFilter::FILTER_ACCEPT ) { $this->currentNode = $node; + return $node; } + } } + + /** + * Filters a node. + * + * @internal + * + * @see https://dom.spec.whatwg.org/#concept-node-filter + * + * @param DOMNode $node The node to check. + * @return int Returns one of NodeFilter's FILTER_* constants. + * - NodeFilter::FILTER_ACCEPT + * - NodeFilter::FILTER_REJECT + * - NodeFilter::FILTER_SKIP + * @throws Exception + */ + private function filterNode( DOMNode $node ): int { + if ( $this->isActive ) { + throw new Exception( 'InvalidStateError' ); + } + + // Let n be node’s nodeType attribute value minus 1. + $n = $node->nodeType - 1; + + // If the nth bit (where 0 is the least significant bit) of whatToShow + // is not set, return FILTER_SKIP. + if ( !( ( 1 << $n ) & $this->whatToShow ) ) { + return NodeFilter::FILTER_SKIP; + } + + // If filter is null, return FILTER_ACCEPT. + if ( !$this->filter ) { + return NodeFilter::FILTER_ACCEPT; + } + + $this->isActive = true; + + try { + // Let $result be the return value of call a user object's operation + // with traverser's filter, "acceptNode", and Node. If this throws + // an exception, then unset traverser's active flag and rethrow the + // exception. + $result = $this->filter instanceof NodeFilter + ? $this->filter->acceptNode( $node ) + : ( $this->filter )( $node ); + } catch ( Throwable $e ) { + $this->isActive = false; + + throw $e; + } + + $this->isActive = false; + + return $result; + } }