Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
40.35% covered (danger)
40.35%
46 / 114
26.32% covered (danger)
26.32%
5 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
PPNode_Hash_Tree
40.71% covered (danger)
40.71%
46 / 113
26.32% covered (danger)
26.32%
5 / 19
759.21
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 factory
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
5.02
 __toString
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 getChildren
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getFirstChild
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getNextSibling
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getChildrenOfType
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 getRawChildren
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLength
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 item
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 splitArg
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 splitRawArg
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
90
 splitExt
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 splitRawExt
90.00% covered (success)
90.00%
18 / 20
0.00% covered (danger)
0.00%
0 / 1
8.06
 splitHeading
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 splitRawHeading
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
42
 splitTemplate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 splitRawTemplate
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
56
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 * @ingroup Parser
6 */
7
8namespace MediaWiki\Parser;
9
10use BadMethodCallException;
11use InvalidArgumentException;
12use Stringable;
13
14/**
15 * @ingroup Parser
16 */
17// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
18class PPNode_Hash_Tree implements Stringable, PPNode {
19
20    /** @var string */
21    public $name;
22
23    /**
24     * The store array for children of this node. It is "raw" in the sense that
25     * nodes are two-element arrays ("descriptors") rather than PPNode_Hash_*
26     * objects.
27     * @var array
28     */
29    private $rawChildren;
30
31    /**
32     * The store array for the siblings of this node, including this node itself.
33     * @var array
34     */
35    private $store;
36
37    /**
38     * The index into $this->store which contains the descriptor of this node.
39     * @var int
40     */
41    private $index;
42
43    /**
44     * The offset of the name within descriptors, used in some places for
45     * readability.
46     */
47    public const NAME = 0;
48
49    /**
50     * The offset of the child list within descriptors, used in some places for
51     * readability.
52     */
53    public const CHILDREN = 1;
54
55    /**
56     * Construct an object using the data from $store[$index]. The rest of the
57     * store array can be accessed via getNextSibling().
58     *
59     * @param array $store
60     * @param int $index
61     */
62    public function __construct( array $store, $index ) {
63        $this->store = $store;
64        $this->index = $index;
65        [ $this->name, $this->rawChildren ] = $this->store[$index];
66    }
67
68    /**
69     * Construct an appropriate PPNode_Hash_* object with a class that depends
70     * on what is at the relevant store index.
71     *
72     * @param array $store
73     * @param int $index
74     * @return PPNode_Hash_Tree|PPNode_Hash_Attr|PPNode_Hash_Text|false
75     */
76    public static function factory( array $store, $index ) {
77        if ( !isset( $store[$index] ) ) {
78            return false;
79        }
80
81        $descriptor = $store[$index];
82        if ( is_string( $descriptor ) ) {
83            $class = PPNode_Hash_Text::class;
84        } elseif ( is_array( $descriptor ) ) {
85            if ( $descriptor[self::NAME][0] === '@' ) {
86                $class = PPNode_Hash_Attr::class;
87            } else {
88                $class = self::class;
89            }
90        } else {
91            throw new InvalidArgumentException( __METHOD__ . ': invalid node descriptor' );
92        }
93        return new $class( $store, $index );
94    }
95
96    /**
97     * Convert a node to XML, for debugging
98     * @return string
99     */
100    public function __toString() {
101        $inner = '';
102        $attribs = '';
103        for ( $node = $this->getFirstChild(); $node; $node = $node->getNextSibling() ) {
104            if ( $node instanceof PPNode_Hash_Attr ) {
105                $attribs .= ' ' . $node->name .
106                    '="' . htmlspecialchars( $node->value, ENT_COMPAT ) . '"';
107            } else {
108                $inner .= $node->__toString();
109            }
110        }
111        if ( $inner === '' ) {
112            return "<{$this->name}$attribs/>";
113        } else {
114            return "<{$this->name}$attribs>$inner</{$this->name}>";
115        }
116    }
117
118    /**
119     * @return PPNode_Hash_Array
120     */
121    public function getChildren() {
122        $children = [];
123        foreach ( $this->rawChildren as $i => $child ) {
124            $children[] = self::factory( $this->rawChildren, $i );
125        }
126        return new PPNode_Hash_Array( $children );
127    }
128
129    /**
130     * Get the first child, or false if there is none. Note that this will
131     * return a temporary proxy object: different instances will be returned
132     * if this is called more than once on the same node.
133     *
134     * @return PPNode_Hash_Tree|PPNode_Hash_Attr|PPNode_Hash_Text|false
135     */
136    public function getFirstChild() {
137        if ( !isset( $this->rawChildren[0] ) ) {
138            return false;
139        } else {
140            return self::factory( $this->rawChildren, 0 );
141        }
142    }
143
144    /**
145     * Get the next sibling, or false if there is none. Note that this will
146     * return a temporary proxy object: different instances will be returned
147     * if this is called more than once on the same node.
148     *
149     * @return PPNode_Hash_Tree|PPNode_Hash_Attr|PPNode_Hash_Text|false
150     */
151    public function getNextSibling() {
152        return self::factory( $this->store, $this->index + 1 );
153    }
154
155    /**
156     * Get an array of the children with a given node name
157     *
158     * @param string $name
159     * @return PPNode_Hash_Array
160     */
161    public function getChildrenOfType( $name ) {
162        $children = [];
163        foreach ( $this->rawChildren as $i => $child ) {
164            if ( is_array( $child ) && $child[self::NAME] === $name ) {
165                $children[] = self::factory( $this->rawChildren, $i );
166            }
167        }
168        return new PPNode_Hash_Array( $children );
169    }
170
171    /**
172     * Get the raw child array. For internal use.
173     * @return array
174     */
175    public function getRawChildren() {
176        return $this->rawChildren;
177    }
178
179    /**
180     * @return bool
181     */
182    public function getLength() {
183        return false;
184    }
185
186    /**
187     * @param int $i
188     * @return bool
189     */
190    public function item( $i ) {
191        return false;
192    }
193
194    /**
195     * @return string
196     */
197    public function getName() {
198        return $this->name;
199    }
200
201    /**
202     * Split a "<part>" node into an associative array containing:
203     *  - name          PPNode name
204     *  - index         String index
205     *  - value         PPNode value
206     *
207     * @return array{name:self,index:string|int,value:self}
208     */
209    public function splitArg() {
210        return self::splitRawArg( $this->rawChildren );
211    }
212
213    /**
214     * Like splitArg() but for a raw child array. For internal use only.
215     * @param array $children
216     * @return array{name:self,index:string|int,value:self}
217     */
218    public static function splitRawArg( array $children ) {
219        $bits = [];
220        foreach ( $children as $i => $child ) {
221            if ( !is_array( $child ) ) {
222                continue;
223            }
224            if ( $child[self::NAME] === 'name' ) {
225                $bits['name'] = new self( $children, $i );
226                if ( isset( $child[self::CHILDREN][0][self::NAME] )
227                    && $child[self::CHILDREN][0][self::NAME] === '@index'
228                ) {
229                    $bits['index'] = $child[self::CHILDREN][0][self::CHILDREN][0];
230                }
231            } elseif ( $child[self::NAME] === 'value' ) {
232                $bits['value'] = new self( $children, $i );
233            }
234        }
235
236        if ( !isset( $bits['name'] ) ) {
237            throw new InvalidArgumentException( 'Invalid brace node passed to ' . __METHOD__ );
238        }
239        if ( !isset( $bits['index'] ) ) {
240            $bits['index'] = '';
241        }
242        return $bits;
243    }
244
245    /**
246     * Split an "<ext>" node into an associative array containing name, attr, inner and close
247     * All values in the resulting array are PPNodes. Inner and close are optional.
248     *
249     * @return array{name:self,attr?:self,inner?:self,close?:self}
250     */
251    public function splitExt() {
252        return self::splitRawExt( $this->rawChildren );
253    }
254
255    /**
256     * Like splitExt() but for a raw child array. For internal use only.
257     * @param array $children
258     * @return array{name:self,attr?:self,inner?:self,close?:self}
259     */
260    public static function splitRawExt( array $children ) {
261        $bits = [];
262        foreach ( $children as $i => $child ) {
263            if ( !is_array( $child ) ) {
264                continue;
265            }
266            switch ( $child[self::NAME] ) {
267                case 'name':
268                    $bits['name'] = new self( $children, $i );
269                    break;
270                case 'attr':
271                    $bits['attr'] = new self( $children, $i );
272                    break;
273                case 'inner':
274                    $bits['inner'] = new self( $children, $i );
275                    break;
276                case 'close':
277                    $bits['close'] = new self( $children, $i );
278                    break;
279            }
280        }
281        if ( !isset( $bits['name'] ) ) {
282            throw new InvalidArgumentException( 'Invalid ext node passed to ' . __METHOD__ );
283        }
284        return $bits;
285    }
286
287    /**
288     * Split an "<h>" node
289     *
290     * @return array{i:int,level:int}
291     */
292    public function splitHeading() {
293        if ( $this->name !== 'h' ) {
294            throw new BadMethodCallException( 'Invalid h node passed to ' . __METHOD__ );
295        }
296        return self::splitRawHeading( $this->rawChildren );
297    }
298
299    /**
300     * Like splitHeading() but for a raw child array. For internal use only.
301     * @param array $children
302     * @return array{i:int,level:int}
303     */
304    public static function splitRawHeading( array $children ) {
305        $bits = [];
306        foreach ( $children as $child ) {
307            if ( !is_array( $child ) ) {
308                continue;
309            }
310            if ( $child[self::NAME] === '@i' ) {
311                $bits['i'] = $child[self::CHILDREN][0];
312            } elseif ( $child[self::NAME] === '@level' ) {
313                $bits['level'] = $child[self::CHILDREN][0];
314            }
315        }
316        if ( !isset( $bits['i'] ) ) {
317            throw new InvalidArgumentException( 'Invalid h node passed to ' . __METHOD__ );
318        }
319        return $bits;
320    }
321
322    /**
323     * Split a "<template>" or "<tplarg>" node
324     *
325     * @return array
326     */
327    public function splitTemplate() {
328        return self::splitRawTemplate( $this->rawChildren );
329    }
330
331    /**
332     * Like splitTemplate() but for a raw child array. For internal use only.
333     * @param array $children
334     * @return array
335     * @suppress SecurityCheck-XSS
336     */
337    public static function splitRawTemplate( array $children ) {
338        $parts = [];
339        $bits = [ 'lineStart' => '' ];
340        foreach ( $children as $i => $child ) {
341            if ( !is_array( $child ) ) {
342                continue;
343            }
344            switch ( $child[self::NAME] ) {
345                case 'title':
346                    $bits['title'] = new self( $children, $i );
347                    break;
348                case 'part':
349                    $parts[] = new self( $children, $i );
350                    break;
351                case '@lineStart':
352                    $bits['lineStart'] = '1';
353                    break;
354            }
355        }
356        if ( !isset( $bits['title'] ) ) {
357            throw new InvalidArgumentException( 'Invalid node passed to ' . __METHOD__ );
358        }
359        $bits['parts'] = new PPNode_Hash_Array( $parts );
360        return $bits;
361    }
362}
363
364/** @deprecated class alias since 1.43 */
365class_alias( PPNode_Hash_Tree::class, 'PPNode_Hash_Tree' );