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 Exception;
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 $root;
20    public $whatToShow;
21    public $currentNode;
22    public $filter;
23
24    private $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     * @throws Exception
106     */
107    private function filterNode( Node $node ): int {
108        if ( $this->isActive ) {
109            throw new Exception( 'InvalidStateError' );
110        }
111
112        // Let n be node’s nodeType attribute value minus 1.
113        $n = $node->nodeType - 1;
114
115        // If the nth bit (where 0 is the least significant bit) of whatToShow
116        // is not set, return FILTER_SKIP.
117        if ( !( ( 1 << $n ) & $this->whatToShow ) ) {
118            return NodeFilter::FILTER_SKIP;
119        }
120
121        // If filter is null, return FILTER_ACCEPT.
122        if ( !$this->filter ) {
123            return NodeFilter::FILTER_ACCEPT;
124        }
125
126        $this->isActive = true;
127
128        try {
129            // Let $result be the return value of call a user object's operation
130            // with traverser's filter, "acceptNode", and Node. If this throws
131            // an exception, then unset traverser's active flag and rethrow the
132            // exception.
133            $result = $this->filter instanceof NodeFilter
134                ? $this->filter->acceptNode( $node )
135                : ( $this->filter )( $node );
136        } catch ( Throwable $e ) {
137            $this->isActive = false;
138
139            throw $e;
140        }
141
142        $this->isActive = false;
143
144        return $result;
145    }
146}