Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
TreeWalker
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 3
272
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 nextNode
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
90
 filterNode
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2
3namespace MediaWiki\Extension\DiscussionTools;
4
5use DOMException;
6use Throwable;
7use Wikimedia\Parsoid\DOM\Node;
8
9/**
10 * Partial implementation of W3 DOM4 TreeWalker interface.
11 *
12 * See also:
13 * - https://dom.spec.whatwg.org/#interface-treewalker
14 *
15 * Ported from https://github.com/TRowbotham/PHPDOM (MIT)
16 */
17class TreeWalker {
18
19    public Node $root;
20    public int $whatToShow;
21    public Node $currentNode;
22    /** @var callable */
23    public $filter;
24    private bool $isActive = false;
25
26    /**
27     * See https://dom.spec.whatwg.org/#interface-treewalker
28     *
29     * @param Node $root
30     * @param int $whatToShow
31     * @param callable|null $filter
32     */
33    public function __construct(
34        Node $root,
35        int $whatToShow = NodeFilter::SHOW_ALL,
36        callable $filter = null
37    ) {
38        $this->currentNode = $root;
39        $this->filter = $filter;
40        $this->root = $root;
41        $this->whatToShow = $whatToShow;
42    }
43
44    /**
45     * See https://dom.spec.whatwg.org/#dom-treewalker-nextnode
46     *
47     * @return Node|null The current node
48     */
49    public function nextNode(): ?Node {
50        $node = $this->currentNode;
51        $result = NodeFilter::FILTER_ACCEPT;
52
53        while ( true ) {
54            while ( $result !== NodeFilter::FILTER_REJECT && $node->firstChild !== null ) {
55                $node = $node->firstChild;
56                $result = $this->filterNode( $node );
57                if ( $result === NodeFilter::FILTER_ACCEPT ) {
58                    $this->currentNode = $node;
59                    return $node;
60                }
61            }
62
63            $sibling = null;
64            $temp = $node;
65            while ( $temp !== null ) {
66                if ( $temp === $this->root ) {
67                    return null;
68                }
69
70                $sibling = $temp->nextSibling;
71
72                if ( $sibling !== null ) {
73                    $node = $sibling;
74
75                    break;
76                }
77
78                $temp = $temp->parentNode;
79            }
80
81            '@phan-var Node $node';
82            $result = $this->filterNode( $node );
83
84            if ( $result === NodeFilter::FILTER_ACCEPT ) {
85                $this->currentNode = $node;
86
87                return $node;
88            }
89
90        }
91    }
92
93    /**
94     * Filters a node.
95     *
96     * @internal
97     *
98     * @see https://dom.spec.whatwg.org/#concept-node-filter
99     *
100     * @param Node $node The node to check.
101     * @return int Returns one of NodeFilter's FILTER_* constants.
102     *     - NodeFilter::FILTER_ACCEPT
103     *     - NodeFilter::FILTER_REJECT
104     *     - NodeFilter::FILTER_SKIP
105     */
106    private function filterNode( Node $node ): int {
107        if ( $this->isActive ) {
108            throw new DOMException( 'InvalidStateError' );
109        }
110
111        // Let n be node’s nodeType attribute value minus 1.
112        $n = $node->nodeType - 1;
113
114        // If the nth bit (where 0 is the least significant bit) of whatToShow
115        // is not set, return FILTER_SKIP.
116        if ( !( ( 1 << $n ) & $this->whatToShow ) ) {
117            return NodeFilter::FILTER_SKIP;
118        }
119
120        // If filter is null, return FILTER_ACCEPT.
121        if ( !$this->filter ) {
122            return NodeFilter::FILTER_ACCEPT;
123        }
124
125        $this->isActive = true;
126
127        try {
128            // Let $result be the return value of call a user object's operation
129            // with traverser's filter, "acceptNode", and Node. If this throws
130            // an exception, then unset traverser's active flag and rethrow the
131            // exception.
132            $result = $this->filter instanceof NodeFilter
133                ? $this->filter->acceptNode( $node )
134                : ( $this->filter )( $node );
135        } catch ( Throwable $e ) {
136            $this->isActive = false;
137
138            throw $e;
139        }
140
141        $this->isActive = false;
142
143        return $result;
144    }
145}