MediaWiki  1.28.0
Preprocessor_Hash.php
Go to the documentation of this file.
1 <?php
42 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
44  // @codingStandardsIgnoreEnd
45 
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 
61  public function newFrame() {
62  return new PPFrame_Hash( $this );
63  }
64 
69  public function newCustomFrame( $args ) {
70  return new PPCustomFrame_Hash( $this, $args );
71  }
72 
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 
119  public function preprocessToObj( $text, $flags = 0 ) {
120  $tree = $this->cacheGetTree( $text, $flags );
121  if ( $tree !== false ) {
122  $store = json_decode( $tree );
123  if ( is_array( $store ) ) {
124  return new PPNode_Hash_Tree( $store, 0 );
125  }
126  }
127 
128  $forInclusion = $flags & Parser::PTD_FOR_INCLUSION;
129 
130  $xmlishElements = $this->parser->getStripList();
131  $xmlishAllowMissingEndTag = [ 'includeonly', 'noinclude', 'onlyinclude' ];
132  $enableOnlyinclude = false;
133  if ( $forInclusion ) {
134  $ignoredTags = [ 'includeonly', '/includeonly' ];
135  $ignoredElements = [ 'noinclude' ];
136  $xmlishElements[] = 'noinclude';
137  if ( strpos( $text, '<onlyinclude>' ) !== false
138  && strpos( $text, '</onlyinclude>' ) !== false
139  ) {
140  $enableOnlyinclude = true;
141  }
142  } else {
143  $ignoredTags = [ 'noinclude', '/noinclude', 'onlyinclude', '/onlyinclude' ];
144  $ignoredElements = [ 'includeonly' ];
145  $xmlishElements[] = 'includeonly';
146  }
147  $xmlishRegex = implode( '|', array_merge( $xmlishElements, $ignoredTags ) );
148 
149  // Use "A" modifier (anchored) instead of "^", because ^ doesn't work with an offset
150  $elementsRegex = "~($xmlishRegex)(?:\s|\/>|>)|(!--)~iA";
151 
152  $stack = new PPDStack_Hash;
153 
154  $searchBase = "[{<\n";
155  // For fast reverse searches
156  $revText = strrev( $text );
157  $lengthText = strlen( $text );
158 
159  // Input pointer, starts out pointing to a pseudo-newline before the start
160  $i = 0;
161  // Current accumulator. See the doc comment for Preprocessor_Hash for the format.
162  $accum =& $stack->getAccum();
163  // True to find equals signs in arguments
164  $findEquals = false;
165  // True to take notice of pipe characters
166  $findPipe = false;
167  $headingIndex = 1;
168  // True if $i is inside a possible heading
169  $inHeading = false;
170  // True if there are no more greater-than (>) signs right of $i
171  $noMoreGT = false;
172  // Map of tag name => true if there are no more closing tags of given type right of $i
173  $noMoreClosingTag = [];
174  // True to ignore all input up to the next <onlyinclude>
175  $findOnlyinclude = $enableOnlyinclude;
176  // Do a line-start run without outputting an LF character
177  $fakeLineStart = true;
178 
179  while ( true ) {
180  // $this->memCheck();
181 
182  if ( $findOnlyinclude ) {
183  // Ignore all input up to the next <onlyinclude>
184  $startPos = strpos( $text, '<onlyinclude>', $i );
185  if ( $startPos === false ) {
186  // Ignored section runs to the end
187  $accum[] = [ 'ignore', [ substr( $text, $i ) ] ];
188  break;
189  }
190  $tagEndPos = $startPos + strlen( '<onlyinclude>' ); // past-the-end
191  $accum[] = [ 'ignore', [ substr( $text, $i, $tagEndPos - $i ) ] ];
192  $i = $tagEndPos;
193  $findOnlyinclude = false;
194  }
195 
196  if ( $fakeLineStart ) {
197  $found = 'line-start';
198  $curChar = '';
199  } else {
200  # Find next opening brace, closing brace or pipe
201  $search = $searchBase;
202  if ( $stack->top === false ) {
203  $currentClosing = '';
204  } else {
205  $currentClosing = $stack->top->close;
206  $search .= $currentClosing;
207  }
208  if ( $findPipe ) {
209  $search .= '|';
210  }
211  if ( $findEquals ) {
212  // First equals will be for the template
213  $search .= '=';
214  }
215  $rule = null;
216  # Output literal section, advance input counter
217  $literalLength = strcspn( $text, $search, $i );
218  if ( $literalLength > 0 ) {
219  self::addLiteral( $accum, substr( $text, $i, $literalLength ) );
220  $i += $literalLength;
221  }
222  if ( $i >= $lengthText ) {
223  if ( $currentClosing == "\n" ) {
224  // Do a past-the-end run to finish off the heading
225  $curChar = '';
226  $found = 'line-end';
227  } else {
228  # All done
229  break;
230  }
231  } else {
232  $curChar = $text[$i];
233  if ( $curChar == '|' ) {
234  $found = 'pipe';
235  } elseif ( $curChar == '=' ) {
236  $found = 'equals';
237  } elseif ( $curChar == '<' ) {
238  $found = 'angle';
239  } elseif ( $curChar == "\n" ) {
240  if ( $inHeading ) {
241  $found = 'line-end';
242  } else {
243  $found = 'line-start';
244  }
245  } elseif ( $curChar == $currentClosing ) {
246  $found = 'close';
247  } elseif ( isset( $this->rules[$curChar] ) ) {
248  $found = 'open';
249  $rule = $this->rules[$curChar];
250  } else {
251  # Some versions of PHP have a strcspn which stops on null characters
252  # Ignore and continue
253  ++$i;
254  continue;
255  }
256  }
257  }
258 
259  if ( $found == 'angle' ) {
260  $matches = false;
261  // Handle </onlyinclude>
262  if ( $enableOnlyinclude
263  && substr( $text, $i, strlen( '</onlyinclude>' ) ) == '</onlyinclude>'
264  ) {
265  $findOnlyinclude = true;
266  continue;
267  }
268 
269  // Determine element name
270  if ( !preg_match( $elementsRegex, $text, $matches, 0, $i + 1 ) ) {
271  // Element name missing or not listed
272  self::addLiteral( $accum, '<' );
273  ++$i;
274  continue;
275  }
276  // Handle comments
277  if ( isset( $matches[2] ) && $matches[2] == '!--' ) {
278 
279  // To avoid leaving blank lines, when a sequence of
280  // space-separated comments is both preceded and followed by
281  // a newline (ignoring spaces), then
282  // trim leading and trailing spaces and the trailing newline.
283 
284  // Find the end
285  $endPos = strpos( $text, '-->', $i + 4 );
286  if ( $endPos === false ) {
287  // Unclosed comment in input, runs to end
288  $inner = substr( $text, $i );
289  $accum[] = [ 'comment', [ $inner ] ];
290  $i = $lengthText;
291  } else {
292  // Search backwards for leading whitespace
293  $wsStart = $i ? ( $i - strspn( $revText, " \t", $lengthText - $i ) ) : 0;
294 
295  // Search forwards for trailing whitespace
296  // $wsEnd will be the position of the last space (or the '>' if there's none)
297  $wsEnd = $endPos + 2 + strspn( $text, " \t", $endPos + 3 );
298 
299  // Keep looking forward as long as we're finding more
300  // comments.
301  $comments = [ [ $wsStart, $wsEnd ] ];
302  while ( substr( $text, $wsEnd + 1, 4 ) == '<!--' ) {
303  $c = strpos( $text, '-->', $wsEnd + 4 );
304  if ( $c === false ) {
305  break;
306  }
307  $c = $c + 2 + strspn( $text, " \t", $c + 3 );
308  $comments[] = [ $wsEnd + 1, $c ];
309  $wsEnd = $c;
310  }
311 
312  // Eat the line if possible
313  // TODO: This could theoretically be done if $wsStart == 0, i.e. for comments at
314  // the overall start. That's not how Sanitizer::removeHTMLcomments() did it, but
315  // it's a possible beneficial b/c break.
316  if ( $wsStart > 0 && substr( $text, $wsStart - 1, 1 ) == "\n"
317  && substr( $text, $wsEnd + 1, 1 ) == "\n"
318  ) {
319  // Remove leading whitespace from the end of the accumulator
320  $wsLength = $i - $wsStart;
321  $endIndex = count( $accum ) - 1;
322 
323  // Sanity check
324  if ( $wsLength > 0
325  && $endIndex >= 0
326  && is_string( $accum[$endIndex] )
327  && strspn( $accum[$endIndex], " \t", -$wsLength ) === $wsLength
328  ) {
329  $accum[$endIndex] = substr( $accum[$endIndex], 0, -$wsLength );
330  }
331 
332  // Dump all but the last comment to the accumulator
333  foreach ( $comments as $j => $com ) {
334  $startPos = $com[0];
335  $endPos = $com[1] + 1;
336  if ( $j == ( count( $comments ) - 1 ) ) {
337  break;
338  }
339  $inner = substr( $text, $startPos, $endPos - $startPos );
340  $accum[] = [ 'comment', [ $inner ] ];
341  }
342 
343  // Do a line-start run next time to look for headings after the comment
344  $fakeLineStart = true;
345  } else {
346  // No line to eat, just take the comment itself
347  $startPos = $i;
348  $endPos += 2;
349  }
350 
351  if ( $stack->top ) {
352  $part = $stack->top->getCurrentPart();
353  if ( !( isset( $part->commentEnd ) && $part->commentEnd == $wsStart - 1 ) ) {
354  $part->visualEnd = $wsStart;
355  }
356  // Else comments abutting, no change in visual end
357  $part->commentEnd = $endPos;
358  }
359  $i = $endPos + 1;
360  $inner = substr( $text, $startPos, $endPos - $startPos + 1 );
361  $accum[] = [ 'comment', [ $inner ] ];
362  }
363  continue;
364  }
365  $name = $matches[1];
366  $lowerName = strtolower( $name );
367  $attrStart = $i + strlen( $name ) + 1;
368 
369  // Find end of tag
370  $tagEndPos = $noMoreGT ? false : strpos( $text, '>', $attrStart );
371  if ( $tagEndPos === false ) {
372  // Infinite backtrack
373  // Disable tag search to prevent worst-case O(N^2) performance
374  $noMoreGT = true;
375  self::addLiteral( $accum, '<' );
376  ++$i;
377  continue;
378  }
379 
380  // Handle ignored tags
381  if ( in_array( $lowerName, $ignoredTags ) ) {
382  $accum[] = [ 'ignore', [ substr( $text, $i, $tagEndPos - $i + 1 ) ] ];
383  $i = $tagEndPos + 1;
384  continue;
385  }
386 
387  $tagStartPos = $i;
388  if ( $text[$tagEndPos - 1] == '/' ) {
389  // Short end tag
390  $attrEnd = $tagEndPos - 1;
391  $inner = null;
392  $i = $tagEndPos + 1;
393  $close = null;
394  } else {
395  $attrEnd = $tagEndPos;
396  // Find closing tag
397  if (
398  !isset( $noMoreClosingTag[$name] ) &&
399  preg_match( "/<\/" . preg_quote( $name, '/' ) . "\s*>/i",
400  $text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 )
401  ) {
402  $inner = substr( $text, $tagEndPos + 1, $matches[0][1] - $tagEndPos - 1 );
403  $i = $matches[0][1] + strlen( $matches[0][0] );
404  $close = $matches[0][0];
405  } else {
406  // No end tag
407  if ( in_array( $name, $xmlishAllowMissingEndTag ) ) {
408  // Let it run out to the end of the text.
409  $inner = substr( $text, $tagEndPos + 1 );
410  $i = $lengthText;
411  $close = null;
412  } else {
413  // Don't match the tag, treat opening tag as literal and resume parsing.
414  $i = $tagEndPos + 1;
415  self::addLiteral( $accum,
416  substr( $text, $tagStartPos, $tagEndPos + 1 - $tagStartPos ) );
417  // Cache results, otherwise we have O(N^2) performance for input like <foo><foo><foo>...
418  $noMoreClosingTag[$name] = true;
419  continue;
420  }
421  }
422  }
423  // <includeonly> and <noinclude> just become <ignore> tags
424  if ( in_array( $lowerName, $ignoredElements ) ) {
425  $accum[] = [ 'ignore', [ substr( $text, $tagStartPos, $i - $tagStartPos ) ] ];
426  continue;
427  }
428 
429  if ( $attrEnd <= $attrStart ) {
430  $attr = '';
431  } else {
432  // Note that the attr element contains the whitespace between name and attribute,
433  // this is necessary for precise reconstruction during pre-save transform.
434  $attr = substr( $text, $attrStart, $attrEnd - $attrStart );
435  }
436 
437  $children = [
438  [ 'name', [ $name ] ],
439  [ 'attr', [ $attr ] ] ];
440  if ( $inner !== null ) {
441  $children[] = [ 'inner', [ $inner ] ];
442  }
443  if ( $close !== null ) {
444  $children[] = [ 'close', [ $close ] ];
445  }
446  $accum[] = [ 'ext', $children ];
447  } elseif ( $found == 'line-start' ) {
448  // Is this the start of a heading?
449  // Line break belongs before the heading element in any case
450  if ( $fakeLineStart ) {
451  $fakeLineStart = false;
452  } else {
453  self::addLiteral( $accum, $curChar );
454  $i++;
455  }
456 
457  $count = strspn( $text, '=', $i, 6 );
458  if ( $count == 1 && $findEquals ) {
459  // DWIM: This looks kind of like a name/value separator.
460  // Let's let the equals handler have it and break the potential
461  // heading. This is heuristic, but AFAICT the methods for
462  // completely correct disambiguation are very complex.
463  } elseif ( $count > 0 ) {
464  $piece = [
465  'open' => "\n",
466  'close' => "\n",
467  'parts' => [ new PPDPart_Hash( str_repeat( '=', $count ) ) ],
468  'startPos' => $i,
469  'count' => $count ];
470  $stack->push( $piece );
471  $accum =& $stack->getAccum();
472  extract( $stack->getFlags() );
473  $i += $count;
474  }
475  } elseif ( $found == 'line-end' ) {
476  $piece = $stack->top;
477  // A heading must be open, otherwise \n wouldn't have been in the search list
478  assert( $piece->open === "\n" );
479  $part = $piece->getCurrentPart();
480  // Search back through the input to see if it has a proper close.
481  // Do this using the reversed string since the other solutions
482  // (end anchor, etc.) are inefficient.
483  $wsLength = strspn( $revText, " \t", $lengthText - $i );
484  $searchStart = $i - $wsLength;
485  if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) {
486  // Comment found at line end
487  // Search for equals signs before the comment
488  $searchStart = $part->visualEnd;
489  $searchStart -= strspn( $revText, " \t", $lengthText - $searchStart );
490  }
491  $count = $piece->count;
492  $equalsLength = strspn( $revText, '=', $lengthText - $searchStart );
493  if ( $equalsLength > 0 ) {
494  if ( $searchStart - $equalsLength == $piece->startPos ) {
495  // This is just a single string of equals signs on its own line
496  // Replicate the doHeadings behavior /={count}(.+)={count}/
497  // First find out how many equals signs there really are (don't stop at 6)
498  $count = $equalsLength;
499  if ( $count < 3 ) {
500  $count = 0;
501  } else {
502  $count = min( 6, intval( ( $count - 1 ) / 2 ) );
503  }
504  } else {
505  $count = min( $equalsLength, $count );
506  }
507  if ( $count > 0 ) {
508  // Normal match, output <h>
509  $element = [ [ 'possible-h',
510  array_merge(
511  [
512  [ '@level', [ $count ] ],
513  [ '@i', [ $headingIndex++ ] ]
514  ],
515  $accum
516  )
517  ] ];
518  } else {
519  // Single equals sign on its own line, count=0
520  $element = $accum;
521  }
522  } else {
523  // No match, no <h>, just pass down the inner text
524  $element = $accum;
525  }
526  // Unwind the stack
527  $stack->pop();
528  $accum =& $stack->getAccum();
529  extract( $stack->getFlags() );
530 
531  // Append the result to the enclosing accumulator
532  array_splice( $accum, count( $accum ), 0, $element );
533 
534  // Note that we do NOT increment the input pointer.
535  // This is because the closing linebreak could be the opening linebreak of
536  // another heading. Infinite loops are avoided because the next iteration MUST
537  // hit the heading open case above, which unconditionally increments the
538  // input pointer.
539  } elseif ( $found == 'open' ) {
540  # count opening brace characters
541  $count = strspn( $text, $curChar, $i );
542 
543  # we need to add to stack only if opening brace count is enough for one of the rules
544  if ( $count >= $rule['min'] ) {
545  # Add it to the stack
546  $piece = [
547  'open' => $curChar,
548  'close' => $rule['end'],
549  'count' => $count,
550  'lineStart' => ( $i > 0 && $text[$i - 1] == "\n" ),
551  ];
552 
553  $stack->push( $piece );
554  $accum =& $stack->getAccum();
555  extract( $stack->getFlags() );
556  } else {
557  # Add literal brace(s)
558  self::addLiteral( $accum, str_repeat( $curChar, $count ) );
559  }
560  $i += $count;
561  } elseif ( $found == 'close' ) {
562  $piece = $stack->top;
563  # lets check if there are enough characters for closing brace
564  $maxCount = $piece->count;
565  $count = strspn( $text, $curChar, $i, $maxCount );
566 
567  # check for maximum matching characters (if there are 5 closing
568  # characters, we will probably need only 3 - depending on the rules)
569  $rule = $this->rules[$piece->open];
570  if ( $count > $rule['max'] ) {
571  # The specified maximum exists in the callback array, unless the caller
572  # has made an error
573  $matchingCount = $rule['max'];
574  } else {
575  # Count is less than the maximum
576  # Skip any gaps in the callback array to find the true largest match
577  # Need to use array_key_exists not isset because the callback can be null
578  $matchingCount = $count;
579  while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $rule['names'] ) ) {
580  --$matchingCount;
581  }
582  }
583 
584  if ( $matchingCount <= 0 ) {
585  # No matching element found in callback array
586  # Output a literal closing brace and continue
587  self::addLiteral( $accum, str_repeat( $curChar, $count ) );
588  $i += $count;
589  continue;
590  }
591  $name = $rule['names'][$matchingCount];
592  if ( $name === null ) {
593  // No element, just literal text
594  $element = $piece->breakSyntax( $matchingCount );
595  self::addLiteral( $element, str_repeat( $rule['end'], $matchingCount ) );
596  } else {
597  # Create XML element
598  $parts = $piece->parts;
599  $titleAccum = $parts[0]->out;
600  unset( $parts[0] );
601 
602  $children = [];
603 
604  # The invocation is at the start of the line if lineStart is set in
605  # the stack, and all opening brackets are used up.
606  if ( $maxCount == $matchingCount && !empty( $piece->lineStart ) ) {
607  $children[] = [ '@lineStart', [ 1 ] ];
608  }
609  $titleNode = [ 'title', $titleAccum ];
610  $children[] = $titleNode;
611  $argIndex = 1;
612  foreach ( $parts as $part ) {
613  if ( isset( $part->eqpos ) ) {
614  $equalsNode = $part->out[$part->eqpos];
615  $nameNode = [ 'name', array_slice( $part->out, 0, $part->eqpos ) ];
616  $valueNode = [ 'value', array_slice( $part->out, $part->eqpos + 1 ) ];
617  $partNode = [ 'part', [ $nameNode, $equalsNode, $valueNode ] ];
618  $children[] = $partNode;
619  } else {
620  $nameNode = [ 'name', [ [ '@index', [ $argIndex++ ] ] ] ];
621  $valueNode = [ 'value', $part->out ];
622  $partNode = [ 'part', [ $nameNode, $valueNode ] ];
623  $children[] = $partNode;
624  }
625  }
626  $element = [ [ $name, $children ] ];
627  }
628 
629  # Advance input pointer
630  $i += $matchingCount;
631 
632  # Unwind the stack
633  $stack->pop();
634  $accum =& $stack->getAccum();
635 
636  # Re-add the old stack element if it still has unmatched opening characters remaining
637  if ( $matchingCount < $piece->count ) {
638  $piece->parts = [ new PPDPart_Hash ];
639  $piece->count -= $matchingCount;
640  # do we still qualify for any callback with remaining count?
641  $min = $this->rules[$piece->open]['min'];
642  if ( $piece->count >= $min ) {
643  $stack->push( $piece );
644  $accum =& $stack->getAccum();
645  } else {
646  self::addLiteral( $accum, str_repeat( $piece->open, $piece->count ) );
647  }
648  }
649 
650  extract( $stack->getFlags() );
651 
652  # Add XML element to the enclosing accumulator
653  array_splice( $accum, count( $accum ), 0, $element );
654  } elseif ( $found == 'pipe' ) {
655  $findEquals = true; // shortcut for getFlags()
656  $stack->addPart();
657  $accum =& $stack->getAccum();
658  ++$i;
659  } elseif ( $found == 'equals' ) {
660  $findEquals = false; // shortcut for getFlags()
661  $accum[] = [ 'equals', [ '=' ] ];
662  $stack->getCurrentPart()->eqpos = count( $accum ) - 1;
663  ++$i;
664  }
665  }
666 
667  # Output any remaining unclosed brackets
668  foreach ( $stack->stack as $piece ) {
669  array_splice( $stack->rootAccum, count( $stack->rootAccum ), 0, $piece->breakSyntax() );
670  }
671 
672  # Enable top-level headings
673  foreach ( $stack->rootAccum as &$node ) {
674  if ( is_array( $node ) && $node[PPNode_Hash_Tree::NAME] === 'possible-h' ) {
675  $node[PPNode_Hash_Tree::NAME] = 'h';
676  }
677  }
678 
679  $rootStore = [ [ 'root', $stack->rootAccum ] ];
680  $rootNode = new PPNode_Hash_Tree( $rootStore, 0 );
681 
682  // Cache
683  $tree = json_encode( $rootStore, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
684  if ( $tree !== false ) {
685  $this->cacheSetTree( $text, $flags, $tree );
686  }
687 
688  return $rootNode;
689  }
690 
691  private static function addLiteral( array &$accum, $text ) {
692  $n = count( $accum );
693  if ( $n && is_string( $accum[$n - 1] ) ) {
694  $accum[$n - 1] .= $text;
695  } else {
696  $accum[] = $text;
697  }
698  }
699 }
700 
705 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
706 class PPDStack_Hash extends PPDStack {
707  // @codingStandardsIgnoreEnd
708 
709  public function __construct() {
710  $this->elementClass = 'PPDStackElement_Hash';
711  parent::__construct();
712  $this->rootAccum = [];
713  }
714 }
715 
719 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
721  // @codingStandardsIgnoreEnd
722 
723  public function __construct( $data = [] ) {
724  $this->partClass = 'PPDPart_Hash';
725  parent::__construct( $data );
726  }
727 
734  public function breakSyntax( $openingCount = false ) {
735  if ( $this->open == "\n" ) {
736  $accum = $this->parts[0]->out;
737  } else {
738  if ( $openingCount === false ) {
739  $openingCount = $this->count;
740  }
741  $accum = [ str_repeat( $this->open, $openingCount ) ];
742  $lastIndex = 0;
743  $first = true;
744  foreach ( $this->parts as $part ) {
745  if ( $first ) {
746  $first = false;
747  } elseif ( is_string( $accum[$lastIndex] ) ) {
748  $accum[$lastIndex] .= '|';
749  } else {
750  $accum[++$lastIndex] = '|';
751  }
752  foreach ( $part->out as $node ) {
753  if ( is_string( $node ) && is_string( $accum[$lastIndex] ) ) {
754  $accum[$lastIndex] .= $node;
755  } else {
756  $accum[++$lastIndex] = $node;
757  }
758  }
759  }
760  }
761  return $accum;
762  }
763 }
764 
768 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
769 class PPDPart_Hash extends PPDPart {
770  // @codingStandardsIgnoreEnd
771 
772  public function __construct( $out = '' ) {
773  if ( $out !== '' ) {
774  $accum = [ $out ];
775  } else {
776  $accum = [];
777  }
778  parent::__construct( $accum );
779  }
780 }
781 
786 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
787 class PPFrame_Hash implements PPFrame {
788  // @codingStandardsIgnoreEnd
789 
793  public $parser;
794 
799 
803  public $title;
804  public $titleCache;
805 
811 
816  public $depth;
817 
818  private $volatile = false;
819  private $ttl = null;
820 
825 
830  public function __construct( $preprocessor ) {
831  $this->preprocessor = $preprocessor;
832  $this->parser = $preprocessor->parser;
833  $this->title = $this->parser->mTitle;
834  $this->titleCache = [ $this->title ? $this->title->getPrefixedDBkey() : false ];
835  $this->loopCheckHash = [];
836  $this->depth = 0;
837  $this->childExpansionCache = [];
838  }
839 
850  public function newChild( $args = false, $title = false, $indexOffset = 0 ) {
851  $namedArgs = [];
852  $numberedArgs = [];
853  if ( $title === false ) {
855  }
856  if ( $args !== false ) {
857  if ( $args instanceof PPNode_Hash_Array ) {
858  $args = $args->value;
859  } elseif ( !is_array( $args ) ) {
860  throw new MWException( __METHOD__ . ': $args must be array or PPNode_Hash_Array' );
861  }
862  foreach ( $args as $arg ) {
863  $bits = $arg->splitArg();
864  if ( $bits['index'] !== '' ) {
865  // Numbered parameter
866  $index = $bits['index'] - $indexOffset;
867  if ( isset( $namedArgs[$index] ) || isset( $numberedArgs[$index] ) ) {
868  $this->parser->getOutput()->addWarning( wfMessage( 'duplicate-args-warning',
869  wfEscapeWikiText( $this->title ),
871  wfEscapeWikiText( $index ) )->text() );
872  $this->parser->addTrackingCategory( 'duplicate-args-category' );
873  }
874  $numberedArgs[$index] = $bits['value'];
875  unset( $namedArgs[$index] );
876  } else {
877  // Named parameter
878  $name = trim( $this->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
879  if ( isset( $namedArgs[$name] ) || isset( $numberedArgs[$name] ) ) {
880  $this->parser->getOutput()->addWarning( wfMessage( 'duplicate-args-warning',
881  wfEscapeWikiText( $this->title ),
883  wfEscapeWikiText( $name ) )->text() );
884  $this->parser->addTrackingCategory( 'duplicate-args-category' );
885  }
886  $namedArgs[$name] = $bits['value'];
887  unset( $numberedArgs[$name] );
888  }
889  }
890  }
891  return new PPTemplateFrame_Hash( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title );
892  }
893 
901  public function cachedExpand( $key, $root, $flags = 0 ) {
902  // we don't have a parent, so we don't have a cache
903  return $this->expand( $root, $flags );
904  }
905 
912  public function expand( $root, $flags = 0 ) {
913  static $expansionDepth = 0;
914  if ( is_string( $root ) ) {
915  return $root;
916  }
917 
918  if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->getMaxPPNodeCount() ) {
919  $this->parser->limitationWarn( 'node-count-exceeded',
920  $this->parser->mPPNodeCount,
921  $this->parser->mOptions->getMaxPPNodeCount()
922  );
923  return '<span class="error">Node-count limit exceeded</span>';
924  }
925  if ( $expansionDepth > $this->parser->mOptions->getMaxPPExpandDepth() ) {
926  $this->parser->limitationWarn( 'expansion-depth-exceeded',
927  $expansionDepth,
928  $this->parser->mOptions->getMaxPPExpandDepth()
929  );
930  return '<span class="error">Expansion depth limit exceeded</span>';
931  }
932  ++$expansionDepth;
933  if ( $expansionDepth > $this->parser->mHighestExpansionDepth ) {
934  $this->parser->mHighestExpansionDepth = $expansionDepth;
935  }
936 
937  $outStack = [ '', '' ];
938  $iteratorStack = [ false, $root ];
939  $indexStack = [ 0, 0 ];
940 
941  while ( count( $iteratorStack ) > 1 ) {
942  $level = count( $outStack ) - 1;
943  $iteratorNode =& $iteratorStack[$level];
944  $out =& $outStack[$level];
945  $index =& $indexStack[$level];
946 
947  if ( is_array( $iteratorNode ) ) {
948  if ( $index >= count( $iteratorNode ) ) {
949  // All done with this iterator
950  $iteratorStack[$level] = false;
951  $contextNode = false;
952  } else {
953  $contextNode = $iteratorNode[$index];
954  $index++;
955  }
956  } elseif ( $iteratorNode instanceof PPNode_Hash_Array ) {
957  if ( $index >= $iteratorNode->getLength() ) {
958  // All done with this iterator
959  $iteratorStack[$level] = false;
960  $contextNode = false;
961  } else {
962  $contextNode = $iteratorNode->item( $index );
963  $index++;
964  }
965  } else {
966  // Copy to $contextNode and then delete from iterator stack,
967  // because this is not an iterator but we do have to execute it once
968  $contextNode = $iteratorStack[$level];
969  $iteratorStack[$level] = false;
970  }
971 
972  $newIterator = false;
973  $contextName = false;
974  $contextChildren = false;
975 
976  if ( $contextNode === false ) {
977  // nothing to do
978  } elseif ( is_string( $contextNode ) ) {
979  $out .= $contextNode;
980  } elseif ( $contextNode instanceof PPNode_Hash_Array ) {
981  $newIterator = $contextNode;
982  } elseif ( $contextNode instanceof PPNode_Hash_Attr ) {
983  // No output
984  } elseif ( $contextNode instanceof PPNode_Hash_Text ) {
985  $out .= $contextNode->value;
986  } elseif ( $contextNode instanceof PPNode_Hash_Tree ) {
987  $contextName = $contextNode->name;
988  $contextChildren = $contextNode->getRawChildren();
989  } elseif ( is_array( $contextNode ) ) {
990  // Node descriptor array
991  if ( count( $contextNode ) !== 2 ) {
992  throw new MWException( __METHOD__.
993  ': found an array where a node descriptor should be' );
994  }
995  list( $contextName, $contextChildren ) = $contextNode;
996  } else {
997  throw new MWException( __METHOD__ . ': Invalid parameter type' );
998  }
999 
1000  // Handle node descriptor array or tree object
1001  if ( $contextName === false ) {
1002  // Not a node, already handled above
1003  } elseif ( $contextName[0] === '@' ) {
1004  // Attribute: no output
1005  } elseif ( $contextName === 'template' ) {
1006  # Double-brace expansion
1007  $bits = PPNode_Hash_Tree::splitRawTemplate( $contextChildren );
1008  if ( $flags & PPFrame::NO_TEMPLATES ) {
1009  $newIterator = $this->virtualBracketedImplode(
1010  '{{', '|', '}}',
1011  $bits['title'],
1012  $bits['parts']
1013  );
1014  } else {
1015  $ret = $this->parser->braceSubstitution( $bits, $this );
1016  if ( isset( $ret['object'] ) ) {
1017  $newIterator = $ret['object'];
1018  } else {
1019  $out .= $ret['text'];
1020  }
1021  }
1022  } elseif ( $contextName === 'tplarg' ) {
1023  # Triple-brace expansion
1024  $bits = PPNode_Hash_Tree::splitRawTemplate( $contextChildren );
1025  if ( $flags & PPFrame::NO_ARGS ) {
1026  $newIterator = $this->virtualBracketedImplode(
1027  '{{{', '|', '}}}',
1028  $bits['title'],
1029  $bits['parts']
1030  );
1031  } else {
1032  $ret = $this->parser->argSubstitution( $bits, $this );
1033  if ( isset( $ret['object'] ) ) {
1034  $newIterator = $ret['object'];
1035  } else {
1036  $out .= $ret['text'];
1037  }
1038  }
1039  } elseif ( $contextName === 'comment' ) {
1040  # HTML-style comment
1041  # Remove it in HTML, pre+remove and STRIP_COMMENTS modes
1042  # Not in RECOVER_COMMENTS mode (msgnw) though.
1043  if ( ( $this->parser->ot['html']
1044  || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() )
1046  ) && !( $flags & PPFrame::RECOVER_COMMENTS )
1047  ) {
1048  $out .= '';
1049  } elseif ( $this->parser->ot['wiki'] && !( $flags & PPFrame::RECOVER_COMMENTS ) ) {
1050  # Add a strip marker in PST mode so that pstPass2() can
1051  # run some old-fashioned regexes on the result.
1052  # Not in RECOVER_COMMENTS mode (extractSections) though.
1053  $out .= $this->parser->insertStripItem( $contextChildren[0] );
1054  } else {
1055  # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
1056  $out .= $contextChildren[0];
1057  }
1058  } elseif ( $contextName === 'ignore' ) {
1059  # Output suppression used by <includeonly> etc.
1060  # OT_WIKI will only respect <ignore> in substed templates.
1061  # The other output types respect it unless NO_IGNORE is set.
1062  # extractSections() sets NO_IGNORE and so never respects it.
1063  if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] )
1064  || ( $flags & PPFrame::NO_IGNORE )
1065  ) {
1066  $out .= $contextChildren[0];
1067  } else {
1068  // $out .= '';
1069  }
1070  } elseif ( $contextName === 'ext' ) {
1071  # Extension tag
1072  $bits = PPNode_Hash_Tree::splitRawExt( $contextChildren ) +
1073  [ 'attr' => null, 'inner' => null, 'close' => null ];
1074  if ( $flags & PPFrame::NO_TAGS ) {
1075  $s = '<' . $bits['name']->getFirstChild()->value;
1076  if ( $bits['attr'] ) {
1077  $s .= $bits['attr']->getFirstChild()->value;
1078  }
1079  if ( $bits['inner'] ) {
1080  $s .= '>' . $bits['inner']->getFirstChild()->value;
1081  if ( $bits['close'] ) {
1082  $s .= $bits['close']->getFirstChild()->value;
1083  }
1084  } else {
1085  $s .= '/>';
1086  }
1087  $out .= $s;
1088  } else {
1089  $out .= $this->parser->extensionSubstitution( $bits, $this );
1090  }
1091  } elseif ( $contextName === 'h' ) {
1092  # Heading
1093  if ( $this->parser->ot['html'] ) {
1094  # Expand immediately and insert heading index marker
1095  $s = $this->expand( $contextChildren, $flags );
1096  $bits = PPNode_Hash_Tree::splitRawHeading( $contextChildren );
1097  $titleText = $this->title->getPrefixedDBkey();
1098  $this->parser->mHeadings[] = [ $titleText, $bits['i'] ];
1099  $serial = count( $this->parser->mHeadings ) - 1;
1100  $marker = Parser::MARKER_PREFIX . "-h-$serial-" . Parser::MARKER_SUFFIX;
1101  $s = substr( $s, 0, $bits['level'] ) . $marker . substr( $s, $bits['level'] );
1102  $this->parser->mStripState->addGeneral( $marker, '' );
1103  $out .= $s;
1104  } else {
1105  # Expand in virtual stack
1106  $newIterator = $contextChildren;
1107  }
1108  } else {
1109  # Generic recursive expansion
1110  $newIterator = $contextChildren;
1111  }
1112 
1113  if ( $newIterator !== false ) {
1114  $outStack[] = '';
1115  $iteratorStack[] = $newIterator;
1116  $indexStack[] = 0;
1117  } elseif ( $iteratorStack[$level] === false ) {
1118  // Return accumulated value to parent
1119  // With tail recursion
1120  while ( $iteratorStack[$level] === false && $level > 0 ) {
1121  $outStack[$level - 1] .= $out;
1122  array_pop( $outStack );
1123  array_pop( $iteratorStack );
1124  array_pop( $indexStack );
1125  $level--;
1126  }
1127  }
1128  }
1129  --$expansionDepth;
1130  return $outStack[0];
1131  }
1132 
1139  public function implodeWithFlags( $sep, $flags /*, ... */ ) {
1140  $args = array_slice( func_get_args(), 2 );
1141 
1142  $first = true;
1143  $s = '';
1144  foreach ( $args as $root ) {
1145  if ( $root instanceof PPNode_Hash_Array ) {
1146  $root = $root->value;
1147  }
1148  if ( !is_array( $root ) ) {
1149  $root = [ $root ];
1150  }
1151  foreach ( $root as $node ) {
1152  if ( $first ) {
1153  $first = false;
1154  } else {
1155  $s .= $sep;
1156  }
1157  $s .= $this->expand( $node, $flags );
1158  }
1159  }
1160  return $s;
1161  }
1162 
1170  public function implode( $sep /*, ... */ ) {
1171  $args = array_slice( func_get_args(), 1 );
1172 
1173  $first = true;
1174  $s = '';
1175  foreach ( $args as $root ) {
1176  if ( $root instanceof PPNode_Hash_Array ) {
1177  $root = $root->value;
1178  }
1179  if ( !is_array( $root ) ) {
1180  $root = [ $root ];
1181  }
1182  foreach ( $root as $node ) {
1183  if ( $first ) {
1184  $first = false;
1185  } else {
1186  $s .= $sep;
1187  }
1188  $s .= $this->expand( $node );
1189  }
1190  }
1191  return $s;
1192  }
1193 
1202  public function virtualImplode( $sep /*, ... */ ) {
1203  $args = array_slice( func_get_args(), 1 );
1204  $out = [];
1205  $first = true;
1206 
1207  foreach ( $args as $root ) {
1208  if ( $root instanceof PPNode_Hash_Array ) {
1209  $root = $root->value;
1210  }
1211  if ( !is_array( $root ) ) {
1212  $root = [ $root ];
1213  }
1214  foreach ( $root as $node ) {
1215  if ( $first ) {
1216  $first = false;
1217  } else {
1218  $out[] = $sep;
1219  }
1220  $out[] = $node;
1221  }
1222  }
1223  return new PPNode_Hash_Array( $out );
1224  }
1225 
1235  public function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) {
1236  $args = array_slice( func_get_args(), 3 );
1237  $out = [ $start ];
1238  $first = true;
1239 
1240  foreach ( $args as $root ) {
1241  if ( $root instanceof PPNode_Hash_Array ) {
1242  $root = $root->value;
1243  }
1244  if ( !is_array( $root ) ) {
1245  $root = [ $root ];
1246  }
1247  foreach ( $root as $node ) {
1248  if ( $first ) {
1249  $first = false;
1250  } else {
1251  $out[] = $sep;
1252  }
1253  $out[] = $node;
1254  }
1255  }
1256  $out[] = $end;
1257  return new PPNode_Hash_Array( $out );
1258  }
1259 
1260  public function __toString() {
1261  return 'frame{}';
1262  }
1263 
1268  public function getPDBK( $level = false ) {
1269  if ( $level === false ) {
1270  return $this->title->getPrefixedDBkey();
1271  } else {
1272  return isset( $this->titleCache[$level] ) ? $this->titleCache[$level] : false;
1273  }
1274  }
1275 
1279  public function getArguments() {
1280  return [];
1281  }
1282 
1286  public function getNumberedArguments() {
1287  return [];
1288  }
1289 
1293  public function getNamedArguments() {
1294  return [];
1295  }
1296 
1302  public function isEmpty() {
1303  return true;
1304  }
1305 
1310  public function getArgument( $name ) {
1311  return false;
1312  }
1313 
1321  public function loopCheck( $title ) {
1322  return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] );
1323  }
1324 
1330  public function isTemplate() {
1331  return false;
1332  }
1333 
1339  public function getTitle() {
1340  return $this->title;
1341  }
1342 
1348  public function setVolatile( $flag = true ) {
1349  $this->volatile = $flag;
1350  }
1351 
1357  public function isVolatile() {
1358  return $this->volatile;
1359  }
1360 
1366  public function setTTL( $ttl ) {
1367  if ( $ttl !== null && ( $this->ttl === null || $ttl < $this->ttl ) ) {
1368  $this->ttl = $ttl;
1369  }
1370  }
1371 
1377  public function getTTL() {
1378  return $this->ttl;
1379  }
1380 }
1381 
1386 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
1388  // @codingStandardsIgnoreEnd
1389 
1392 
1400  public function __construct( $preprocessor, $parent = false, $numberedArgs = [],
1401  $namedArgs = [], $title = false
1402  ) {
1403  parent::__construct( $preprocessor );
1404 
1405  $this->parent = $parent;
1406  $this->numberedArgs = $numberedArgs;
1407  $this->namedArgs = $namedArgs;
1408  $this->title = $title;
1409  $pdbk = $title ? $title->getPrefixedDBkey() : false;
1410  $this->titleCache = $parent->titleCache;
1411  $this->titleCache[] = $pdbk;
1412  $this->loopCheckHash = /*clone*/ $parent->loopCheckHash;
1413  if ( $pdbk !== false ) {
1414  $this->loopCheckHash[$pdbk] = true;
1415  }
1416  $this->depth = $parent->depth + 1;
1417  $this->numberedExpansionCache = $this->namedExpansionCache = [];
1418  }
1419 
1420  public function __toString() {
1421  $s = 'tplframe{';
1422  $first = true;
1423  $args = $this->numberedArgs + $this->namedArgs;
1424  foreach ( $args as $name => $value ) {
1425  if ( $first ) {
1426  $first = false;
1427  } else {
1428  $s .= ', ';
1429  }
1430  $s .= "\"$name\":\"" .
1431  str_replace( '"', '\\"', $value->__toString() ) . '"';
1432  }
1433  $s .= '}';
1434  return $s;
1435  }
1436 
1444  public function cachedExpand( $key, $root, $flags = 0 ) {
1445  if ( isset( $this->parent->childExpansionCache[$key] ) ) {
1446  return $this->parent->childExpansionCache[$key];
1447  }
1448  $retval = $this->expand( $root, $flags );
1449  if ( !$this->isVolatile() ) {
1450  $this->parent->childExpansionCache[$key] = $retval;
1451  }
1452  return $retval;
1453  }
1454 
1460  public function isEmpty() {
1461  return !count( $this->numberedArgs ) && !count( $this->namedArgs );
1462  }
1463 
1467  public function getArguments() {
1468  $arguments = [];
1469  foreach ( array_merge(
1470  array_keys( $this->numberedArgs ),
1471  array_keys( $this->namedArgs ) ) as $key ) {
1472  $arguments[$key] = $this->getArgument( $key );
1473  }
1474  return $arguments;
1475  }
1476 
1480  public function getNumberedArguments() {
1481  $arguments = [];
1482  foreach ( array_keys( $this->numberedArgs ) as $key ) {
1483  $arguments[$key] = $this->getArgument( $key );
1484  }
1485  return $arguments;
1486  }
1487 
1491  public function getNamedArguments() {
1492  $arguments = [];
1493  foreach ( array_keys( $this->namedArgs ) as $key ) {
1494  $arguments[$key] = $this->getArgument( $key );
1495  }
1496  return $arguments;
1497  }
1498 
1503  public function getNumberedArgument( $index ) {
1504  if ( !isset( $this->numberedArgs[$index] ) ) {
1505  return false;
1506  }
1507  if ( !isset( $this->numberedExpansionCache[$index] ) ) {
1508  # No trimming for unnamed arguments
1509  $this->numberedExpansionCache[$index] = $this->parent->expand(
1510  $this->numberedArgs[$index],
1512  );
1513  }
1514  return $this->numberedExpansionCache[$index];
1515  }
1516 
1521  public function getNamedArgument( $name ) {
1522  if ( !isset( $this->namedArgs[$name] ) ) {
1523  return false;
1524  }
1525  if ( !isset( $this->namedExpansionCache[$name] ) ) {
1526  # Trim named arguments post-expand, for backwards compatibility
1527  $this->namedExpansionCache[$name] = trim(
1528  $this->parent->expand( $this->namedArgs[$name], PPFrame::STRIP_COMMENTS ) );
1529  }
1530  return $this->namedExpansionCache[$name];
1531  }
1532 
1537  public function getArgument( $name ) {
1538  $text = $this->getNumberedArgument( $name );
1539  if ( $text === false ) {
1540  $text = $this->getNamedArgument( $name );
1541  }
1542  return $text;
1543  }
1544 
1550  public function isTemplate() {
1551  return true;
1552  }
1553 
1554  public function setVolatile( $flag = true ) {
1555  parent::setVolatile( $flag );
1556  $this->parent->setVolatile( $flag );
1557  }
1558 
1559  public function setTTL( $ttl ) {
1560  parent::setTTL( $ttl );
1561  $this->parent->setTTL( $ttl );
1562  }
1563 }
1564 
1569 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
1571  // @codingStandardsIgnoreEnd
1572 
1573  public $args;
1574 
1575  public function __construct( $preprocessor, $args ) {
1576  parent::__construct( $preprocessor );
1577  $this->args = $args;
1578  }
1579 
1580  public function __toString() {
1581  $s = 'cstmframe{';
1582  $first = true;
1583  foreach ( $this->args as $name => $value ) {
1584  if ( $first ) {
1585  $first = false;
1586  } else {
1587  $s .= ', ';
1588  }
1589  $s .= "\"$name\":\"" .
1590  str_replace( '"', '\\"', $value->__toString() ) . '"';
1591  }
1592  $s .= '}';
1593  return $s;
1594  }
1595 
1599  public function isEmpty() {
1600  return !count( $this->args );
1601  }
1602 
1607  public function getArgument( $index ) {
1608  if ( !isset( $this->args[$index] ) ) {
1609  return false;
1610  }
1611  return $this->args[$index];
1612  }
1613 
1614  public function getArguments() {
1615  return $this->args;
1616  }
1617 }
1618 
1622 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
1623 class PPNode_Hash_Tree implements PPNode {
1624  // @codingStandardsIgnoreEnd
1625 
1626  public $name;
1627 
1633  private $rawChildren;
1634 
1638  private $store;
1639 
1643  private $index;
1644 
1649  const NAME = 0;
1650 
1655  const CHILDREN = 1;
1656 
1664  public function __construct( array $store, $index ) {
1665  $this->store = $store;
1666  $this->index = $index;
1667  list( $this->name, $this->rawChildren ) = $this->store[$index];
1668  }
1669 
1678  public static function factory( array $store, $index ) {
1679  if ( !isset( $store[$index] ) ) {
1680  return false;
1681  }
1682 
1683  $descriptor = $store[$index];
1684  if ( is_string( $descriptor ) ) {
1685  $class = 'PPNode_Hash_Text';
1686  } elseif ( is_array( $descriptor ) ) {
1687  if ( $descriptor[self::NAME][0] === '@' ) {
1688  $class = 'PPNode_Hash_Attr';
1689  } else {
1690  $class = 'PPNode_Hash_Tree';
1691  }
1692  } else {
1693  throw new MWException( __METHOD__.': invalid node descriptor' );
1694  }
1695  return new $class( $store, $index );
1696  }
1697 
1701  public function __toString() {
1702  $inner = '';
1703  $attribs = '';
1704  for ( $node = $this->getFirstChild(); $node; $node = $node->getNextSibling() ) {
1705  if ( $node instanceof PPNode_Hash_Attr ) {
1706  $attribs .= ' ' . $node->name . '="' . htmlspecialchars( $node->value ) . '"';
1707  } else {
1708  $inner .= $node->__toString();
1709  }
1710  }
1711  if ( $inner === '' ) {
1712  return "<{$this->name}$attribs/>";
1713  } else {
1714  return "<{$this->name}$attribs>$inner</{$this->name}>";
1715  }
1716  }
1717 
1721  public function getChildren() {
1722  $children = [];
1723  foreach ( $this->rawChildren as $i => $child ) {
1724  $children[] = self::factory( $this->rawChildren, $i );
1725  }
1726  return new PPNode_Hash_Array( $children );
1727  }
1728 
1736  public function getFirstChild() {
1737  if ( !isset( $this->rawChildren[0] ) ) {
1738  return false;
1739  } else {
1740  return self::factory( $this->rawChildren, 0 );
1741  }
1742  }
1743 
1751  public function getNextSibling() {
1752  return self::factory( $this->store, $this->index + 1 );
1753  }
1754 
1761  public function getChildrenOfType( $name ) {
1762  $children = [];
1763  foreach ( $this->rawChildren as $i => $child ) {
1764  if ( is_array( $child ) && $child[self::NAME] === $name ) {
1765  $children[] = self::factory( $this->rawChildren, $i );
1766  }
1767  }
1768  return new PPNode_Hash_Array( $children );
1769  }
1770 
1775  public function getRawChildren() {
1776  return $this->rawChildren;
1777  }
1778 
1782  public function getLength() {
1783  return false;
1784  }
1785 
1790  public function item( $i ) {
1791  return false;
1792  }
1793 
1797  public function getName() {
1798  return $this->name;
1799  }
1800 
1810  public function splitArg() {
1811  return self::splitRawArg( $this->rawChildren );
1812  }
1813 
1817  public static function splitRawArg( array $children ) {
1818  $bits = [];
1819  foreach ( $children as $i => $child ) {
1820  if ( !is_array( $child ) ) {
1821  continue;
1822  }
1823  if ( $child[self::NAME] === 'name' ) {
1824  $bits['name'] = new self( $children, $i );
1825  if ( isset( $child[self::CHILDREN][0][self::NAME] )
1826  && $child[self::CHILDREN][0][self::NAME] === '@index'
1827  ) {
1828  $bits['index'] = $child[self::CHILDREN][0][self::CHILDREN][0];
1829  }
1830  } elseif ( $child[self::NAME] === 'value' ) {
1831  $bits['value'] = new self( $children, $i );
1832  }
1833  }
1834 
1835  if ( !isset( $bits['name'] ) ) {
1836  throw new MWException( 'Invalid brace node passed to ' . __METHOD__ );
1837  }
1838  if ( !isset( $bits['index'] ) ) {
1839  $bits['index'] = '';
1840  }
1841  return $bits;
1842  }
1843 
1851  public function splitExt() {
1852  return self::splitRawExt( $this->rawChildren );
1853  }
1854 
1858  public static function splitRawExt( array $children ) {
1859  $bits = [];
1860  foreach ( $children as $i => $child ) {
1861  if ( !is_array( $child ) ) {
1862  continue;
1863  }
1864  switch ( $child[self::NAME] ) {
1865  case 'name':
1866  $bits['name'] = new self( $children, $i );
1867  break;
1868  case 'attr':
1869  $bits['attr'] = new self( $children, $i );
1870  break;
1871  case 'inner':
1872  $bits['inner'] = new self( $children, $i );
1873  break;
1874  case 'close':
1875  $bits['close'] = new self( $children, $i );
1876  break;
1877  }
1878  }
1879  if ( !isset( $bits['name'] ) ) {
1880  throw new MWException( 'Invalid ext node passed to ' . __METHOD__ );
1881  }
1882  return $bits;
1883  }
1884 
1891  public function splitHeading() {
1892  if ( $this->name !== 'h' ) {
1893  throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
1894  }
1895  return self::splitRawHeading( $this->rawChildren );
1896  }
1897 
1901  public static function splitRawHeading( array $children ) {
1902  $bits = [];
1903  foreach ( $children as $i => $child ) {
1904  if ( !is_array( $child ) ) {
1905  continue;
1906  }
1907  if ( $child[self::NAME] === '@i' ) {
1908  $bits['i'] = $child[self::CHILDREN][0];
1909  } elseif ( $child[self::NAME] === '@level' ) {
1910  $bits['level'] = $child[self::CHILDREN][0];
1911  }
1912  }
1913  if ( !isset( $bits['i'] ) ) {
1914  throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
1915  }
1916  return $bits;
1917  }
1918 
1925  public function splitTemplate() {
1926  return self::splitRawTemplate( $this->rawChildren );
1927  }
1928 
1932  public static function splitRawTemplate( array $children ) {
1933  $parts = [];
1934  $bits = [ 'lineStart' => '' ];
1935  foreach ( $children as $i => $child ) {
1936  if ( !is_array( $child ) ) {
1937  continue;
1938  }
1939  switch ( $child[self::NAME] ) {
1940  case 'title':
1941  $bits['title'] = new self( $children, $i );
1942  break;
1943  case 'part':
1944  $parts[] = new self( $children, $i );
1945  break;
1946  case '@lineStart':
1947  $bits['lineStart'] = '1';
1948  break;
1949  }
1950  }
1951  if ( !isset( $bits['title'] ) ) {
1952  throw new MWException( 'Invalid node passed to ' . __METHOD__ );
1953  }
1954  $bits['parts'] = new PPNode_Hash_Array( $parts );
1955  return $bits;
1956  }
1957 }
1958 
1962 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
1963 class PPNode_Hash_Text implements PPNode {
1964  // @codingStandardsIgnoreEnd
1965 
1966  public $value;
1967  private $store, $index;
1968 
1976  public function __construct( array $store, $index ) {
1977  $this->value = $store[$index];
1978  if ( !is_scalar( $this->value ) ) {
1979  throw new MWException( __CLASS__ . ' given object instead of string' );
1980  }
1981  $this->store = $store;
1982  $this->index = $index;
1983  }
1984 
1985  public function __toString() {
1986  return htmlspecialchars( $this->value );
1987  }
1988 
1989  public function getNextSibling() {
1990  return PPNode_Hash_Tree::factory( $this->store, $this->index + 1 );
1991  }
1992 
1993  public function getChildren() {
1994  return false;
1995  }
1996 
1997  public function getFirstChild() {
1998  return false;
1999  }
2000 
2001  public function getChildrenOfType( $name ) {
2002  return false;
2003  }
2004 
2005  public function getLength() {
2006  return false;
2007  }
2008 
2009  public function item( $i ) {
2010  return false;
2011  }
2012 
2013  public function getName() {
2014  return '#text';
2015  }
2016 
2017  public function splitArg() {
2018  throw new MWException( __METHOD__ . ': not supported' );
2019  }
2020 
2021  public function splitExt() {
2022  throw new MWException( __METHOD__ . ': not supported' );
2023  }
2024 
2025  public function splitHeading() {
2026  throw new MWException( __METHOD__ . ': not supported' );
2027  }
2028 }
2029 
2033 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
2034 class PPNode_Hash_Array implements PPNode {
2035  // @codingStandardsIgnoreEnd
2036 
2037  public $value;
2038 
2039  public function __construct( $value ) {
2040  $this->value = $value;
2041  }
2042 
2043  public function __toString() {
2044  return var_export( $this, true );
2045  }
2046 
2047  public function getLength() {
2048  return count( $this->value );
2049  }
2050 
2051  public function item( $i ) {
2052  return $this->value[$i];
2053  }
2054 
2055  public function getName() {
2056  return '#nodelist';
2057  }
2058 
2059  public function getNextSibling() {
2060  return false;
2061  }
2062 
2063  public function getChildren() {
2064  return false;
2065  }
2066 
2067  public function getFirstChild() {
2068  return false;
2069  }
2070 
2071  public function getChildrenOfType( $name ) {
2072  return false;
2073  }
2074 
2075  public function splitArg() {
2076  throw new MWException( __METHOD__ . ': not supported' );
2077  }
2078 
2079  public function splitExt() {
2080  throw new MWException( __METHOD__ . ': not supported' );
2081  }
2082 
2083  public function splitHeading() {
2084  throw new MWException( __METHOD__ . ': not supported' );
2085  }
2086 }
2087 
2091 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
2092 class PPNode_Hash_Attr implements PPNode {
2093  // @codingStandardsIgnoreEnd
2094 
2095  public $name, $value;
2096  private $store, $index;
2097 
2105  public function __construct( array $store, $index ) {
2106  $descriptor = $store[$index];
2107  if ( $descriptor[PPNode_Hash_Tree::NAME][0] !== '@' ) {
2108  throw new MWException( __METHOD__.': invalid name in attribute descriptor' );
2109  }
2110  $this->name = substr( $descriptor[PPNode_Hash_Tree::NAME], 1 );
2111  $this->value = $descriptor[PPNode_Hash_Tree::CHILDREN][0];
2112  $this->store = $store;
2113  $this->index = $index;
2114  }
2115 
2116  public function __toString() {
2117  return "<@{$this->name}>" . htmlspecialchars( $this->value ) . "</@{$this->name}>";
2118  }
2119 
2120  public function getName() {
2121  return $this->name;
2122  }
2123 
2124  public function getNextSibling() {
2125  return PPNode_Hash_Tree::factory( $this->store, $this->index + 1 );
2126  }
2127 
2128  public function getChildren() {
2129  return false;
2130  }
2131 
2132  public function getFirstChild() {
2133  return false;
2134  }
2135 
2136  public function getChildrenOfType( $name ) {
2137  return false;
2138  }
2139 
2140  public function getLength() {
2141  return false;
2142  }
2143 
2144  public function item( $i ) {
2145  return false;
2146  }
2147 
2148  public function splitArg() {
2149  throw new MWException( __METHOD__ . ': not supported' );
2150  }
2151 
2152  public function splitExt() {
2153  throw new MWException( __METHOD__ . ': not supported' );
2154  }
2155 
2156  public function splitHeading() {
2157  throw new MWException( __METHOD__ . ': not supported' );
2158  }
2159 }
splitArg()
Split a "" node into an associative array containing:
const MARKER_PREFIX
Definition: Parser.php:134
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition: deferred.txt:11
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output $out
Definition: hooks.txt:802
the array() calling protocol came about after MediaWiki 1.4rc1.
splitTemplate()
Split a "