]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/parser/Preprocessor_Hash.php
MediaWiki 1.30.2-scripts2
[autoinstalls/mediawiki.git] / includes / parser / Preprocessor_Hash.php
1 <?php
2 /**
3  * Preprocessor using PHP arrays
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18  * http://www.gnu.org/copyleft/gpl.html
19  *
20  * @file
21  * @ingroup Parser
22  */
23
24 /**
25  * Differences from DOM schema:
26  *   * attribute nodes are children
27  *   * "<h>" nodes that aren't at the top are replaced with <possible-h>
28  *
29  * Nodes are stored in a recursive array data structure. A node store is an
30  * array where each element may be either a scalar (representing a text node)
31  * or a "descriptor", which is a two-element array where the first element is
32  * the node name and the second element is the node store for the children.
33  *
34  * Attributes are represented as children that have a node name starting with
35  * "@", and a single text node child.
36  *
37  * @todo: Consider replacing descriptor arrays with objects of a new class.
38  * Benchmark and measure resulting memory impact.
39  *
40  * @ingroup Parser
41  */
42 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
43 class Preprocessor_Hash extends Preprocessor {
44         // @codingStandardsIgnoreEnd
45
46         /**
47          * @var Parser
48          */
49         public $parser;
50
51         const CACHE_PREFIX = 'preprocess-hash';
52         const CACHE_VERSION = 2;
53
54         public function __construct( $parser ) {
55                 $this->parser = $parser;
56         }
57
58         /**
59          * @return PPFrame_Hash
60          */
61         public function newFrame() {
62                 return new PPFrame_Hash( $this );
63         }
64
65         /**
66          * @param array $args
67          * @return PPCustomFrame_Hash
68          */
69         public function newCustomFrame( $args ) {
70                 return new PPCustomFrame_Hash( $this, $args );
71         }
72
73         /**
74          * @param array $values
75          * @return PPNode_Hash_Array
76          */
77         public function newPartNodeArray( $values ) {
78                 $list = [];
79
80                 foreach ( $values as $k => $val ) {
81                         if ( is_int( $k ) ) {
82                                 $store = [ [ 'part', [
83                                         [ 'name', [ [ '@index', [ $k ] ] ] ],
84                                         [ 'value', [ strval( $val ) ] ],
85                                 ] ] ];
86                         } else {
87                                 $store = [ [ 'part', [
88                                         [ 'name', [ strval( $k ) ] ],
89                                         '=',
90                                         [ 'value', [ strval( $val ) ] ],
91                                 ] ] ];
92                         }
93
94                         $list[] = new PPNode_Hash_Tree( $store, 0 );
95                 }
96
97                 $node = new PPNode_Hash_Array( $list );
98                 return $node;
99         }
100
101         /**
102          * Preprocess some wikitext and return the document tree.
103          *
104          * @param string $text The text to parse
105          * @param int $flags Bitwise combination of:
106          *    Parser::PTD_FOR_INCLUSION    Handle "<noinclude>" and "<includeonly>" as if the text is being
107          *                                 included. Default is to assume a direct page view.
108          *
109          * The generated DOM tree must depend only on the input text and the flags.
110          * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of T6899.
111          *
112          * Any flag added to the $flags parameter here, or any other parameter liable to cause a
113          * change in the DOM tree for a given text, must be passed through the section identifier
114          * in the section edit link and thus back to extractSections().
115          *
116          * @throws MWException
117          * @return PPNode_Hash_Tree
118          */
119         public function preprocessToObj( $text, $flags = 0 ) {
120                 global $wgDisableLangConversion;
121
122                 $tree = $this->cacheGetTree( $text, $flags );
123                 if ( $tree !== false ) {
124                         $store = json_decode( $tree );
125                         if ( is_array( $store ) ) {
126                                 return new PPNode_Hash_Tree( $store, 0 );
127                         }
128                 }
129
130                 $forInclusion = $flags & Parser::PTD_FOR_INCLUSION;
131
132                 $xmlishElements = $this->parser->getStripList();
133                 $xmlishAllowMissingEndTag = [ 'includeonly', 'noinclude', 'onlyinclude' ];
134                 $enableOnlyinclude = false;
135                 if ( $forInclusion ) {
136                         $ignoredTags = [ 'includeonly', '/includeonly' ];
137                         $ignoredElements = [ 'noinclude' ];
138                         $xmlishElements[] = 'noinclude';
139                         if ( strpos( $text, '<onlyinclude>' ) !== false
140                                 && strpos( $text, '</onlyinclude>' ) !== false
141                         ) {
142                                 $enableOnlyinclude = true;
143                         }
144                 } else {
145                         $ignoredTags = [ 'noinclude', '/noinclude', 'onlyinclude', '/onlyinclude' ];
146                         $ignoredElements = [ 'includeonly' ];
147                         $xmlishElements[] = 'includeonly';
148                 }
149                 $xmlishRegex = implode( '|', array_merge( $xmlishElements, $ignoredTags ) );
150
151                 // Use "A" modifier (anchored) instead of "^", because ^ doesn't work with an offset
152                 $elementsRegex = "~($xmlishRegex)(?:\s|\/>|>)|(!--)~iA";
153
154                 $stack = new PPDStack_Hash;
155
156                 $searchBase = "[{<\n";
157                 if ( !$wgDisableLangConversion ) {
158                         $searchBase .= '-';
159                 }
160
161                 // For fast reverse searches
162                 $revText = strrev( $text );
163                 $lengthText = strlen( $text );
164
165                 // Input pointer, starts out pointing to a pseudo-newline before the start
166                 $i = 0;
167                 // Current accumulator. See the doc comment for Preprocessor_Hash for the format.
168                 $accum =& $stack->getAccum();
169                 // True to find equals signs in arguments
170                 $findEquals = false;
171                 // True to take notice of pipe characters
172                 $findPipe = false;
173                 $headingIndex = 1;
174                 // True if $i is inside a possible heading
175                 $inHeading = false;
176                 // True if there are no more greater-than (>) signs right of $i
177                 $noMoreGT = false;
178                 // Map of tag name => true if there are no more closing tags of given type right of $i
179                 $noMoreClosingTag = [];
180                 // True to ignore all input up to the next <onlyinclude>
181                 $findOnlyinclude = $enableOnlyinclude;
182                 // Do a line-start run without outputting an LF character
183                 $fakeLineStart = true;
184
185                 while ( true ) {
186                         // $this->memCheck();
187
188                         if ( $findOnlyinclude ) {
189                                 // Ignore all input up to the next <onlyinclude>
190                                 $startPos = strpos( $text, '<onlyinclude>', $i );
191                                 if ( $startPos === false ) {
192                                         // Ignored section runs to the end
193                                         $accum[] = [ 'ignore', [ substr( $text, $i ) ] ];
194                                         break;
195                                 }
196                                 $tagEndPos = $startPos + strlen( '<onlyinclude>' ); // past-the-end
197                                 $accum[] = [ 'ignore', [ substr( $text, $i, $tagEndPos - $i ) ] ];
198                                 $i = $tagEndPos;
199                                 $findOnlyinclude = false;
200                         }
201
202                         if ( $fakeLineStart ) {
203                                 $found = 'line-start';
204                                 $curChar = '';
205                         } else {
206                                 # Find next opening brace, closing brace or pipe
207                                 $search = $searchBase;
208                                 if ( $stack->top === false ) {
209                                         $currentClosing = '';
210                                 } elseif (
211                                         $stack->top->close === '}-' &&
212                                         $stack->top->count > 2
213                                 ) {
214                                         # adjust closing for -{{{...{{
215                                         $currentClosing = '}';
216                                         $search .= $currentClosing;
217                                 } else {
218                                         $currentClosing = $stack->top->close;
219                                         $search .= $currentClosing;
220                                 }
221                                 if ( $findPipe ) {
222                                         $search .= '|';
223                                 }
224                                 if ( $findEquals ) {
225                                         // First equals will be for the template
226                                         $search .= '=';
227                                 }
228                                 $rule = null;
229                                 # Output literal section, advance input counter
230                                 $literalLength = strcspn( $text, $search, $i );
231                                 if ( $literalLength > 0 ) {
232                                         self::addLiteral( $accum, substr( $text, $i, $literalLength ) );
233                                         $i += $literalLength;
234                                 }
235                                 if ( $i >= $lengthText ) {
236                                         if ( $currentClosing == "\n" ) {
237                                                 // Do a past-the-end run to finish off the heading
238                                                 $curChar = '';
239                                                 $found = 'line-end';
240                                         } else {
241                                                 # All done
242                                                 break;
243                                         }
244                                 } else {
245                                         $curChar = $curTwoChar = $text[$i];
246                                         if ( ( $i + 1 ) < $lengthText ) {
247                                                 $curTwoChar .= $text[$i + 1];
248                                         }
249                                         if ( $curChar == '|' ) {
250                                                 $found = 'pipe';
251                                         } elseif ( $curChar == '=' ) {
252                                                 $found = 'equals';
253                                         } elseif ( $curChar == '<' ) {
254                                                 $found = 'angle';
255                                         } elseif ( $curChar == "\n" ) {
256                                                 if ( $inHeading ) {
257                                                         $found = 'line-end';
258                                                 } else {
259                                                         $found = 'line-start';
260                                                 }
261                                         } elseif ( $curTwoChar == $currentClosing ) {
262                                                 $found = 'close';
263                                                 $curChar = $curTwoChar;
264                                         } elseif ( $curChar == $currentClosing ) {
265                                                 $found = 'close';
266                                         } elseif ( isset( $this->rules[$curTwoChar] ) ) {
267                                                 $curChar = $curTwoChar;
268                                                 $found = 'open';
269                                                 $rule = $this->rules[$curChar];
270                                         } elseif ( isset( $this->rules[$curChar] ) ) {
271                                                 $found = 'open';
272                                                 $rule = $this->rules[$curChar];
273                                         } else {
274                                                 # Some versions of PHP have a strcspn which stops on
275                                                 # null characters; ignore these and continue.
276                                                 # We also may get '-' and '}' characters here which
277                                                 # don't match -{ or $currentClosing.  Add these to
278                                                 # output and continue.
279                                                 if ( $curChar == '-' || $curChar == '}' ) {
280                                                         self::addLiteral( $accum, $curChar );
281                                                 }
282                                                 ++$i;
283                                                 continue;
284                                         }
285                                 }
286                         }
287
288                         if ( $found == 'angle' ) {
289                                 $matches = false;
290                                 // Handle </onlyinclude>
291                                 if ( $enableOnlyinclude
292                                         && substr( $text, $i, strlen( '</onlyinclude>' ) ) == '</onlyinclude>'
293                                 ) {
294                                         $findOnlyinclude = true;
295                                         continue;
296                                 }
297
298                                 // Determine element name
299                                 if ( !preg_match( $elementsRegex, $text, $matches, 0, $i + 1 ) ) {
300                                         // Element name missing or not listed
301                                         self::addLiteral( $accum, '<' );
302                                         ++$i;
303                                         continue;
304                                 }
305                                 // Handle comments
306                                 if ( isset( $matches[2] ) && $matches[2] == '!--' ) {
307                                         // To avoid leaving blank lines, when a sequence of
308                                         // space-separated comments is both preceded and followed by
309                                         // a newline (ignoring spaces), then
310                                         // trim leading and trailing spaces and the trailing newline.
311
312                                         // Find the end
313                                         $endPos = strpos( $text, '-->', $i + 4 );
314                                         if ( $endPos === false ) {
315                                                 // Unclosed comment in input, runs to end
316                                                 $inner = substr( $text, $i );
317                                                 $accum[] = [ 'comment', [ $inner ] ];
318                                                 $i = $lengthText;
319                                         } else {
320                                                 // Search backwards for leading whitespace
321                                                 $wsStart = $i ? ( $i - strspn( $revText, " \t", $lengthText - $i ) ) : 0;
322
323                                                 // Search forwards for trailing whitespace
324                                                 // $wsEnd will be the position of the last space (or the '>' if there's none)
325                                                 $wsEnd = $endPos + 2 + strspn( $text, " \t", $endPos + 3 );
326
327                                                 // Keep looking forward as long as we're finding more
328                                                 // comments.
329                                                 $comments = [ [ $wsStart, $wsEnd ] ];
330                                                 while ( substr( $text, $wsEnd + 1, 4 ) == '<!--' ) {
331                                                         $c = strpos( $text, '-->', $wsEnd + 4 );
332                                                         if ( $c === false ) {
333                                                                 break;
334                                                         }
335                                                         $c = $c + 2 + strspn( $text, " \t", $c + 3 );
336                                                         $comments[] = [ $wsEnd + 1, $c ];
337                                                         $wsEnd = $c;
338                                                 }
339
340                                                 // Eat the line if possible
341                                                 // TODO: This could theoretically be done if $wsStart == 0, i.e. for comments at
342                                                 // the overall start. That's not how Sanitizer::removeHTMLcomments() did it, but
343                                                 // it's a possible beneficial b/c break.
344                                                 if ( $wsStart > 0 && substr( $text, $wsStart - 1, 1 ) == "\n"
345                                                         && substr( $text, $wsEnd + 1, 1 ) == "\n"
346                                                 ) {
347                                                         // Remove leading whitespace from the end of the accumulator
348                                                         $wsLength = $i - $wsStart;
349                                                         $endIndex = count( $accum ) - 1;
350
351                                                         // Sanity check
352                                                         if ( $wsLength > 0
353                                                                 && $endIndex >= 0
354                                                                 && is_string( $accum[$endIndex] )
355                                                                 && strspn( $accum[$endIndex], " \t", -$wsLength ) === $wsLength
356                                                         ) {
357                                                                 $accum[$endIndex] = substr( $accum[$endIndex], 0, -$wsLength );
358                                                         }
359
360                                                         // Dump all but the last comment to the accumulator
361                                                         foreach ( $comments as $j => $com ) {
362                                                                 $startPos = $com[0];
363                                                                 $endPos = $com[1] + 1;
364                                                                 if ( $j == ( count( $comments ) - 1 ) ) {
365                                                                         break;
366                                                                 }
367                                                                 $inner = substr( $text, $startPos, $endPos - $startPos );
368                                                                 $accum[] = [ 'comment', [ $inner ] ];
369                                                         }
370
371                                                         // Do a line-start run next time to look for headings after the comment
372                                                         $fakeLineStart = true;
373                                                 } else {
374                                                         // No line to eat, just take the comment itself
375                                                         $startPos = $i;
376                                                         $endPos += 2;
377                                                 }
378
379                                                 if ( $stack->top ) {
380                                                         $part = $stack->top->getCurrentPart();
381                                                         if ( !( isset( $part->commentEnd ) && $part->commentEnd == $wsStart - 1 ) ) {
382                                                                 $part->visualEnd = $wsStart;
383                                                         }
384                                                         // Else comments abutting, no change in visual end
385                                                         $part->commentEnd = $endPos;
386                                                 }
387                                                 $i = $endPos + 1;
388                                                 $inner = substr( $text, $startPos, $endPos - $startPos + 1 );
389                                                 $accum[] = [ 'comment', [ $inner ] ];
390                                         }
391                                         continue;
392                                 }
393                                 $name = $matches[1];
394                                 $lowerName = strtolower( $name );
395                                 $attrStart = $i + strlen( $name ) + 1;
396
397                                 // Find end of tag
398                                 $tagEndPos = $noMoreGT ? false : strpos( $text, '>', $attrStart );
399                                 if ( $tagEndPos === false ) {
400                                         // Infinite backtrack
401                                         // Disable tag search to prevent worst-case O(N^2) performance
402                                         $noMoreGT = true;
403                                         self::addLiteral( $accum, '<' );
404                                         ++$i;
405                                         continue;
406                                 }
407
408                                 // Handle ignored tags
409                                 if ( in_array( $lowerName, $ignoredTags ) ) {
410                                         $accum[] = [ 'ignore', [ substr( $text, $i, $tagEndPos - $i + 1 ) ] ];
411                                         $i = $tagEndPos + 1;
412                                         continue;
413                                 }
414
415                                 $tagStartPos = $i;
416                                 if ( $text[$tagEndPos - 1] == '/' ) {
417                                         // Short end tag
418                                         $attrEnd = $tagEndPos - 1;
419                                         $inner = null;
420                                         $i = $tagEndPos + 1;
421                                         $close = null;
422                                 } else {
423                                         $attrEnd = $tagEndPos;
424                                         // Find closing tag
425                                         if (
426                                                 !isset( $noMoreClosingTag[$name] ) &&
427                                                 preg_match( "/<\/" . preg_quote( $name, '/' ) . "\s*>/i",
428                                                         $text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 )
429                                         ) {
430                                                 $inner = substr( $text, $tagEndPos + 1, $matches[0][1] - $tagEndPos - 1 );
431                                                 $i = $matches[0][1] + strlen( $matches[0][0] );
432                                                 $close = $matches[0][0];
433                                         } else {
434                                                 // No end tag
435                                                 if ( in_array( $name, $xmlishAllowMissingEndTag ) ) {
436                                                         // Let it run out to the end of the text.
437                                                         $inner = substr( $text, $tagEndPos + 1 );
438                                                         $i = $lengthText;
439                                                         $close = null;
440                                                 } else {
441                                                         // Don't match the tag, treat opening tag as literal and resume parsing.
442                                                         $i = $tagEndPos + 1;
443                                                         self::addLiteral( $accum,
444                                                                 substr( $text, $tagStartPos, $tagEndPos + 1 - $tagStartPos ) );
445                                                         // Cache results, otherwise we have O(N^2) performance for input like <foo><foo><foo>...
446                                                         $noMoreClosingTag[$name] = true;
447                                                         continue;
448                                                 }
449                                         }
450                                 }
451                                 // <includeonly> and <noinclude> just become <ignore> tags
452                                 if ( in_array( $lowerName, $ignoredElements ) ) {
453                                         $accum[] = [ 'ignore', [ substr( $text, $tagStartPos, $i - $tagStartPos ) ] ];
454                                         continue;
455                                 }
456
457                                 if ( $attrEnd <= $attrStart ) {
458                                         $attr = '';
459                                 } else {
460                                         // Note that the attr element contains the whitespace between name and attribute,
461                                         // this is necessary for precise reconstruction during pre-save transform.
462                                         $attr = substr( $text, $attrStart, $attrEnd - $attrStart );
463                                 }
464
465                                 $children = [
466                                         [ 'name', [ $name ] ],
467                                         [ 'attr', [ $attr ] ] ];
468                                 if ( $inner !== null ) {
469                                         $children[] = [ 'inner', [ $inner ] ];
470                                 }
471                                 if ( $close !== null ) {
472                                         $children[] = [ 'close', [ $close ] ];
473                                 }
474                                 $accum[] = [ 'ext', $children ];
475                         } elseif ( $found == 'line-start' ) {
476                                 // Is this the start of a heading?
477                                 // Line break belongs before the heading element in any case
478                                 if ( $fakeLineStart ) {
479                                         $fakeLineStart = false;
480                                 } else {
481                                         self::addLiteral( $accum, $curChar );
482                                         $i++;
483                                 }
484
485                                 $count = strspn( $text, '=', $i, 6 );
486                                 if ( $count == 1 && $findEquals ) {
487                                         // DWIM: This looks kind of like a name/value separator.
488                                         // Let's let the equals handler have it and break the potential
489                                         // heading. This is heuristic, but AFAICT the methods for
490                                         // completely correct disambiguation are very complex.
491                                 } elseif ( $count > 0 ) {
492                                         $piece = [
493                                                 'open' => "\n",
494                                                 'close' => "\n",
495                                                 'parts' => [ new PPDPart_Hash( str_repeat( '=', $count ) ) ],
496                                                 'startPos' => $i,
497                                                 'count' => $count ];
498                                         $stack->push( $piece );
499                                         $accum =& $stack->getAccum();
500                                         extract( $stack->getFlags() );
501                                         $i += $count;
502                                 }
503                         } elseif ( $found == 'line-end' ) {
504                                 $piece = $stack->top;
505                                 // A heading must be open, otherwise \n wouldn't have been in the search list
506                                 assert( $piece->open === "\n" );
507                                 $part = $piece->getCurrentPart();
508                                 // Search back through the input to see if it has a proper close.
509                                 // Do this using the reversed string since the other solutions
510                                 // (end anchor, etc.) are inefficient.
511                                 $wsLength = strspn( $revText, " \t", $lengthText - $i );
512                                 $searchStart = $i - $wsLength;
513                                 if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) {
514                                         // Comment found at line end
515                                         // Search for equals signs before the comment
516                                         $searchStart = $part->visualEnd;
517                                         $searchStart -= strspn( $revText, " \t", $lengthText - $searchStart );
518                                 }
519                                 $count = $piece->count;
520                                 $equalsLength = strspn( $revText, '=', $lengthText - $searchStart );
521                                 if ( $equalsLength > 0 ) {
522                                         if ( $searchStart - $equalsLength == $piece->startPos ) {
523                                                 // This is just a single string of equals signs on its own line
524                                                 // Replicate the doHeadings behavior /={count}(.+)={count}/
525                                                 // First find out how many equals signs there really are (don't stop at 6)
526                                                 $count = $equalsLength;
527                                                 if ( $count < 3 ) {
528                                                         $count = 0;
529                                                 } else {
530                                                         $count = min( 6, intval( ( $count - 1 ) / 2 ) );
531                                                 }
532                                         } else {
533                                                 $count = min( $equalsLength, $count );
534                                         }
535                                         if ( $count > 0 ) {
536                                                 // Normal match, output <h>
537                                                 $element = [ [ 'possible-h',
538                                                         array_merge(
539                                                                 [
540                                                                         [ '@level', [ $count ] ],
541                                                                         [ '@i', [ $headingIndex++ ] ]
542                                                                 ],
543                                                                 $accum
544                                                         )
545                                                 ] ];
546                                         } else {
547                                                 // Single equals sign on its own line, count=0
548                                                 $element = $accum;
549                                         }
550                                 } else {
551                                         // No match, no <h>, just pass down the inner text
552                                         $element = $accum;
553                                 }
554                                 // Unwind the stack
555                                 $stack->pop();
556                                 $accum =& $stack->getAccum();
557                                 extract( $stack->getFlags() );
558
559                                 // Append the result to the enclosing accumulator
560                                 array_splice( $accum, count( $accum ), 0, $element );
561
562                                 // Note that we do NOT increment the input pointer.
563                                 // This is because the closing linebreak could be the opening linebreak of
564                                 // another heading. Infinite loops are avoided because the next iteration MUST
565                                 // hit the heading open case above, which unconditionally increments the
566                                 // input pointer.
567                         } elseif ( $found == 'open' ) {
568                                 # count opening brace characters
569                                 $curLen = strlen( $curChar );
570                                 $count = ( $curLen > 1 ) ?
571                                         # allow the final character to repeat
572                                         strspn( $text, $curChar[$curLen - 1], $i + 1 ) + 1 :
573                                         strspn( $text, $curChar, $i );
574
575                                 # we need to add to stack only if opening brace count is enough for one of the rules
576                                 if ( $count >= $rule['min'] ) {
577                                         # Add it to the stack
578                                         $piece = [
579                                                 'open' => $curChar,
580                                                 'close' => $rule['end'],
581                                                 'count' => $count,
582                                                 'lineStart' => ( $i > 0 && $text[$i - 1] == "\n" ),
583                                         ];
584
585                                         $stack->push( $piece );
586                                         $accum =& $stack->getAccum();
587                                         extract( $stack->getFlags() );
588                                 } else {
589                                         # Add literal brace(s)
590                                         self::addLiteral( $accum, str_repeat( $curChar, $count ) );
591                                 }
592                                 $i += $count;
593                         } elseif ( $found == 'close' ) {
594                                 $piece = $stack->top;
595                                 # lets check if there are enough characters for closing brace
596                                 $maxCount = $piece->count;
597                                 if ( $piece->close === '}-' && $curChar === '}' ) {
598                                         $maxCount--; # don't try to match closing '-' as a '}'
599                                 }
600                                 $curLen = strlen( $curChar );
601                                 $count = ( $curLen > 1 ) ? $curLen :
602                                         strspn( $text, $curChar, $i, $maxCount );
603
604                                 # check for maximum matching characters (if there are 5 closing
605                                 # characters, we will probably need only 3 - depending on the rules)
606                                 $rule = $this->rules[$piece->open];
607                                 if ( $piece->close === '}-' && $piece->count > 2 ) {
608                                         # tweak for -{..{{ }}..}-
609                                         $rule = $this->rules['{'];
610                                 }
611                                 if ( $count > $rule['max'] ) {
612                                         # The specified maximum exists in the callback array, unless the caller
613                                         # has made an error
614                                         $matchingCount = $rule['max'];
615                                 } else {
616                                         # Count is less than the maximum
617                                         # Skip any gaps in the callback array to find the true largest match
618                                         # Need to use array_key_exists not isset because the callback can be null
619                                         $matchingCount = $count;
620                                         while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $rule['names'] ) ) {
621                                                 --$matchingCount;
622                                         }
623                                 }
624
625                                 if ( $matchingCount <= 0 ) {
626                                         # No matching element found in callback array
627                                         # Output a literal closing brace and continue
628                                         $endText = substr( $text, $i, $count );
629                                         self::addLiteral( $accum, $endText );
630                                         $i += $count;
631                                         continue;
632                                 }
633                                 $name = $rule['names'][$matchingCount];
634                                 if ( $name === null ) {
635                                         // No element, just literal text
636                                         $endText = substr( $text, $i, $matchingCount );
637                                         $element = $piece->breakSyntax( $matchingCount );
638                                         self::addLiteral( $element, $endText );
639                                 } else {
640                                         # Create XML element
641                                         $parts = $piece->parts;
642                                         $titleAccum = $parts[0]->out;
643                                         unset( $parts[0] );
644
645                                         $children = [];
646
647                                         # The invocation is at the start of the line if lineStart is set in
648                                         # the stack, and all opening brackets are used up.
649                                         if ( $maxCount == $matchingCount && !empty( $piece->lineStart ) ) {
650                                                 $children[] = [ '@lineStart', [ 1 ] ];
651                                         }
652                                         $titleNode = [ 'title', $titleAccum ];
653                                         $children[] = $titleNode;
654                                         $argIndex = 1;
655                                         foreach ( $parts as $part ) {
656                                                 if ( isset( $part->eqpos ) ) {
657                                                         $equalsNode = $part->out[$part->eqpos];
658                                                         $nameNode = [ 'name', array_slice( $part->out, 0, $part->eqpos ) ];
659                                                         $valueNode = [ 'value', array_slice( $part->out, $part->eqpos + 1 ) ];
660                                                         $partNode = [ 'part', [ $nameNode, $equalsNode, $valueNode ] ];
661                                                         $children[] = $partNode;
662                                                 } else {
663                                                         $nameNode = [ 'name', [ [ '@index', [ $argIndex++ ] ] ] ];
664                                                         $valueNode = [ 'value', $part->out ];
665                                                         $partNode = [ 'part', [ $nameNode, $valueNode ] ];
666                                                         $children[] = $partNode;
667                                                 }
668                                         }
669                                         $element = [ [ $name, $children ] ];
670                                 }
671
672                                 # Advance input pointer
673                                 $i += $matchingCount;
674
675                                 # Unwind the stack
676                                 $stack->pop();
677                                 $accum =& $stack->getAccum();
678
679                                 # Re-add the old stack element if it still has unmatched opening characters remaining
680                                 if ( $matchingCount < $piece->count ) {
681                                         $piece->parts = [ new PPDPart_Hash ];
682                                         $piece->count -= $matchingCount;
683                                         # do we still qualify for any callback with remaining count?
684                                         $min = $this->rules[$piece->open]['min'];
685                                         if ( $piece->count >= $min ) {
686                                                 $stack->push( $piece );
687                                                 $accum =& $stack->getAccum();
688                                         } else {
689                                                 $s = substr( $piece->open, 0, -1 );
690                                                 $s .= str_repeat(
691                                                         substr( $piece->open, -1 ),
692                                                         $piece->count - strlen( $s )
693                                                 );
694                                                 self::addLiteral( $accum, $s );
695                                         }
696                                 }
697
698                                 extract( $stack->getFlags() );
699
700                                 # Add XML element to the enclosing accumulator
701                                 array_splice( $accum, count( $accum ), 0, $element );
702                         } elseif ( $found == 'pipe' ) {
703                                 $findEquals = true; // shortcut for getFlags()
704                                 $stack->addPart();
705                                 $accum =& $stack->getAccum();
706                                 ++$i;
707                         } elseif ( $found == 'equals' ) {
708                                 $findEquals = false; // shortcut for getFlags()
709                                 $accum[] = [ 'equals', [ '=' ] ];
710                                 $stack->getCurrentPart()->eqpos = count( $accum ) - 1;
711                                 ++$i;
712                         } elseif ( $found == 'dash' ) {
713                                 self::addLiteral( $accum, '-' );
714                                 ++$i;
715                         }
716                 }
717
718                 # Output any remaining unclosed brackets
719                 foreach ( $stack->stack as $piece ) {
720                         array_splice( $stack->rootAccum, count( $stack->rootAccum ), 0, $piece->breakSyntax() );
721                 }
722
723                 # Enable top-level headings
724                 foreach ( $stack->rootAccum as &$node ) {
725                         if ( is_array( $node ) && $node[PPNode_Hash_Tree::NAME] === 'possible-h' ) {
726                                 $node[PPNode_Hash_Tree::NAME] = 'h';
727                         }
728                 }
729
730                 $rootStore = [ [ 'root', $stack->rootAccum ] ];
731                 $rootNode = new PPNode_Hash_Tree( $rootStore, 0 );
732
733                 // Cache
734                 $tree = json_encode( $rootStore, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
735                 if ( $tree !== false ) {
736                         $this->cacheSetTree( $text, $flags, $tree );
737                 }
738
739                 return $rootNode;
740         }
741
742         private static function addLiteral( array &$accum, $text ) {
743                 $n = count( $accum );
744                 if ( $n && is_string( $accum[$n - 1] ) ) {
745                         $accum[$n - 1] .= $text;
746                 } else {
747                         $accum[] = $text;
748                 }
749         }
750 }
751
752 /**
753  * Stack class to help Preprocessor::preprocessToObj()
754  * @ingroup Parser
755  */
756 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
757 class PPDStack_Hash extends PPDStack {
758         // @codingStandardsIgnoreEnd
759
760         public function __construct() {
761                 $this->elementClass = 'PPDStackElement_Hash';
762                 parent::__construct();
763                 $this->rootAccum = [];
764         }
765 }
766
767 /**
768  * @ingroup Parser
769  */
770 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
771 class PPDStackElement_Hash extends PPDStackElement {
772         // @codingStandardsIgnoreEnd
773
774         public function __construct( $data = [] ) {
775                 $this->partClass = 'PPDPart_Hash';
776                 parent::__construct( $data );
777         }
778
779         /**
780          * Get the accumulator that would result if the close is not found.
781          *
782          * @param int|bool $openingCount
783          * @return array
784          */
785         public function breakSyntax( $openingCount = false ) {
786                 if ( $this->open == "\n" ) {
787                         $accum = $this->parts[0]->out;
788                 } else {
789                         if ( $openingCount === false ) {
790                                 $openingCount = $this->count;
791                         }
792                         $s = substr( $this->open, 0, -1 );
793                         $s .= str_repeat(
794                                 substr( $this->open, -1 ),
795                                 $openingCount - strlen( $s )
796                         );
797                         $accum = [ $s ];
798                         $lastIndex = 0;
799                         $first = true;
800                         foreach ( $this->parts as $part ) {
801                                 if ( $first ) {
802                                         $first = false;
803                                 } elseif ( is_string( $accum[$lastIndex] ) ) {
804                                         $accum[$lastIndex] .= '|';
805                                 } else {
806                                         $accum[++$lastIndex] = '|';
807                                 }
808                                 foreach ( $part->out as $node ) {
809                                         if ( is_string( $node ) && is_string( $accum[$lastIndex] ) ) {
810                                                 $accum[$lastIndex] .= $node;
811                                         } else {
812                                                 $accum[++$lastIndex] = $node;
813                                         }
814                                 }
815                         }
816                 }
817                 return $accum;
818         }
819 }
820
821 /**
822  * @ingroup Parser
823  */
824 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
825 class PPDPart_Hash extends PPDPart {
826         // @codingStandardsIgnoreEnd
827
828         public function __construct( $out = '' ) {
829                 if ( $out !== '' ) {
830                         $accum = [ $out ];
831                 } else {
832                         $accum = [];
833                 }
834                 parent::__construct( $accum );
835         }
836 }
837
838 /**
839  * An expansion frame, used as a context to expand the result of preprocessToObj()
840  * @ingroup Parser
841  */
842 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
843 class PPFrame_Hash implements PPFrame {
844         // @codingStandardsIgnoreEnd
845
846         /**
847          * @var Parser
848          */
849         public $parser;
850
851         /**
852          * @var Preprocessor
853          */
854         public $preprocessor;
855
856         /**
857          * @var Title
858          */
859         public $title;
860         public $titleCache;
861
862         /**
863          * Hashtable listing templates which are disallowed for expansion in this frame,
864          * having been encountered previously in parent frames.
865          */
866         public $loopCheckHash;
867
868         /**
869          * Recursion depth of this frame, top = 0
870          * Note that this is NOT the same as expansion depth in expand()
871          */
872         public $depth;
873
874         private $volatile = false;
875         private $ttl = null;
876
877         /**
878          * @var array
879          */
880         protected $childExpansionCache;
881
882         /**
883          * Construct a new preprocessor frame.
884          * @param Preprocessor $preprocessor The parent preprocessor
885          */
886         public function __construct( $preprocessor ) {
887                 $this->preprocessor = $preprocessor;
888                 $this->parser = $preprocessor->parser;
889                 $this->title = $this->parser->mTitle;
890                 $this->titleCache = [ $this->title ? $this->title->getPrefixedDBkey() : false ];
891                 $this->loopCheckHash = [];
892                 $this->depth = 0;
893                 $this->childExpansionCache = [];
894         }
895
896         /**
897          * Create a new child frame
898          * $args is optionally a multi-root PPNode or array containing the template arguments
899          *
900          * @param array|bool|PPNode_Hash_Array $args
901          * @param Title|bool $title
902          * @param int $indexOffset
903          * @throws MWException
904          * @return PPTemplateFrame_Hash
905          */
906         public function newChild( $args = false, $title = false, $indexOffset = 0 ) {
907                 $namedArgs = [];
908                 $numberedArgs = [];
909                 if ( $title === false ) {
910                         $title = $this->title;
911                 }
912                 if ( $args !== false ) {
913                         if ( $args instanceof PPNode_Hash_Array ) {
914                                 $args = $args->value;
915                         } elseif ( !is_array( $args ) ) {
916                                 throw new MWException( __METHOD__ . ': $args must be array or PPNode_Hash_Array' );
917                         }
918                         foreach ( $args as $arg ) {
919                                 $bits = $arg->splitArg();
920                                 if ( $bits['index'] !== '' ) {
921                                         // Numbered parameter
922                                         $index = $bits['index'] - $indexOffset;
923                                         if ( isset( $namedArgs[$index] ) || isset( $numberedArgs[$index] ) ) {
924                                                 $this->parser->getOutput()->addWarning( wfMessage( 'duplicate-args-warning',
925                                                         wfEscapeWikiText( $this->title ),
926                                                         wfEscapeWikiText( $title ),
927                                                         wfEscapeWikiText( $index ) )->text() );
928                                                 $this->parser->addTrackingCategory( 'duplicate-args-category' );
929                                         }
930                                         $numberedArgs[$index] = $bits['value'];
931                                         unset( $namedArgs[$index] );
932                                 } else {
933                                         // Named parameter
934                                         $name = trim( $this->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
935                                         if ( isset( $namedArgs[$name] ) || isset( $numberedArgs[$name] ) ) {
936                                                 $this->parser->getOutput()->addWarning( wfMessage( 'duplicate-args-warning',
937                                                         wfEscapeWikiText( $this->title ),
938                                                         wfEscapeWikiText( $title ),
939                                                         wfEscapeWikiText( $name ) )->text() );
940                                                 $this->parser->addTrackingCategory( 'duplicate-args-category' );
941                                         }
942                                         $namedArgs[$name] = $bits['value'];
943                                         unset( $numberedArgs[$name] );
944                                 }
945                         }
946                 }
947                 return new PPTemplateFrame_Hash( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title );
948         }
949
950         /**
951          * @throws MWException
952          * @param string|int $key
953          * @param string|PPNode $root
954          * @param int $flags
955          * @return string
956          */
957         public function cachedExpand( $key, $root, $flags = 0 ) {
958                 // we don't have a parent, so we don't have a cache
959                 return $this->expand( $root, $flags );
960         }
961
962         /**
963          * @throws MWException
964          * @param string|PPNode $root
965          * @param int $flags
966          * @return string
967          */
968         public function expand( $root, $flags = 0 ) {
969                 static $expansionDepth = 0;
970                 if ( is_string( $root ) ) {
971                         return $root;
972                 }
973
974                 if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->getMaxPPNodeCount() ) {
975                         $this->parser->limitationWarn( 'node-count-exceeded',
976                                         $this->parser->mPPNodeCount,
977                                         $this->parser->mOptions->getMaxPPNodeCount()
978                         );
979                         return '<span class="error">Node-count limit exceeded</span>';
980                 }
981                 if ( $expansionDepth > $this->parser->mOptions->getMaxPPExpandDepth() ) {
982                         $this->parser->limitationWarn( 'expansion-depth-exceeded',
983                                         $expansionDepth,
984                                         $this->parser->mOptions->getMaxPPExpandDepth()
985                         );
986                         return '<span class="error">Expansion depth limit exceeded</span>';
987                 }
988                 ++$expansionDepth;
989                 if ( $expansionDepth > $this->parser->mHighestExpansionDepth ) {
990                         $this->parser->mHighestExpansionDepth = $expansionDepth;
991                 }
992
993                 $outStack = [ '', '' ];
994                 $iteratorStack = [ false, $root ];
995                 $indexStack = [ 0, 0 ];
996
997                 while ( count( $iteratorStack ) > 1 ) {
998                         $level = count( $outStack ) - 1;
999                         $iteratorNode =& $iteratorStack[$level];
1000                         $out =& $outStack[$level];
1001                         $index =& $indexStack[$level];
1002
1003                         if ( is_array( $iteratorNode ) ) {
1004                                 if ( $index >= count( $iteratorNode ) ) {
1005                                         // All done with this iterator
1006                                         $iteratorStack[$level] = false;
1007                                         $contextNode = false;
1008                                 } else {
1009                                         $contextNode = $iteratorNode[$index];
1010                                         $index++;
1011                                 }
1012                         } elseif ( $iteratorNode instanceof PPNode_Hash_Array ) {
1013                                 if ( $index >= $iteratorNode->getLength() ) {
1014                                         // All done with this iterator
1015                                         $iteratorStack[$level] = false;
1016                                         $contextNode = false;
1017                                 } else {
1018                                         $contextNode = $iteratorNode->item( $index );
1019                                         $index++;
1020                                 }
1021                         } else {
1022                                 // Copy to $contextNode and then delete from iterator stack,
1023                                 // because this is not an iterator but we do have to execute it once
1024                                 $contextNode = $iteratorStack[$level];
1025                                 $iteratorStack[$level] = false;
1026                         }
1027
1028                         $newIterator = false;
1029                         $contextName = false;
1030                         $contextChildren = false;
1031
1032                         if ( $contextNode === false ) {
1033                                 // nothing to do
1034                         } elseif ( is_string( $contextNode ) ) {
1035                                 $out .= $contextNode;
1036                         } elseif ( $contextNode instanceof PPNode_Hash_Array ) {
1037                                 $newIterator = $contextNode;
1038                         } elseif ( $contextNode instanceof PPNode_Hash_Attr ) {
1039                                 // No output
1040                         } elseif ( $contextNode instanceof PPNode_Hash_Text ) {
1041                                 $out .= $contextNode->value;
1042                         } elseif ( $contextNode instanceof PPNode_Hash_Tree ) {
1043                                 $contextName = $contextNode->name;
1044                                 $contextChildren = $contextNode->getRawChildren();
1045                         } elseif ( is_array( $contextNode ) ) {
1046                                 // Node descriptor array
1047                                 if ( count( $contextNode ) !== 2 ) {
1048                                         throw new MWException( __METHOD__.
1049                                                 ': found an array where a node descriptor should be' );
1050                                 }
1051                                 list( $contextName, $contextChildren ) = $contextNode;
1052                         } else {
1053                                 throw new MWException( __METHOD__ . ': Invalid parameter type' );
1054                         }
1055
1056                         // Handle node descriptor array or tree object
1057                         if ( $contextName === false ) {
1058                                 // Not a node, already handled above
1059                         } elseif ( $contextName[0] === '@' ) {
1060                                 // Attribute: no output
1061                         } elseif ( $contextName === 'template' ) {
1062                                 # Double-brace expansion
1063                                 $bits = PPNode_Hash_Tree::splitRawTemplate( $contextChildren );
1064                                 if ( $flags & PPFrame::NO_TEMPLATES ) {
1065                                         $newIterator = $this->virtualBracketedImplode(
1066                                                 '{{', '|', '}}',
1067                                                 $bits['title'],
1068                                                 $bits['parts']
1069                                         );
1070                                 } else {
1071                                         $ret = $this->parser->braceSubstitution( $bits, $this );
1072                                         if ( isset( $ret['object'] ) ) {
1073                                                 $newIterator = $ret['object'];
1074                                         } else {
1075                                                 $out .= $ret['text'];
1076                                         }
1077                                 }
1078                         } elseif ( $contextName === 'tplarg' ) {
1079                                 # Triple-brace expansion
1080                                 $bits = PPNode_Hash_Tree::splitRawTemplate( $contextChildren );
1081                                 if ( $flags & PPFrame::NO_ARGS ) {
1082                                         $newIterator = $this->virtualBracketedImplode(
1083                                                 '{{{', '|', '}}}',
1084                                                 $bits['title'],
1085                                                 $bits['parts']
1086                                         );
1087                                 } else {
1088                                         $ret = $this->parser->argSubstitution( $bits, $this );
1089                                         if ( isset( $ret['object'] ) ) {
1090                                                 $newIterator = $ret['object'];
1091                                         } else {
1092                                                 $out .= $ret['text'];
1093                                         }
1094                                 }
1095                         } elseif ( $contextName === 'comment' ) {
1096                                 # HTML-style comment
1097                                 # Remove it in HTML, pre+remove and STRIP_COMMENTS modes
1098                                 # Not in RECOVER_COMMENTS mode (msgnw) though.
1099                                 if ( ( $this->parser->ot['html']
1100                                         || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() )
1101                                         || ( $flags & PPFrame::STRIP_COMMENTS )
1102                                         ) && !( $flags & PPFrame::RECOVER_COMMENTS )
1103                                 ) {
1104                                         $out .= '';
1105                                 } elseif ( $this->parser->ot['wiki'] && !( $flags & PPFrame::RECOVER_COMMENTS ) ) {
1106                                         # Add a strip marker in PST mode so that pstPass2() can
1107                                         # run some old-fashioned regexes on the result.
1108                                         # Not in RECOVER_COMMENTS mode (extractSections) though.
1109                                         $out .= $this->parser->insertStripItem( $contextChildren[0] );
1110                                 } else {
1111                                         # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
1112                                         $out .= $contextChildren[0];
1113                                 }
1114                         } elseif ( $contextName === 'ignore' ) {
1115                                 # Output suppression used by <includeonly> etc.
1116                                 # OT_WIKI will only respect <ignore> in substed templates.
1117                                 # The other output types respect it unless NO_IGNORE is set.
1118                                 # extractSections() sets NO_IGNORE and so never respects it.
1119                                 if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] )
1120                                         || ( $flags & PPFrame::NO_IGNORE )
1121                                 ) {
1122                                         $out .= $contextChildren[0];
1123                                 } else {
1124                                         // $out .= '';
1125                                 }
1126                         } elseif ( $contextName === 'ext' ) {
1127                                 # Extension tag
1128                                 $bits = PPNode_Hash_Tree::splitRawExt( $contextChildren ) +
1129                                         [ 'attr' => null, 'inner' => null, 'close' => null ];
1130                                 if ( $flags & PPFrame::NO_TAGS ) {
1131                                         $s = '<' . $bits['name']->getFirstChild()->value;
1132                                         if ( $bits['attr'] ) {
1133                                                 $s .= $bits['attr']->getFirstChild()->value;
1134                                         }
1135                                         if ( $bits['inner'] ) {
1136                                                 $s .= '>' . $bits['inner']->getFirstChild()->value;
1137                                                 if ( $bits['close'] ) {
1138                                                         $s .= $bits['close']->getFirstChild()->value;
1139                                                 }
1140                                         } else {
1141                                                 $s .= '/>';
1142                                         }
1143                                         $out .= $s;
1144                                 } else {
1145                                         $out .= $this->parser->extensionSubstitution( $bits, $this );
1146                                 }
1147                         } elseif ( $contextName === 'h' ) {
1148                                 # Heading
1149                                 if ( $this->parser->ot['html'] ) {
1150                                         # Expand immediately and insert heading index marker
1151                                         $s = $this->expand( $contextChildren, $flags );
1152                                         $bits = PPNode_Hash_Tree::splitRawHeading( $contextChildren );
1153                                         $titleText = $this->title->getPrefixedDBkey();
1154                                         $this->parser->mHeadings[] = [ $titleText, $bits['i'] ];
1155                                         $serial = count( $this->parser->mHeadings ) - 1;
1156                                         $marker = Parser::MARKER_PREFIX . "-h-$serial-" . Parser::MARKER_SUFFIX;
1157                                         $s = substr( $s, 0, $bits['level'] ) . $marker . substr( $s, $bits['level'] );
1158                                         $this->parser->mStripState->addGeneral( $marker, '' );
1159                                         $out .= $s;
1160                                 } else {
1161                                         # Expand in virtual stack
1162                                         $newIterator = $contextChildren;
1163                                 }
1164                         } else {
1165                                 # Generic recursive expansion
1166                                 $newIterator = $contextChildren;
1167                         }
1168
1169                         if ( $newIterator !== false ) {
1170                                 $outStack[] = '';
1171                                 $iteratorStack[] = $newIterator;
1172                                 $indexStack[] = 0;
1173                         } elseif ( $iteratorStack[$level] === false ) {
1174                                 // Return accumulated value to parent
1175                                 // With tail recursion
1176                                 while ( $iteratorStack[$level] === false && $level > 0 ) {
1177                                         $outStack[$level - 1] .= $out;
1178                                         array_pop( $outStack );
1179                                         array_pop( $iteratorStack );
1180                                         array_pop( $indexStack );
1181                                         $level--;
1182                                 }
1183                         }
1184                 }
1185                 --$expansionDepth;
1186                 return $outStack[0];
1187         }
1188
1189         /**
1190          * @param string $sep
1191          * @param int $flags
1192          * @param string|PPNode $args,...
1193          * @return string
1194          */
1195         public function implodeWithFlags( $sep, $flags /*, ... */ ) {
1196                 $args = array_slice( func_get_args(), 2 );
1197
1198                 $first = true;
1199                 $s = '';
1200                 foreach ( $args as $root ) {
1201                         if ( $root instanceof PPNode_Hash_Array ) {
1202                                 $root = $root->value;
1203                         }
1204                         if ( !is_array( $root ) ) {
1205                                 $root = [ $root ];
1206                         }
1207                         foreach ( $root as $node ) {
1208                                 if ( $first ) {
1209                                         $first = false;
1210                                 } else {
1211                                         $s .= $sep;
1212                                 }
1213                                 $s .= $this->expand( $node, $flags );
1214                         }
1215                 }
1216                 return $s;
1217         }
1218
1219         /**
1220          * Implode with no flags specified
1221          * This previously called implodeWithFlags but has now been inlined to reduce stack depth
1222          * @param string $sep
1223          * @param string|PPNode $args,...
1224          * @return string
1225          */
1226         public function implode( $sep /*, ... */ ) {
1227                 $args = array_slice( func_get_args(), 1 );
1228
1229                 $first = true;
1230                 $s = '';
1231                 foreach ( $args as $root ) {
1232                         if ( $root instanceof PPNode_Hash_Array ) {
1233                                 $root = $root->value;
1234                         }
1235                         if ( !is_array( $root ) ) {
1236                                 $root = [ $root ];
1237                         }
1238                         foreach ( $root as $node ) {
1239                                 if ( $first ) {
1240                                         $first = false;
1241                                 } else {
1242                                         $s .= $sep;
1243                                 }
1244                                 $s .= $this->expand( $node );
1245                         }
1246                 }
1247                 return $s;
1248         }
1249
1250         /**
1251          * Makes an object that, when expand()ed, will be the same as one obtained
1252          * with implode()
1253          *
1254          * @param string $sep
1255          * @param string|PPNode $args,...
1256          * @return PPNode_Hash_Array
1257          */
1258         public function virtualImplode( $sep /*, ... */ ) {
1259                 $args = array_slice( func_get_args(), 1 );
1260                 $out = [];
1261                 $first = true;
1262
1263                 foreach ( $args as $root ) {
1264                         if ( $root instanceof PPNode_Hash_Array ) {
1265                                 $root = $root->value;
1266                         }
1267                         if ( !is_array( $root ) ) {
1268                                 $root = [ $root ];
1269                         }
1270                         foreach ( $root as $node ) {
1271                                 if ( $first ) {
1272                                         $first = false;
1273                                 } else {
1274                                         $out[] = $sep;
1275                                 }
1276                                 $out[] = $node;
1277                         }
1278                 }
1279                 return new PPNode_Hash_Array( $out );
1280         }
1281
1282         /**
1283          * Virtual implode with brackets
1284          *
1285          * @param string $start
1286          * @param string $sep
1287          * @param string $end
1288          * @param string|PPNode $args,...
1289          * @return PPNode_Hash_Array
1290          */
1291         public function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) {
1292                 $args = array_slice( func_get_args(), 3 );
1293                 $out = [ $start ];
1294                 $first = true;
1295
1296                 foreach ( $args as $root ) {
1297                         if ( $root instanceof PPNode_Hash_Array ) {
1298                                 $root = $root->value;
1299                         }
1300                         if ( !is_array( $root ) ) {
1301                                 $root = [ $root ];
1302                         }
1303                         foreach ( $root as $node ) {
1304                                 if ( $first ) {
1305                                         $first = false;
1306                                 } else {
1307                                         $out[] = $sep;
1308                                 }
1309                                 $out[] = $node;
1310                         }
1311                 }
1312                 $out[] = $end;
1313                 return new PPNode_Hash_Array( $out );
1314         }
1315
1316         public function __toString() {
1317                 return 'frame{}';
1318         }
1319
1320         /**
1321          * @param bool $level
1322          * @return array|bool|string
1323          */
1324         public function getPDBK( $level = false ) {
1325                 if ( $level === false ) {
1326                         return $this->title->getPrefixedDBkey();
1327                 } else {
1328                         return isset( $this->titleCache[$level] ) ? $this->titleCache[$level] : false;
1329                 }
1330         }
1331
1332         /**
1333          * @return array
1334          */
1335         public function getArguments() {
1336                 return [];
1337         }
1338
1339         /**
1340          * @return array
1341          */
1342         public function getNumberedArguments() {
1343                 return [];
1344         }
1345
1346         /**
1347          * @return array
1348          */
1349         public function getNamedArguments() {
1350                 return [];
1351         }
1352
1353         /**
1354          * Returns true if there are no arguments in this frame
1355          *
1356          * @return bool
1357          */
1358         public function isEmpty() {
1359                 return true;
1360         }
1361
1362         /**
1363          * @param int|string $name
1364          * @return bool Always false in this implementation.
1365          */
1366         public function getArgument( $name ) {
1367                 return false;
1368         }
1369
1370         /**
1371          * Returns true if the infinite loop check is OK, false if a loop is detected
1372          *
1373          * @param Title $title
1374          *
1375          * @return bool
1376          */
1377         public function loopCheck( $title ) {
1378                 return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] );
1379         }
1380
1381         /**
1382          * Return true if the frame is a template frame
1383          *
1384          * @return bool
1385          */
1386         public function isTemplate() {
1387                 return false;
1388         }
1389
1390         /**
1391          * Get a title of frame
1392          *
1393          * @return Title
1394          */
1395         public function getTitle() {
1396                 return $this->title;
1397         }
1398
1399         /**
1400          * Set the volatile flag
1401          *
1402          * @param bool $flag
1403          */
1404         public function setVolatile( $flag = true ) {
1405                 $this->volatile = $flag;
1406         }
1407
1408         /**
1409          * Get the volatile flag
1410          *
1411          * @return bool
1412          */
1413         public function isVolatile() {
1414                 return $this->volatile;
1415         }
1416
1417         /**
1418          * Set the TTL
1419          *
1420          * @param int $ttl
1421          */
1422         public function setTTL( $ttl ) {
1423                 if ( $ttl !== null && ( $this->ttl === null || $ttl < $this->ttl ) ) {
1424                         $this->ttl = $ttl;
1425                 }
1426         }
1427
1428         /**
1429          * Get the TTL
1430          *
1431          * @return int|null
1432          */
1433         public function getTTL() {
1434                 return $this->ttl;
1435         }
1436 }
1437
1438 /**
1439  * Expansion frame with template arguments
1440  * @ingroup Parser
1441  */
1442 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
1443 class PPTemplateFrame_Hash extends PPFrame_Hash {
1444         // @codingStandardsIgnoreEnd
1445
1446         public $numberedArgs, $namedArgs, $parent;
1447         public $numberedExpansionCache, $namedExpansionCache;
1448
1449         /**
1450          * @param Preprocessor $preprocessor
1451          * @param bool|PPFrame $parent
1452          * @param array $numberedArgs
1453          * @param array $namedArgs
1454          * @param bool|Title $title
1455          */
1456         public function __construct( $preprocessor, $parent = false, $numberedArgs = [],
1457                 $namedArgs = [], $title = false
1458         ) {
1459                 parent::__construct( $preprocessor );
1460
1461                 $this->parent = $parent;
1462                 $this->numberedArgs = $numberedArgs;
1463                 $this->namedArgs = $namedArgs;
1464                 $this->title = $title;
1465                 $pdbk = $title ? $title->getPrefixedDBkey() : false;
1466                 $this->titleCache = $parent->titleCache;
1467                 $this->titleCache[] = $pdbk;
1468                 $this->loopCheckHash = /*clone*/ $parent->loopCheckHash;
1469                 if ( $pdbk !== false ) {
1470                         $this->loopCheckHash[$pdbk] = true;
1471                 }
1472                 $this->depth = $parent->depth + 1;
1473                 $this->numberedExpansionCache = $this->namedExpansionCache = [];
1474         }
1475
1476         public function __toString() {
1477                 $s = 'tplframe{';
1478                 $first = true;
1479                 $args = $this->numberedArgs + $this->namedArgs;
1480                 foreach ( $args as $name => $value ) {
1481                         if ( $first ) {
1482                                 $first = false;
1483                         } else {
1484                                 $s .= ', ';
1485                         }
1486                         $s .= "\"$name\":\"" .
1487                                 str_replace( '"', '\\"', $value->__toString() ) . '"';
1488                 }
1489                 $s .= '}';
1490                 return $s;
1491         }
1492
1493         /**
1494          * @throws MWException
1495          * @param string|int $key
1496          * @param string|PPNode $root
1497          * @param int $flags
1498          * @return string
1499          */
1500         public function cachedExpand( $key, $root, $flags = 0 ) {
1501                 if ( isset( $this->parent->childExpansionCache[$key] ) ) {
1502                         return $this->parent->childExpansionCache[$key];
1503                 }
1504                 $retval = $this->expand( $root, $flags );
1505                 if ( !$this->isVolatile() ) {
1506                         $this->parent->childExpansionCache[$key] = $retval;
1507                 }
1508                 return $retval;
1509         }
1510
1511         /**
1512          * Returns true if there are no arguments in this frame
1513          *
1514          * @return bool
1515          */
1516         public function isEmpty() {
1517                 return !count( $this->numberedArgs ) && !count( $this->namedArgs );
1518         }
1519
1520         /**
1521          * @return array
1522          */
1523         public function getArguments() {
1524                 $arguments = [];
1525                 foreach ( array_merge(
1526                                 array_keys( $this->numberedArgs ),
1527                                 array_keys( $this->namedArgs ) ) as $key ) {
1528                         $arguments[$key] = $this->getArgument( $key );
1529                 }
1530                 return $arguments;
1531         }
1532
1533         /**
1534          * @return array
1535          */
1536         public function getNumberedArguments() {
1537                 $arguments = [];
1538                 foreach ( array_keys( $this->numberedArgs ) as $key ) {
1539                         $arguments[$key] = $this->getArgument( $key );
1540                 }
1541                 return $arguments;
1542         }
1543
1544         /**
1545          * @return array
1546          */
1547         public function getNamedArguments() {
1548                 $arguments = [];
1549                 foreach ( array_keys( $this->namedArgs ) as $key ) {
1550                         $arguments[$key] = $this->getArgument( $key );
1551                 }
1552                 return $arguments;
1553         }
1554
1555         /**
1556          * @param int $index
1557          * @return string|bool
1558          */
1559         public function getNumberedArgument( $index ) {
1560                 if ( !isset( $this->numberedArgs[$index] ) ) {
1561                         return false;
1562                 }
1563                 if ( !isset( $this->numberedExpansionCache[$index] ) ) {
1564                         # No trimming for unnamed arguments
1565                         $this->numberedExpansionCache[$index] = $this->parent->expand(
1566                                 $this->numberedArgs[$index],
1567                                 PPFrame::STRIP_COMMENTS
1568                         );
1569                 }
1570                 return $this->numberedExpansionCache[$index];
1571         }
1572
1573         /**
1574          * @param string $name
1575          * @return string|bool
1576          */
1577         public function getNamedArgument( $name ) {
1578                 if ( !isset( $this->namedArgs[$name] ) ) {
1579                         return false;
1580                 }
1581                 if ( !isset( $this->namedExpansionCache[$name] ) ) {
1582                         # Trim named arguments post-expand, for backwards compatibility
1583                         $this->namedExpansionCache[$name] = trim(
1584                                 $this->parent->expand( $this->namedArgs[$name], PPFrame::STRIP_COMMENTS ) );
1585                 }
1586                 return $this->namedExpansionCache[$name];
1587         }
1588
1589         /**
1590          * @param int|string $name
1591          * @return string|bool
1592          */
1593         public function getArgument( $name ) {
1594                 $text = $this->getNumberedArgument( $name );
1595                 if ( $text === false ) {
1596                         $text = $this->getNamedArgument( $name );
1597                 }
1598                 return $text;
1599         }
1600
1601         /**
1602          * Return true if the frame is a template frame
1603          *
1604          * @return bool
1605          */
1606         public function isTemplate() {
1607                 return true;
1608         }
1609
1610         public function setVolatile( $flag = true ) {
1611                 parent::setVolatile( $flag );
1612                 $this->parent->setVolatile( $flag );
1613         }
1614
1615         public function setTTL( $ttl ) {
1616                 parent::setTTL( $ttl );
1617                 $this->parent->setTTL( $ttl );
1618         }
1619 }
1620
1621 /**
1622  * Expansion frame with custom arguments
1623  * @ingroup Parser
1624  */
1625 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
1626 class PPCustomFrame_Hash extends PPFrame_Hash {
1627         // @codingStandardsIgnoreEnd
1628
1629         public $args;
1630
1631         public function __construct( $preprocessor, $args ) {
1632                 parent::__construct( $preprocessor );
1633                 $this->args = $args;
1634         }
1635
1636         public function __toString() {
1637                 $s = 'cstmframe{';
1638                 $first = true;
1639                 foreach ( $this->args as $name => $value ) {
1640                         if ( $first ) {
1641                                 $first = false;
1642                         } else {
1643                                 $s .= ', ';
1644                         }
1645                         $s .= "\"$name\":\"" .
1646                                 str_replace( '"', '\\"', $value->__toString() ) . '"';
1647                 }
1648                 $s .= '}';
1649                 return $s;
1650         }
1651
1652         /**
1653          * @return bool
1654          */
1655         public function isEmpty() {
1656                 return !count( $this->args );
1657         }
1658
1659         /**
1660          * @param int|string $index
1661          * @return string|bool
1662          */
1663         public function getArgument( $index ) {
1664                 if ( !isset( $this->args[$index] ) ) {
1665                         return false;
1666                 }
1667                 return $this->args[$index];
1668         }
1669
1670         public function getArguments() {
1671                 return $this->args;
1672         }
1673 }
1674
1675 /**
1676  * @ingroup Parser
1677  */
1678 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
1679 class PPNode_Hash_Tree implements PPNode {
1680         // @codingStandardsIgnoreEnd
1681
1682         public $name;
1683
1684         /**
1685          * The store array for children of this node. It is "raw" in the sense that
1686          * nodes are two-element arrays ("descriptors") rather than PPNode_Hash_*
1687          * objects.
1688          */
1689         private $rawChildren;
1690
1691         /**
1692          * The store array for the siblings of this node, including this node itself.
1693          */
1694         private $store;
1695
1696         /**
1697          * The index into $this->store which contains the descriptor of this node.
1698          */
1699         private $index;
1700
1701         /**
1702          * The offset of the name within descriptors, used in some places for
1703          * readability.
1704          */
1705         const NAME = 0;
1706
1707         /**
1708          * The offset of the child list within descriptors, used in some places for
1709          * readability.
1710          */
1711         const CHILDREN = 1;
1712
1713         /**
1714          * Construct an object using the data from $store[$index]. The rest of the
1715          * store array can be accessed via getNextSibling().
1716          *
1717          * @param array $store
1718          * @param int $index
1719          */
1720         public function __construct( array $store, $index ) {
1721                 $this->store = $store;
1722                 $this->index = $index;
1723                 list( $this->name, $this->rawChildren ) = $this->store[$index];
1724         }
1725
1726         /**
1727          * Construct an appropriate PPNode_Hash_* object with a class that depends
1728          * on what is at the relevant store index.
1729          *
1730          * @param array $store
1731          * @param int $index
1732          * @return PPNode_Hash_Tree|PPNode_Hash_Attr|PPNode_Hash_Text
1733          */
1734         public static function factory( array $store, $index ) {
1735                 if ( !isset( $store[$index] ) ) {
1736                         return false;
1737                 }
1738
1739                 $descriptor = $store[$index];
1740                 if ( is_string( $descriptor ) ) {
1741                         $class = 'PPNode_Hash_Text';
1742                 } elseif ( is_array( $descriptor ) ) {
1743                         if ( $descriptor[self::NAME][0] === '@' ) {
1744                                 $class = 'PPNode_Hash_Attr';
1745                         } else {
1746                                 $class = 'PPNode_Hash_Tree';
1747                         }
1748                 } else {
1749                         throw new MWException( __METHOD__.': invalid node descriptor' );
1750                 }
1751                 return new $class( $store, $index );
1752         }
1753
1754         /**
1755          * Convert a node to XML, for debugging
1756          */
1757         public function __toString() {
1758                 $inner = '';
1759                 $attribs = '';
1760                 for ( $node = $this->getFirstChild(); $node; $node = $node->getNextSibling() ) {
1761                         if ( $node instanceof PPNode_Hash_Attr ) {
1762                                 $attribs .= ' ' . $node->name . '="' . htmlspecialchars( $node->value ) . '"';
1763                         } else {
1764                                 $inner .= $node->__toString();
1765                         }
1766                 }
1767                 if ( $inner === '' ) {
1768                         return "<{$this->name}$attribs/>";
1769                 } else {
1770                         return "<{$this->name}$attribs>$inner</{$this->name}>";
1771                 }
1772         }
1773
1774         /**
1775          * @return PPNode_Hash_Array
1776          */
1777         public function getChildren() {
1778                 $children = [];
1779                 foreach ( $this->rawChildren as $i => $child ) {
1780                         $children[] = self::factory( $this->rawChildren, $i );
1781                 }
1782                 return new PPNode_Hash_Array( $children );
1783         }
1784
1785         /**
1786          * Get the first child, or false if there is none. Note that this will
1787          * return a temporary proxy object: different instances will be returned
1788          * if this is called more than once on the same node.
1789          *
1790          * @return PPNode_Hash_Tree|PPNode_Hash_Attr|PPNode_Hash_Text|bool
1791          */
1792         public function getFirstChild() {
1793                 if ( !isset( $this->rawChildren[0] ) ) {
1794                         return false;
1795                 } else {
1796                         return self::factory( $this->rawChildren, 0 );
1797                 }
1798         }
1799
1800         /**
1801          * Get the next sibling, or false if there is none. Note that this will
1802          * return a temporary proxy object: different instances will be returned
1803          * if this is called more than once on the same node.
1804          *
1805          * @return PPNode_Hash_Tree|PPNode_Hash_Attr|PPNode_Hash_Text|bool
1806          */
1807         public function getNextSibling() {
1808                 return self::factory( $this->store, $this->index + 1 );
1809         }
1810
1811         /**
1812          * Get an array of the children with a given node name
1813          *
1814          * @param string $name
1815          * @return PPNode_Hash_Array
1816          */
1817         public function getChildrenOfType( $name ) {
1818                 $children = [];
1819                 foreach ( $this->rawChildren as $i => $child ) {
1820                         if ( is_array( $child ) && $child[self::NAME] === $name ) {
1821                                 $children[] = self::factory( $this->rawChildren, $i );
1822                         }
1823                 }
1824                 return new PPNode_Hash_Array( $children );
1825         }
1826
1827         /**
1828          * Get the raw child array. For internal use.
1829          * @return array
1830          */
1831         public function getRawChildren() {
1832                 return $this->rawChildren;
1833         }
1834
1835         /**
1836          * @return bool
1837          */
1838         public function getLength() {
1839                 return false;
1840         }
1841
1842         /**
1843          * @param int $i
1844          * @return bool
1845          */
1846         public function item( $i ) {
1847                 return false;
1848         }
1849
1850         /**
1851          * @return string
1852          */
1853         public function getName() {
1854                 return $this->name;
1855         }
1856
1857         /**
1858          * Split a "<part>" node into an associative array containing:
1859          *  - name          PPNode name
1860          *  - index         String index
1861          *  - value         PPNode value
1862          *
1863          * @throws MWException
1864          * @return array
1865          */
1866         public function splitArg() {
1867                 return self::splitRawArg( $this->rawChildren );
1868         }
1869
1870         /**
1871          * Like splitArg() but for a raw child array. For internal use only.
1872          * @param array $children
1873          * @return array
1874          */
1875         public static function splitRawArg( array $children ) {
1876                 $bits = [];
1877                 foreach ( $children as $i => $child ) {
1878                         if ( !is_array( $child ) ) {
1879                                 continue;
1880                         }
1881                         if ( $child[self::NAME] === 'name' ) {
1882                                 $bits['name'] = new self( $children, $i );
1883                                 if ( isset( $child[self::CHILDREN][0][self::NAME] )
1884                                         && $child[self::CHILDREN][0][self::NAME] === '@index'
1885                                 ) {
1886                                         $bits['index'] = $child[self::CHILDREN][0][self::CHILDREN][0];
1887                                 }
1888                         } elseif ( $child[self::NAME] === 'value' ) {
1889                                 $bits['value'] = new self( $children, $i );
1890                         }
1891                 }
1892
1893                 if ( !isset( $bits['name'] ) ) {
1894                         throw new MWException( 'Invalid brace node passed to ' . __METHOD__ );
1895                 }
1896                 if ( !isset( $bits['index'] ) ) {
1897                         $bits['index'] = '';
1898                 }
1899                 return $bits;
1900         }
1901
1902         /**
1903          * Split an "<ext>" node into an associative array containing name, attr, inner and close
1904          * All values in the resulting array are PPNodes. Inner and close are optional.
1905          *
1906          * @throws MWException
1907          * @return array
1908          */
1909         public function splitExt() {
1910                 return self::splitRawExt( $this->rawChildren );
1911         }
1912
1913         /**
1914          * Like splitExt() but for a raw child array. For internal use only.
1915          * @param array $children
1916          * @return array
1917          */
1918         public static function splitRawExt( array $children ) {
1919                 $bits = [];
1920                 foreach ( $children as $i => $child ) {
1921                         if ( !is_array( $child ) ) {
1922                                 continue;
1923                         }
1924                         switch ( $child[self::NAME] ) {
1925                         case 'name':
1926                                 $bits['name'] = new self( $children, $i );
1927                                 break;
1928                         case 'attr':
1929                                 $bits['attr'] = new self( $children, $i );
1930                                 break;
1931                         case 'inner':
1932                                 $bits['inner'] = new self( $children, $i );
1933                                 break;
1934                         case 'close':
1935                                 $bits['close'] = new self( $children, $i );
1936                                 break;
1937                         }
1938                 }
1939                 if ( !isset( $bits['name'] ) ) {
1940                         throw new MWException( 'Invalid ext node passed to ' . __METHOD__ );
1941                 }
1942                 return $bits;
1943         }
1944
1945         /**
1946          * Split an "<h>" node
1947          *
1948          * @throws MWException
1949          * @return array
1950          */
1951         public function splitHeading() {
1952                 if ( $this->name !== 'h' ) {
1953                         throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
1954                 }
1955                 return self::splitRawHeading( $this->rawChildren );
1956         }
1957
1958         /**
1959          * Like splitHeading() but for a raw child array. For internal use only.
1960          * @param array $children
1961          * @return array
1962          */
1963         public static function splitRawHeading( array $children ) {
1964                 $bits = [];
1965                 foreach ( $children as $i => $child ) {
1966                         if ( !is_array( $child ) ) {
1967                                 continue;
1968                         }
1969                         if ( $child[self::NAME] === '@i' ) {
1970                                 $bits['i'] = $child[self::CHILDREN][0];
1971                         } elseif ( $child[self::NAME] === '@level' ) {
1972                                 $bits['level'] = $child[self::CHILDREN][0];
1973                         }
1974                 }
1975                 if ( !isset( $bits['i'] ) ) {
1976                         throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
1977                 }
1978                 return $bits;
1979         }
1980
1981         /**
1982          * Split a "<template>" or "<tplarg>" node
1983          *
1984          * @throws MWException
1985          * @return array
1986          */
1987         public function splitTemplate() {
1988                 return self::splitRawTemplate( $this->rawChildren );
1989         }
1990
1991         /**
1992          * Like splitTemplate() but for a raw child array. For internal use only.
1993          * @param array $children
1994          * @return array
1995          */
1996         public static function splitRawTemplate( array $children ) {
1997                 $parts = [];
1998                 $bits = [ 'lineStart' => '' ];
1999                 foreach ( $children as $i => $child ) {
2000                         if ( !is_array( $child ) ) {
2001                                 continue;
2002                         }
2003                         switch ( $child[self::NAME] ) {
2004                         case 'title':
2005                                 $bits['title'] = new self( $children, $i );
2006                                 break;
2007                         case 'part':
2008                                 $parts[] = new self( $children, $i );
2009                                 break;
2010                         case '@lineStart':
2011                                 $bits['lineStart'] = '1';
2012                                 break;
2013                         }
2014                 }
2015                 if ( !isset( $bits['title'] ) ) {
2016                         throw new MWException( 'Invalid node passed to ' . __METHOD__ );
2017                 }
2018                 $bits['parts'] = new PPNode_Hash_Array( $parts );
2019                 return $bits;
2020         }
2021 }
2022
2023 /**
2024  * @ingroup Parser
2025  */
2026 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
2027 class PPNode_Hash_Text implements PPNode {
2028         // @codingStandardsIgnoreEnd
2029
2030         public $value;
2031         private $store, $index;
2032
2033         /**
2034          * Construct an object using the data from $store[$index]. The rest of the
2035          * store array can be accessed via getNextSibling().
2036          *
2037          * @param array $store
2038          * @param int $index
2039          */
2040         public function __construct( array $store, $index ) {
2041                 $this->value = $store[$index];
2042                 if ( !is_scalar( $this->value ) ) {
2043                         throw new MWException( __CLASS__ . ' given object instead of string' );
2044                 }
2045                 $this->store = $store;
2046                 $this->index = $index;
2047         }
2048
2049         public function __toString() {
2050                 return htmlspecialchars( $this->value );
2051         }
2052
2053         public function getNextSibling() {
2054                 return PPNode_Hash_Tree::factory( $this->store, $this->index + 1 );
2055         }
2056
2057         public function getChildren() {
2058                 return false;
2059         }
2060
2061         public function getFirstChild() {
2062                 return false;
2063         }
2064
2065         public function getChildrenOfType( $name ) {
2066                 return false;
2067         }
2068
2069         public function getLength() {
2070                 return false;
2071         }
2072
2073         public function item( $i ) {
2074                 return false;
2075         }
2076
2077         public function getName() {
2078                 return '#text';
2079         }
2080
2081         public function splitArg() {
2082                 throw new MWException( __METHOD__ . ': not supported' );
2083         }
2084
2085         public function splitExt() {
2086                 throw new MWException( __METHOD__ . ': not supported' );
2087         }
2088
2089         public function splitHeading() {
2090                 throw new MWException( __METHOD__ . ': not supported' );
2091         }
2092 }
2093
2094 /**
2095  * @ingroup Parser
2096  */
2097 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
2098 class PPNode_Hash_Array implements PPNode {
2099         // @codingStandardsIgnoreEnd
2100
2101         public $value;
2102
2103         public function __construct( $value ) {
2104                 $this->value = $value;
2105         }
2106
2107         public function __toString() {
2108                 return var_export( $this, true );
2109         }
2110
2111         public function getLength() {
2112                 return count( $this->value );
2113         }
2114
2115         public function item( $i ) {
2116                 return $this->value[$i];
2117         }
2118
2119         public function getName() {
2120                 return '#nodelist';
2121         }
2122
2123         public function getNextSibling() {
2124                 return false;
2125         }
2126
2127         public function getChildren() {
2128                 return false;
2129         }
2130
2131         public function getFirstChild() {
2132                 return false;
2133         }
2134
2135         public function getChildrenOfType( $name ) {
2136                 return false;
2137         }
2138
2139         public function splitArg() {
2140                 throw new MWException( __METHOD__ . ': not supported' );
2141         }
2142
2143         public function splitExt() {
2144                 throw new MWException( __METHOD__ . ': not supported' );
2145         }
2146
2147         public function splitHeading() {
2148                 throw new MWException( __METHOD__ . ': not supported' );
2149         }
2150 }
2151
2152 /**
2153  * @ingroup Parser
2154  */
2155 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
2156 class PPNode_Hash_Attr implements PPNode {
2157         // @codingStandardsIgnoreEnd
2158
2159         public $name, $value;
2160         private $store, $index;
2161
2162         /**
2163          * Construct an object using the data from $store[$index]. The rest of the
2164          * store array can be accessed via getNextSibling().
2165          *
2166          * @param array $store
2167          * @param int $index
2168          */
2169         public function __construct( array $store, $index ) {
2170                 $descriptor = $store[$index];
2171                 if ( $descriptor[PPNode_Hash_Tree::NAME][0] !== '@' ) {
2172                         throw new MWException( __METHOD__.': invalid name in attribute descriptor' );
2173                 }
2174                 $this->name = substr( $descriptor[PPNode_Hash_Tree::NAME], 1 );
2175                 $this->value = $descriptor[PPNode_Hash_Tree::CHILDREN][0];
2176                 $this->store = $store;
2177                 $this->index = $index;
2178         }
2179
2180         public function __toString() {
2181                 return "<@{$this->name}>" . htmlspecialchars( $this->value ) . "</@{$this->name}>";
2182         }
2183
2184         public function getName() {
2185                 return $this->name;
2186         }
2187
2188         public function getNextSibling() {
2189                 return PPNode_Hash_Tree::factory( $this->store, $this->index + 1 );
2190         }
2191
2192         public function getChildren() {
2193                 return false;
2194         }
2195
2196         public function getFirstChild() {
2197                 return false;
2198         }
2199
2200         public function getChildrenOfType( $name ) {
2201                 return false;
2202         }
2203
2204         public function getLength() {
2205                 return false;
2206         }
2207
2208         public function item( $i ) {
2209                 return false;
2210         }
2211
2212         public function splitArg() {
2213                 throw new MWException( __METHOD__ . ': not supported' );
2214         }
2215
2216         public function splitExt() {
2217                 throw new MWException( __METHOD__ . ': not supported' );
2218         }
2219
2220         public function splitHeading() {
2221                 throw new MWException( __METHOD__ . ': not supported' );
2222         }
2223 }