MediaWiki  1.27.1
Preprocessor_Hash.php
Go to the documentation of this file.
1 <?php
30 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
32  // @codingStandardsIgnoreEnd
33 
37  public $parser;
38 
39  const CACHE_PREFIX = 'preprocess-hash';
40 
41  public function __construct( $parser ) {
42  $this->parser = $parser;
43  }
44 
48  public function newFrame() {
49  return new PPFrame_Hash( $this );
50  }
51 
56  public function newCustomFrame( $args ) {
57  return new PPCustomFrame_Hash( $this, $args );
58  }
59 
64  public function newPartNodeArray( $values ) {
65  $list = [];
66 
67  foreach ( $values as $k => $val ) {
68  $partNode = new PPNode_Hash_Tree( 'part' );
69  $nameNode = new PPNode_Hash_Tree( 'name' );
70 
71  if ( is_int( $k ) ) {
72  $nameNode->addChild( new PPNode_Hash_Attr( 'index', $k ) );
73  $partNode->addChild( $nameNode );
74  } else {
75  $nameNode->addChild( new PPNode_Hash_Text( $k ) );
76  $partNode->addChild( $nameNode );
77  $partNode->addChild( new PPNode_Hash_Text( '=' ) );
78  }
79 
80  $valueNode = new PPNode_Hash_Tree( 'value' );
81  $valueNode->addChild( new PPNode_Hash_Text( $val ) );
82  $partNode->addChild( $valueNode );
83 
84  $list[] = $partNode;
85  }
86 
87  $node = new PPNode_Hash_Array( $list );
88  return $node;
89  }
90 
114  public function preprocessToObj( $text, $flags = 0 ) {
115  $tree = $this->cacheGetTree( $text, $flags );
116  if ( $tree !== false ) {
117  return unserialize( $tree );
118  }
119 
120  $forInclusion = $flags & Parser::PTD_FOR_INCLUSION;
121 
122  $xmlishElements = $this->parser->getStripList();
123  $xmlishAllowMissingEndTag = [ 'includeonly', 'noinclude', 'onlyinclude' ];
124  $enableOnlyinclude = false;
125  if ( $forInclusion ) {
126  $ignoredTags = [ 'includeonly', '/includeonly' ];
127  $ignoredElements = [ 'noinclude' ];
128  $xmlishElements[] = 'noinclude';
129  if ( strpos( $text, '<onlyinclude>' ) !== false
130  && strpos( $text, '</onlyinclude>' ) !== false
131  ) {
132  $enableOnlyinclude = true;
133  }
134  } else {
135  $ignoredTags = [ 'noinclude', '/noinclude', 'onlyinclude', '/onlyinclude' ];
136  $ignoredElements = [ 'includeonly' ];
137  $xmlishElements[] = 'includeonly';
138  }
139  $xmlishRegex = implode( '|', array_merge( $xmlishElements, $ignoredTags ) );
140 
141  // Use "A" modifier (anchored) instead of "^", because ^ doesn't work with an offset
142  $elementsRegex = "~($xmlishRegex)(?:\s|\/>|>)|(!--)~iA";
143 
144  $stack = new PPDStack_Hash;
145 
146  $searchBase = "[{<\n";
147  // For fast reverse searches
148  $revText = strrev( $text );
149  $lengthText = strlen( $text );
150 
151  // Input pointer, starts out pointing to a pseudo-newline before the start
152  $i = 0;
153  // Current accumulator
154  $accum =& $stack->getAccum();
155  // True to find equals signs in arguments
156  $findEquals = false;
157  // True to take notice of pipe characters
158  $findPipe = false;
159  $headingIndex = 1;
160  // True if $i is inside a possible heading
161  $inHeading = false;
162  // True if there are no more greater-than (>) signs right of $i
163  $noMoreGT = false;
164  // Map of tag name => true if there are no more closing tags of given type right of $i
165  $noMoreClosingTag = [];
166  // True to ignore all input up to the next <onlyinclude>
167  $findOnlyinclude = $enableOnlyinclude;
168  // Do a line-start run without outputting an LF character
169  $fakeLineStart = true;
170 
171  while ( true ) {
172  // $this->memCheck();
173 
174  if ( $findOnlyinclude ) {
175  // Ignore all input up to the next <onlyinclude>
176  $startPos = strpos( $text, '<onlyinclude>', $i );
177  if ( $startPos === false ) {
178  // Ignored section runs to the end
179  $accum->addNodeWithText( 'ignore', substr( $text, $i ) );
180  break;
181  }
182  $tagEndPos = $startPos + strlen( '<onlyinclude>' ); // past-the-end
183  $accum->addNodeWithText( 'ignore', substr( $text, $i, $tagEndPos - $i ) );
184  $i = $tagEndPos;
185  $findOnlyinclude = false;
186  }
187 
188  if ( $fakeLineStart ) {
189  $found = 'line-start';
190  $curChar = '';
191  } else {
192  # Find next opening brace, closing brace or pipe
193  $search = $searchBase;
194  if ( $stack->top === false ) {
195  $currentClosing = '';
196  } else {
197  $currentClosing = $stack->top->close;
198  $search .= $currentClosing;
199  }
200  if ( $findPipe ) {
201  $search .= '|';
202  }
203  if ( $findEquals ) {
204  // First equals will be for the template
205  $search .= '=';
206  }
207  $rule = null;
208  # Output literal section, advance input counter
209  $literalLength = strcspn( $text, $search, $i );
210  if ( $literalLength > 0 ) {
211  $accum->addLiteral( substr( $text, $i, $literalLength ) );
212  $i += $literalLength;
213  }
214  if ( $i >= $lengthText ) {
215  if ( $currentClosing == "\n" ) {
216  // Do a past-the-end run to finish off the heading
217  $curChar = '';
218  $found = 'line-end';
219  } else {
220  # All done
221  break;
222  }
223  } else {
224  $curChar = $text[$i];
225  if ( $curChar == '|' ) {
226  $found = 'pipe';
227  } elseif ( $curChar == '=' ) {
228  $found = 'equals';
229  } elseif ( $curChar == '<' ) {
230  $found = 'angle';
231  } elseif ( $curChar == "\n" ) {
232  if ( $inHeading ) {
233  $found = 'line-end';
234  } else {
235  $found = 'line-start';
236  }
237  } elseif ( $curChar == $currentClosing ) {
238  $found = 'close';
239  } elseif ( isset( $this->rules[$curChar] ) ) {
240  $found = 'open';
241  $rule = $this->rules[$curChar];
242  } else {
243  # Some versions of PHP have a strcspn which stops on null characters
244  # Ignore and continue
245  ++$i;
246  continue;
247  }
248  }
249  }
250 
251  if ( $found == 'angle' ) {
252  $matches = false;
253  // Handle </onlyinclude>
254  if ( $enableOnlyinclude
255  && substr( $text, $i, strlen( '</onlyinclude>' ) ) == '</onlyinclude>'
256  ) {
257  $findOnlyinclude = true;
258  continue;
259  }
260 
261  // Determine element name
262  if ( !preg_match( $elementsRegex, $text, $matches, 0, $i + 1 ) ) {
263  // Element name missing or not listed
264  $accum->addLiteral( '<' );
265  ++$i;
266  continue;
267  }
268  // Handle comments
269  if ( isset( $matches[2] ) && $matches[2] == '!--' ) {
270 
271  // To avoid leaving blank lines, when a sequence of
272  // space-separated comments is both preceded and followed by
273  // a newline (ignoring spaces), then
274  // trim leading and trailing spaces and the trailing newline.
275 
276  // Find the end
277  $endPos = strpos( $text, '-->', $i + 4 );
278  if ( $endPos === false ) {
279  // Unclosed comment in input, runs to end
280  $inner = substr( $text, $i );
281  $accum->addNodeWithText( 'comment', $inner );
282  $i = $lengthText;
283  } else {
284  // Search backwards for leading whitespace
285  $wsStart = $i ? ( $i - strspn( $revText, " \t", $lengthText - $i ) ) : 0;
286 
287  // Search forwards for trailing whitespace
288  // $wsEnd will be the position of the last space (or the '>' if there's none)
289  $wsEnd = $endPos + 2 + strspn( $text, " \t", $endPos + 3 );
290 
291  // Keep looking forward as long as we're finding more
292  // comments.
293  $comments = [ [ $wsStart, $wsEnd ] ];
294  while ( substr( $text, $wsEnd + 1, 4 ) == '<!--' ) {
295  $c = strpos( $text, '-->', $wsEnd + 4 );
296  if ( $c === false ) {
297  break;
298  }
299  $c = $c + 2 + strspn( $text, " \t", $c + 3 );
300  $comments[] = [ $wsEnd + 1, $c ];
301  $wsEnd = $c;
302  }
303 
304  // Eat the line if possible
305  // TODO: This could theoretically be done if $wsStart == 0, i.e. for comments at
306  // the overall start. That's not how Sanitizer::removeHTMLcomments() did it, but
307  // it's a possible beneficial b/c break.
308  if ( $wsStart > 0 && substr( $text, $wsStart - 1, 1 ) == "\n"
309  && substr( $text, $wsEnd + 1, 1 ) == "\n"
310  ) {
311  // Remove leading whitespace from the end of the accumulator
312  // Sanity check first though
313  $wsLength = $i - $wsStart;
314  if ( $wsLength > 0
315  && $accum->lastNode instanceof PPNode_Hash_Text
316  && strspn( $accum->lastNode->value, " \t", -$wsLength ) === $wsLength
317  ) {
318  $accum->lastNode->value = substr( $accum->lastNode->value, 0, -$wsLength );
319  }
320 
321  // Dump all but the last comment to the accumulator
322  foreach ( $comments as $j => $com ) {
323  $startPos = $com[0];
324  $endPos = $com[1] + 1;
325  if ( $j == ( count( $comments ) - 1 ) ) {
326  break;
327  }
328  $inner = substr( $text, $startPos, $endPos - $startPos );
329  $accum->addNodeWithText( 'comment', $inner );
330  }
331 
332  // Do a line-start run next time to look for headings after the comment
333  $fakeLineStart = true;
334  } else {
335  // No line to eat, just take the comment itself
336  $startPos = $i;
337  $endPos += 2;
338  }
339 
340  if ( $stack->top ) {
341  $part = $stack->top->getCurrentPart();
342  if ( !( isset( $part->commentEnd ) && $part->commentEnd == $wsStart - 1 ) ) {
343  $part->visualEnd = $wsStart;
344  }
345  // Else comments abutting, no change in visual end
346  $part->commentEnd = $endPos;
347  }
348  $i = $endPos + 1;
349  $inner = substr( $text, $startPos, $endPos - $startPos + 1 );
350  $accum->addNodeWithText( 'comment', $inner );
351  }
352  continue;
353  }
354  $name = $matches[1];
355  $lowerName = strtolower( $name );
356  $attrStart = $i + strlen( $name ) + 1;
357 
358  // Find end of tag
359  $tagEndPos = $noMoreGT ? false : strpos( $text, '>', $attrStart );
360  if ( $tagEndPos === false ) {
361  // Infinite backtrack
362  // Disable tag search to prevent worst-case O(N^2) performance
363  $noMoreGT = true;
364  $accum->addLiteral( '<' );
365  ++$i;
366  continue;
367  }
368 
369  // Handle ignored tags
370  if ( in_array( $lowerName, $ignoredTags ) ) {
371  $accum->addNodeWithText( 'ignore', substr( $text, $i, $tagEndPos - $i + 1 ) );
372  $i = $tagEndPos + 1;
373  continue;
374  }
375 
376  $tagStartPos = $i;
377  if ( $text[$tagEndPos - 1] == '/' ) {
378  // Short end tag
379  $attrEnd = $tagEndPos - 1;
380  $inner = null;
381  $i = $tagEndPos + 1;
382  $close = null;
383  } else {
384  $attrEnd = $tagEndPos;
385  // Find closing tag
386  if (
387  !isset( $noMoreClosingTag[$name] ) &&
388  preg_match( "/<\/" . preg_quote( $name, '/' ) . "\s*>/i",
389  $text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 )
390  ) {
391  $inner = substr( $text, $tagEndPos + 1, $matches[0][1] - $tagEndPos - 1 );
392  $i = $matches[0][1] + strlen( $matches[0][0] );
393  $close = $matches[0][0];
394  } else {
395  // No end tag
396  if ( in_array( $name, $xmlishAllowMissingEndTag ) ) {
397  // Let it run out to the end of the text.
398  $inner = substr( $text, $tagEndPos + 1 );
399  $i = $lengthText;
400  $close = null;
401  } else {
402  // Don't match the tag, treat opening tag as literal and resume parsing.
403  $i = $tagEndPos + 1;
404  $accum->addLiteral( substr( $text, $tagStartPos, $tagEndPos + 1 - $tagStartPos ) );
405  // Cache results, otherwise we have O(N^2) performance for input like <foo><foo><foo>...
406  $noMoreClosingTag[$name] = true;
407  continue;
408  }
409  }
410  }
411  // <includeonly> and <noinclude> just become <ignore> tags
412  if ( in_array( $lowerName, $ignoredElements ) ) {
413  $accum->addNodeWithText( 'ignore', substr( $text, $tagStartPos, $i - $tagStartPos ) );
414  continue;
415  }
416 
417  if ( $attrEnd <= $attrStart ) {
418  $attr = '';
419  } else {
420  // Note that the attr element contains the whitespace between name and attribute,
421  // this is necessary for precise reconstruction during pre-save transform.
422  $attr = substr( $text, $attrStart, $attrEnd - $attrStart );
423  }
424 
425  $extNode = new PPNode_Hash_Tree( 'ext' );
426  $extNode->addChild( PPNode_Hash_Tree::newWithText( 'name', $name ) );
427  $extNode->addChild( PPNode_Hash_Tree::newWithText( 'attr', $attr ) );
428  if ( $inner !== null ) {
429  $extNode->addChild( PPNode_Hash_Tree::newWithText( 'inner', $inner ) );
430  }
431  if ( $close !== null ) {
432  $extNode->addChild( PPNode_Hash_Tree::newWithText( 'close', $close ) );
433  }
434  $accum->addNode( $extNode );
435  } elseif ( $found == 'line-start' ) {
436  // Is this the start of a heading?
437  // Line break belongs before the heading element in any case
438  if ( $fakeLineStart ) {
439  $fakeLineStart = false;
440  } else {
441  $accum->addLiteral( $curChar );
442  $i++;
443  }
444 
445  $count = strspn( $text, '=', $i, 6 );
446  if ( $count == 1 && $findEquals ) {
447  // DWIM: This looks kind of like a name/value separator.
448  // Let's let the equals handler have it and break the potential
449  // heading. This is heuristic, but AFAICT the methods for
450  // completely correct disambiguation are very complex.
451  } elseif ( $count > 0 ) {
452  $piece = [
453  'open' => "\n",
454  'close' => "\n",
455  'parts' => [ new PPDPart_Hash( str_repeat( '=', $count ) ) ],
456  'startPos' => $i,
457  'count' => $count ];
458  $stack->push( $piece );
459  $accum =& $stack->getAccum();
460  extract( $stack->getFlags() );
461  $i += $count;
462  }
463  } elseif ( $found == 'line-end' ) {
464  $piece = $stack->top;
465  // A heading must be open, otherwise \n wouldn't have been in the search list
466  assert( '$piece->open == "\n"' );
467  $part = $piece->getCurrentPart();
468  // Search back through the input to see if it has a proper close.
469  // Do this using the reversed string since the other solutions
470  // (end anchor, etc.) are inefficient.
471  $wsLength = strspn( $revText, " \t", $lengthText - $i );
472  $searchStart = $i - $wsLength;
473  if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) {
474  // Comment found at line end
475  // Search for equals signs before the comment
476  $searchStart = $part->visualEnd;
477  $searchStart -= strspn( $revText, " \t", $lengthText - $searchStart );
478  }
479  $count = $piece->count;
480  $equalsLength = strspn( $revText, '=', $lengthText - $searchStart );
481  if ( $equalsLength > 0 ) {
482  if ( $searchStart - $equalsLength == $piece->startPos ) {
483  // This is just a single string of equals signs on its own line
484  // Replicate the doHeadings behavior /={count}(.+)={count}/
485  // First find out how many equals signs there really are (don't stop at 6)
486  $count = $equalsLength;
487  if ( $count < 3 ) {
488  $count = 0;
489  } else {
490  $count = min( 6, intval( ( $count - 1 ) / 2 ) );
491  }
492  } else {
493  $count = min( $equalsLength, $count );
494  }
495  if ( $count > 0 ) {
496  // Normal match, output <h>
497  $element = new PPNode_Hash_Tree( 'possible-h' );
498  $element->addChild( new PPNode_Hash_Attr( 'level', $count ) );
499  $element->addChild( new PPNode_Hash_Attr( 'i', $headingIndex++ ) );
500  $element->lastChild->nextSibling = $accum->firstNode;
501  $element->lastChild = $accum->lastNode;
502  } else {
503  // Single equals sign on its own line, count=0
504  $element = $accum;
505  }
506  } else {
507  // No match, no <h>, just pass down the inner text
508  $element = $accum;
509  }
510  // Unwind the stack
511  $stack->pop();
512  $accum =& $stack->getAccum();
513  extract( $stack->getFlags() );
514 
515  // Append the result to the enclosing accumulator
516  if ( $element instanceof PPNode ) {
517  $accum->addNode( $element );
518  } else {
519  $accum->addAccum( $element );
520  }
521  // Note that we do NOT increment the input pointer.
522  // This is because the closing linebreak could be the opening linebreak of
523  // another heading. Infinite loops are avoided because the next iteration MUST
524  // hit the heading open case above, which unconditionally increments the
525  // input pointer.
526  } elseif ( $found == 'open' ) {
527  # count opening brace characters
528  $count = strspn( $text, $curChar, $i );
529 
530  # we need to add to stack only if opening brace count is enough for one of the rules
531  if ( $count >= $rule['min'] ) {
532  # Add it to the stack
533  $piece = [
534  'open' => $curChar,
535  'close' => $rule['end'],
536  'count' => $count,
537  'lineStart' => ( $i > 0 && $text[$i - 1] == "\n" ),
538  ];
539 
540  $stack->push( $piece );
541  $accum =& $stack->getAccum();
542  extract( $stack->getFlags() );
543  } else {
544  # Add literal brace(s)
545  $accum->addLiteral( str_repeat( $curChar, $count ) );
546  }
547  $i += $count;
548  } elseif ( $found == 'close' ) {
549  $piece = $stack->top;
550  # lets check if there are enough characters for closing brace
551  $maxCount = $piece->count;
552  $count = strspn( $text, $curChar, $i, $maxCount );
553 
554  # check for maximum matching characters (if there are 5 closing
555  # characters, we will probably need only 3 - depending on the rules)
556  $rule = $this->rules[$piece->open];
557  if ( $count > $rule['max'] ) {
558  # The specified maximum exists in the callback array, unless the caller
559  # has made an error
560  $matchingCount = $rule['max'];
561  } else {
562  # Count is less than the maximum
563  # Skip any gaps in the callback array to find the true largest match
564  # Need to use array_key_exists not isset because the callback can be null
565  $matchingCount = $count;
566  while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $rule['names'] ) ) {
567  --$matchingCount;
568  }
569  }
570 
571  if ( $matchingCount <= 0 ) {
572  # No matching element found in callback array
573  # Output a literal closing brace and continue
574  $accum->addLiteral( str_repeat( $curChar, $count ) );
575  $i += $count;
576  continue;
577  }
578  $name = $rule['names'][$matchingCount];
579  if ( $name === null ) {
580  // No element, just literal text
581  $element = $piece->breakSyntax( $matchingCount );
582  $element->addLiteral( str_repeat( $rule['end'], $matchingCount ) );
583  } else {
584  # Create XML element
585  # Note: $parts is already XML, does not need to be encoded further
586  $parts = $piece->parts;
587  $titleAccum = $parts[0]->out;
588  unset( $parts[0] );
589 
590  $element = new PPNode_Hash_Tree( $name );
591 
592  # The invocation is at the start of the line if lineStart is set in
593  # the stack, and all opening brackets are used up.
594  if ( $maxCount == $matchingCount && !empty( $piece->lineStart ) ) {
595  $element->addChild( new PPNode_Hash_Attr( 'lineStart', 1 ) );
596  }
597  $titleNode = new PPNode_Hash_Tree( 'title' );
598  $titleNode->firstChild = $titleAccum->firstNode;
599  $titleNode->lastChild = $titleAccum->lastNode;
600  $element->addChild( $titleNode );
601  $argIndex = 1;
602  foreach ( $parts as $part ) {
603  if ( isset( $part->eqpos ) ) {
604  // Find equals
605  $lastNode = false;
606  for ( $node = $part->out->firstNode; $node; $node = $node->nextSibling ) {
607  if ( $node === $part->eqpos ) {
608  break;
609  }
610  $lastNode = $node;
611  }
612  if ( !$node ) {
613  // if ( $cacheable ) { ... }
614  throw new MWException( __METHOD__ . ': eqpos not found' );
615  }
616  if ( $node->name !== 'equals' ) {
617  // if ( $cacheable ) { ... }
618  throw new MWException( __METHOD__ . ': eqpos is not equals' );
619  }
620  $equalsNode = $node;
621 
622  // Construct name node
623  $nameNode = new PPNode_Hash_Tree( 'name' );
624  if ( $lastNode !== false ) {
625  $lastNode->nextSibling = false;
626  $nameNode->firstChild = $part->out->firstNode;
627  $nameNode->lastChild = $lastNode;
628  }
629 
630  // Construct value node
631  $valueNode = new PPNode_Hash_Tree( 'value' );
632  if ( $equalsNode->nextSibling !== false ) {
633  $valueNode->firstChild = $equalsNode->nextSibling;
634  $valueNode->lastChild = $part->out->lastNode;
635  }
636  $partNode = new PPNode_Hash_Tree( 'part' );
637  $partNode->addChild( $nameNode );
638  $partNode->addChild( $equalsNode->firstChild );
639  $partNode->addChild( $valueNode );
640  $element->addChild( $partNode );
641  } else {
642  $partNode = new PPNode_Hash_Tree( 'part' );
643  $nameNode = new PPNode_Hash_Tree( 'name' );
644  $nameNode->addChild( new PPNode_Hash_Attr( 'index', $argIndex++ ) );
645  $valueNode = new PPNode_Hash_Tree( 'value' );
646  $valueNode->firstChild = $part->out->firstNode;
647  $valueNode->lastChild = $part->out->lastNode;
648  $partNode->addChild( $nameNode );
649  $partNode->addChild( $valueNode );
650  $element->addChild( $partNode );
651  }
652  }
653  }
654 
655  # Advance input pointer
656  $i += $matchingCount;
657 
658  # Unwind the stack
659  $stack->pop();
660  $accum =& $stack->getAccum();
661 
662  # Re-add the old stack element if it still has unmatched opening characters remaining
663  if ( $matchingCount < $piece->count ) {
664  $piece->parts = [ new PPDPart_Hash ];
665  $piece->count -= $matchingCount;
666  # do we still qualify for any callback with remaining count?
667  $min = $this->rules[$piece->open]['min'];
668  if ( $piece->count >= $min ) {
669  $stack->push( $piece );
670  $accum =& $stack->getAccum();
671  } else {
672  $accum->addLiteral( str_repeat( $piece->open, $piece->count ) );
673  }
674  }
675 
676  extract( $stack->getFlags() );
677 
678  # Add XML element to the enclosing accumulator
679  if ( $element instanceof PPNode ) {
680  $accum->addNode( $element );
681  } else {
682  $accum->addAccum( $element );
683  }
684  } elseif ( $found == 'pipe' ) {
685  $findEquals = true; // shortcut for getFlags()
686  $stack->addPart();
687  $accum =& $stack->getAccum();
688  ++$i;
689  } elseif ( $found == 'equals' ) {
690  $findEquals = false; // shortcut for getFlags()
691  $accum->addNodeWithText( 'equals', '=' );
692  $stack->getCurrentPart()->eqpos = $accum->lastNode;
693  ++$i;
694  }
695  }
696 
697  # Output any remaining unclosed brackets
698  foreach ( $stack->stack as $piece ) {
699  $stack->rootAccum->addAccum( $piece->breakSyntax() );
700  }
701 
702  # Enable top-level headings
703  for ( $node = $stack->rootAccum->firstNode; $node; $node = $node->nextSibling ) {
704  if ( isset( $node->name ) && $node->name === 'possible-h' ) {
705  $node->name = 'h';
706  }
707  }
708 
709  $rootNode = new PPNode_Hash_Tree( 'root' );
710  $rootNode->firstChild = $stack->rootAccum->firstNode;
711  $rootNode->lastChild = $stack->rootAccum->lastNode;
712 
713  // Cache
714  $this->cacheSetTree( $text, $flags, serialize( $rootNode ) );
715 
716  return $rootNode;
717  }
718 }
719 
724 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
725 class PPDStack_Hash extends PPDStack {
726  // @codingStandardsIgnoreEnd
727 
728  public function __construct() {
729  $this->elementClass = 'PPDStackElement_Hash';
730  parent::__construct();
731  $this->rootAccum = new PPDAccum_Hash;
732  }
733 }
734 
738 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
740  // @codingStandardsIgnoreEnd
741 
742  public function __construct( $data = [] ) {
743  $this->partClass = 'PPDPart_Hash';
744  parent::__construct( $data );
745  }
746 
753  public function breakSyntax( $openingCount = false ) {
754  if ( $this->open == "\n" ) {
755  $accum = $this->parts[0]->out;
756  } else {
757  if ( $openingCount === false ) {
758  $openingCount = $this->count;
759  }
760  $accum = new PPDAccum_Hash;
761  $accum->addLiteral( str_repeat( $this->open, $openingCount ) );
762  $first = true;
763  foreach ( $this->parts as $part ) {
764  if ( $first ) {
765  $first = false;
766  } else {
767  $accum->addLiteral( '|' );
768  }
769  $accum->addAccum( $part->out );
770  }
771  }
772  return $accum;
773  }
774 }
775 
779 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
780 class PPDPart_Hash extends PPDPart {
781  // @codingStandardsIgnoreEnd
782 
783  public function __construct( $out = '' ) {
784  $accum = new PPDAccum_Hash;
785  if ( $out !== '' ) {
786  $accum->addLiteral( $out );
787  }
788  parent::__construct( $accum );
789  }
790 }
791 
795 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
797  // @codingStandardsIgnoreEnd
798 
800 
801  public function __construct() {
802  $this->firstNode = $this->lastNode = false;
803  }
804 
809  public function addLiteral( $s ) {
810  if ( $this->lastNode === false ) {
811  $this->firstNode = $this->lastNode = new PPNode_Hash_Text( $s );
812  } elseif ( $this->lastNode instanceof PPNode_Hash_Text ) {
813  $this->lastNode->value .= $s;
814  } else {
815  $this->lastNode->nextSibling = new PPNode_Hash_Text( $s );
816  $this->lastNode = $this->lastNode->nextSibling;
817  }
818  }
819 
824  public function addNode( PPNode $node ) {
825  if ( $this->lastNode === false ) {
826  $this->firstNode = $this->lastNode = $node;
827  } else {
828  $this->lastNode->nextSibling = $node;
829  $this->lastNode = $node;
830  }
831  }
832 
838  public function addNodeWithText( $name, $value ) {
840  $this->addNode( $node );
841  }
842 
849  public function addAccum( $accum ) {
850  if ( $accum->lastNode === false ) {
851  // nothing to add
852  } elseif ( $this->lastNode === false ) {
853  $this->firstNode = $accum->firstNode;
854  $this->lastNode = $accum->lastNode;
855  } else {
856  $this->lastNode->nextSibling = $accum->firstNode;
857  $this->lastNode = $accum->lastNode;
858  }
859  }
860 }
861 
866 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
867 class PPFrame_Hash implements PPFrame {
868  // @codingStandardsIgnoreEnd
869 
873  public $parser;
874 
879 
883  public $title;
884  public $titleCache;
885 
891 
896  public $depth;
897 
898  private $volatile = false;
899  private $ttl = null;
900 
905 
910  public function __construct( $preprocessor ) {
911  $this->preprocessor = $preprocessor;
912  $this->parser = $preprocessor->parser;
913  $this->title = $this->parser->mTitle;
914  $this->titleCache = [ $this->title ? $this->title->getPrefixedDBkey() : false ];
915  $this->loopCheckHash = [];
916  $this->depth = 0;
917  $this->childExpansionCache = [];
918  }
919 
930  public function newChild( $args = false, $title = false, $indexOffset = 0 ) {
931  $namedArgs = [];
932  $numberedArgs = [];
933  if ( $title === false ) {
935  }
936  if ( $args !== false ) {
937  if ( $args instanceof PPNode_Hash_Array ) {
938  $args = $args->value;
939  } elseif ( !is_array( $args ) ) {
940  throw new MWException( __METHOD__ . ': $args must be array or PPNode_Hash_Array' );
941  }
942  foreach ( $args as $arg ) {
943  $bits = $arg->splitArg();
944  if ( $bits['index'] !== '' ) {
945  // Numbered parameter
946  $index = $bits['index'] - $indexOffset;
947  if ( isset( $namedArgs[$index] ) || isset( $numberedArgs[$index] ) ) {
948  $this->parser->getOutput()->addWarning( wfMessage( 'duplicate-args-warning',
949  wfEscapeWikiText( $this->title ),
951  wfEscapeWikiText( $index ) )->text() );
952  $this->parser->addTrackingCategory( 'duplicate-args-category' );
953  }
954  $numberedArgs[$index] = $bits['value'];
955  unset( $namedArgs[$index] );
956  } else {
957  // Named parameter
958  $name = trim( $this->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
959  if ( isset( $namedArgs[$name] ) || isset( $numberedArgs[$name] ) ) {
960  $this->parser->getOutput()->addWarning( wfMessage( 'duplicate-args-warning',
961  wfEscapeWikiText( $this->title ),
963  wfEscapeWikiText( $name ) )->text() );
964  $this->parser->addTrackingCategory( 'duplicate-args-category' );
965  }
966  $namedArgs[$name] = $bits['value'];
967  unset( $numberedArgs[$name] );
968  }
969  }
970  }
971  return new PPTemplateFrame_Hash( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title );
972  }
973 
981  public function cachedExpand( $key, $root, $flags = 0 ) {
982  // we don't have a parent, so we don't have a cache
983  return $this->expand( $root, $flags );
984  }
985 
992  public function expand( $root, $flags = 0 ) {
993  static $expansionDepth = 0;
994  if ( is_string( $root ) ) {
995  return $root;
996  }
997 
998  if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->getMaxPPNodeCount() ) {
999  $this->parser->limitationWarn( 'node-count-exceeded',
1000  $this->parser->mPPNodeCount,
1001  $this->parser->mOptions->getMaxPPNodeCount()
1002  );
1003  return '<span class="error">Node-count limit exceeded</span>';
1004  }
1005  if ( $expansionDepth > $this->parser->mOptions->getMaxPPExpandDepth() ) {
1006  $this->parser->limitationWarn( 'expansion-depth-exceeded',
1007  $expansionDepth,
1008  $this->parser->mOptions->getMaxPPExpandDepth()
1009  );
1010  return '<span class="error">Expansion depth limit exceeded</span>';
1011  }
1012  ++$expansionDepth;
1013  if ( $expansionDepth > $this->parser->mHighestExpansionDepth ) {
1014  $this->parser->mHighestExpansionDepth = $expansionDepth;
1015  }
1016 
1017  $outStack = [ '', '' ];
1018  $iteratorStack = [ false, $root ];
1019  $indexStack = [ 0, 0 ];
1020 
1021  while ( count( $iteratorStack ) > 1 ) {
1022  $level = count( $outStack ) - 1;
1023  $iteratorNode =& $iteratorStack[$level];
1024  $out =& $outStack[$level];
1025  $index =& $indexStack[$level];
1026 
1027  if ( is_array( $iteratorNode ) ) {
1028  if ( $index >= count( $iteratorNode ) ) {
1029  // All done with this iterator
1030  $iteratorStack[$level] = false;
1031  $contextNode = false;
1032  } else {
1033  $contextNode = $iteratorNode[$index];
1034  $index++;
1035  }
1036  } elseif ( $iteratorNode instanceof PPNode_Hash_Array ) {
1037  if ( $index >= $iteratorNode->getLength() ) {
1038  // All done with this iterator
1039  $iteratorStack[$level] = false;
1040  $contextNode = false;
1041  } else {
1042  $contextNode = $iteratorNode->item( $index );
1043  $index++;
1044  }
1045  } else {
1046  // Copy to $contextNode and then delete from iterator stack,
1047  // because this is not an iterator but we do have to execute it once
1048  $contextNode = $iteratorStack[$level];
1049  $iteratorStack[$level] = false;
1050  }
1051 
1052  $newIterator = false;
1053 
1054  if ( $contextNode === false ) {
1055  // nothing to do
1056  } elseif ( is_string( $contextNode ) ) {
1057  $out .= $contextNode;
1058  } elseif ( is_array( $contextNode ) || $contextNode instanceof PPNode_Hash_Array ) {
1059  $newIterator = $contextNode;
1060  } elseif ( $contextNode instanceof PPNode_Hash_Attr ) {
1061  // No output
1062  } elseif ( $contextNode instanceof PPNode_Hash_Text ) {
1063  $out .= $contextNode->value;
1064  } elseif ( $contextNode instanceof PPNode_Hash_Tree ) {
1065  if ( $contextNode->name == 'template' ) {
1066  # Double-brace expansion
1067  $bits = $contextNode->splitTemplate();
1068  if ( $flags & PPFrame::NO_TEMPLATES ) {
1069  $newIterator = $this->virtualBracketedImplode(
1070  '{{', '|', '}}',
1071  $bits['title'],
1072  $bits['parts']
1073  );
1074  } else {
1075  $ret = $this->parser->braceSubstitution( $bits, $this );
1076  if ( isset( $ret['object'] ) ) {
1077  $newIterator = $ret['object'];
1078  } else {
1079  $out .= $ret['text'];
1080  }
1081  }
1082  } elseif ( $contextNode->name == 'tplarg' ) {
1083  # Triple-brace expansion
1084  $bits = $contextNode->splitTemplate();
1085  if ( $flags & PPFrame::NO_ARGS ) {
1086  $newIterator = $this->virtualBracketedImplode(
1087  '{{{', '|', '}}}',
1088  $bits['title'],
1089  $bits['parts']
1090  );
1091  } else {
1092  $ret = $this->parser->argSubstitution( $bits, $this );
1093  if ( isset( $ret['object'] ) ) {
1094  $newIterator = $ret['object'];
1095  } else {
1096  $out .= $ret['text'];
1097  }
1098  }
1099  } elseif ( $contextNode->name == 'comment' ) {
1100  # HTML-style comment
1101  # Remove it in HTML, pre+remove and STRIP_COMMENTS modes
1102  # Not in RECOVER_COMMENTS mode (msgnw) though.
1103  if ( ( $this->parser->ot['html']
1104  || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() )
1106  ) && !( $flags & PPFrame::RECOVER_COMMENTS )
1107  ) {
1108  $out .= '';
1109  } elseif ( $this->parser->ot['wiki'] && !( $flags & PPFrame::RECOVER_COMMENTS ) ) {
1110  # Add a strip marker in PST mode so that pstPass2() can
1111  # run some old-fashioned regexes on the result.
1112  # Not in RECOVER_COMMENTS mode (extractSections) though.
1113  $out .= $this->parser->insertStripItem( $contextNode->firstChild->value );
1114  } else {
1115  # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
1116  $out .= $contextNode->firstChild->value;
1117  }
1118  } elseif ( $contextNode->name == 'ignore' ) {
1119  # Output suppression used by <includeonly> etc.
1120  # OT_WIKI will only respect <ignore> in substed templates.
1121  # The other output types respect it unless NO_IGNORE is set.
1122  # extractSections() sets NO_IGNORE and so never respects it.
1123  if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] )
1124  || ( $flags & PPFrame::NO_IGNORE )
1125  ) {
1126  $out .= $contextNode->firstChild->value;
1127  } else {
1128  // $out .= '';
1129  }
1130  } elseif ( $contextNode->name == 'ext' ) {
1131  # Extension tag
1132  $bits = $contextNode->splitExt() + [ 'attr' => null, 'inner' => null, 'close' => null ];
1133  if ( $flags & PPFrame::NO_TAGS ) {
1134  $s = '<' . $bits['name']->firstChild->value;
1135  if ( $bits['attr'] ) {
1136  $s .= $bits['attr']->firstChild->value;
1137  }
1138  if ( $bits['inner'] ) {
1139  $s .= '>' . $bits['inner']->firstChild->value;
1140  if ( $bits['close'] ) {
1141  $s .= $bits['close']->firstChild->value;
1142  }
1143  } else {
1144  $s .= '/>';
1145  }
1146  $out .= $s;
1147  } else {
1148  $out .= $this->parser->extensionSubstitution( $bits, $this );
1149  }
1150  } elseif ( $contextNode->name == 'h' ) {
1151  # Heading
1152  if ( $this->parser->ot['html'] ) {
1153  # Expand immediately and insert heading index marker
1154  $s = '';
1155  for ( $node = $contextNode->firstChild; $node; $node = $node->nextSibling ) {
1156  $s .= $this->expand( $node, $flags );
1157  }
1158 
1159  $bits = $contextNode->splitHeading();
1160  $titleText = $this->title->getPrefixedDBkey();
1161  $this->parser->mHeadings[] = [ $titleText, $bits['i'] ];
1162  $serial = count( $this->parser->mHeadings ) - 1;
1163  $marker = Parser::MARKER_PREFIX . "-h-$serial-" . Parser::MARKER_SUFFIX;
1164  $s = substr( $s, 0, $bits['level'] ) . $marker . substr( $s, $bits['level'] );
1165  $this->parser->mStripState->addGeneral( $marker, '' );
1166  $out .= $s;
1167  } else {
1168  # Expand in virtual stack
1169  $newIterator = $contextNode->getChildren();
1170  }
1171  } else {
1172  # Generic recursive expansion
1173  $newIterator = $contextNode->getChildren();
1174  }
1175  } else {
1176  throw new MWException( __METHOD__ . ': Invalid parameter type' );
1177  }
1178 
1179  if ( $newIterator !== false ) {
1180  $outStack[] = '';
1181  $iteratorStack[] = $newIterator;
1182  $indexStack[] = 0;
1183  } elseif ( $iteratorStack[$level] === false ) {
1184  // Return accumulated value to parent
1185  // With tail recursion
1186  while ( $iteratorStack[$level] === false && $level > 0 ) {
1187  $outStack[$level - 1] .= $out;
1188  array_pop( $outStack );
1189  array_pop( $iteratorStack );
1190  array_pop( $indexStack );
1191  $level--;
1192  }
1193  }
1194  }
1195  --$expansionDepth;
1196  return $outStack[0];
1197  }
1198 
1205  public function implodeWithFlags( $sep, $flags /*, ... */ ) {
1206  $args = array_slice( func_get_args(), 2 );
1207 
1208  $first = true;
1209  $s = '';
1210  foreach ( $args as $root ) {
1211  if ( $root instanceof PPNode_Hash_Array ) {
1212  $root = $root->value;
1213  }
1214  if ( !is_array( $root ) ) {
1215  $root = [ $root ];
1216  }
1217  foreach ( $root as $node ) {
1218  if ( $first ) {
1219  $first = false;
1220  } else {
1221  $s .= $sep;
1222  }
1223  $s .= $this->expand( $node, $flags );
1224  }
1225  }
1226  return $s;
1227  }
1228 
1236  public function implode( $sep /*, ... */ ) {
1237  $args = array_slice( func_get_args(), 1 );
1238 
1239  $first = true;
1240  $s = '';
1241  foreach ( $args as $root ) {
1242  if ( $root instanceof PPNode_Hash_Array ) {
1243  $root = $root->value;
1244  }
1245  if ( !is_array( $root ) ) {
1246  $root = [ $root ];
1247  }
1248  foreach ( $root as $node ) {
1249  if ( $first ) {
1250  $first = false;
1251  } else {
1252  $s .= $sep;
1253  }
1254  $s .= $this->expand( $node );
1255  }
1256  }
1257  return $s;
1258  }
1259 
1268  public function virtualImplode( $sep /*, ... */ ) {
1269  $args = array_slice( func_get_args(), 1 );
1270  $out = [];
1271  $first = true;
1272 
1273  foreach ( $args as $root ) {
1274  if ( $root instanceof PPNode_Hash_Array ) {
1275  $root = $root->value;
1276  }
1277  if ( !is_array( $root ) ) {
1278  $root = [ $root ];
1279  }
1280  foreach ( $root as $node ) {
1281  if ( $first ) {
1282  $first = false;
1283  } else {
1284  $out[] = $sep;
1285  }
1286  $out[] = $node;
1287  }
1288  }
1289  return new PPNode_Hash_Array( $out );
1290  }
1291 
1301  public function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) {
1302  $args = array_slice( func_get_args(), 3 );
1303  $out = [ $start ];
1304  $first = true;
1305 
1306  foreach ( $args as $root ) {
1307  if ( $root instanceof PPNode_Hash_Array ) {
1308  $root = $root->value;
1309  }
1310  if ( !is_array( $root ) ) {
1311  $root = [ $root ];
1312  }
1313  foreach ( $root as $node ) {
1314  if ( $first ) {
1315  $first = false;
1316  } else {
1317  $out[] = $sep;
1318  }
1319  $out[] = $node;
1320  }
1321  }
1322  $out[] = $end;
1323  return new PPNode_Hash_Array( $out );
1324  }
1325 
1326  public function __toString() {
1327  return 'frame{}';
1328  }
1329 
1334  public function getPDBK( $level = false ) {
1335  if ( $level === false ) {
1336  return $this->title->getPrefixedDBkey();
1337  } else {
1338  return isset( $this->titleCache[$level] ) ? $this->titleCache[$level] : false;
1339  }
1340  }
1341 
1345  public function getArguments() {
1346  return [];
1347  }
1348 
1352  public function getNumberedArguments() {
1353  return [];
1354  }
1355 
1359  public function getNamedArguments() {
1360  return [];
1361  }
1362 
1368  public function isEmpty() {
1369  return true;
1370  }
1371 
1376  public function getArgument( $name ) {
1377  return false;
1378  }
1379 
1387  public function loopCheck( $title ) {
1388  return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] );
1389  }
1390 
1396  public function isTemplate() {
1397  return false;
1398  }
1399 
1405  public function getTitle() {
1406  return $this->title;
1407  }
1408 
1414  public function setVolatile( $flag = true ) {
1415  $this->volatile = $flag;
1416  }
1417 
1423  public function isVolatile() {
1424  return $this->volatile;
1425  }
1426 
1432  public function setTTL( $ttl ) {
1433  if ( $ttl !== null && ( $this->ttl === null || $ttl < $this->ttl ) ) {
1434  $this->ttl = $ttl;
1435  }
1436  }
1437 
1443  public function getTTL() {
1444  return $this->ttl;
1445  }
1446 }
1447 
1452 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
1454  // @codingStandardsIgnoreEnd
1455 
1458 
1466  public function __construct( $preprocessor, $parent = false, $numberedArgs = [],
1467  $namedArgs = [], $title = false
1468  ) {
1469  parent::__construct( $preprocessor );
1470 
1471  $this->parent = $parent;
1472  $this->numberedArgs = $numberedArgs;
1473  $this->namedArgs = $namedArgs;
1474  $this->title = $title;
1475  $pdbk = $title ? $title->getPrefixedDBkey() : false;
1476  $this->titleCache = $parent->titleCache;
1477  $this->titleCache[] = $pdbk;
1478  $this->loopCheckHash = /*clone*/ $parent->loopCheckHash;
1479  if ( $pdbk !== false ) {
1480  $this->loopCheckHash[$pdbk] = true;
1481  }
1482  $this->depth = $parent->depth + 1;
1483  $this->numberedExpansionCache = $this->namedExpansionCache = [];
1484  }
1485 
1486  public function __toString() {
1487  $s = 'tplframe{';
1488  $first = true;
1489  $args = $this->numberedArgs + $this->namedArgs;
1490  foreach ( $args as $name => $value ) {
1491  if ( $first ) {
1492  $first = false;
1493  } else {
1494  $s .= ', ';
1495  }
1496  $s .= "\"$name\":\"" .
1497  str_replace( '"', '\\"', $value->__toString() ) . '"';
1498  }
1499  $s .= '}';
1500  return $s;
1501  }
1502 
1510  public function cachedExpand( $key, $root, $flags = 0 ) {
1511  if ( isset( $this->parent->childExpansionCache[$key] ) ) {
1512  return $this->parent->childExpansionCache[$key];
1513  }
1514  $retval = $this->expand( $root, $flags );
1515  if ( !$this->isVolatile() ) {
1516  $this->parent->childExpansionCache[$key] = $retval;
1517  }
1518  return $retval;
1519  }
1520 
1526  public function isEmpty() {
1527  return !count( $this->numberedArgs ) && !count( $this->namedArgs );
1528  }
1529 
1533  public function getArguments() {
1534  $arguments = [];
1535  foreach ( array_merge(
1536  array_keys( $this->numberedArgs ),
1537  array_keys( $this->namedArgs ) ) as $key ) {
1538  $arguments[$key] = $this->getArgument( $key );
1539  }
1540  return $arguments;
1541  }
1542 
1546  public function getNumberedArguments() {
1547  $arguments = [];
1548  foreach ( array_keys( $this->numberedArgs ) as $key ) {
1549  $arguments[$key] = $this->getArgument( $key );
1550  }
1551  return $arguments;
1552  }
1553 
1557  public function getNamedArguments() {
1558  $arguments = [];
1559  foreach ( array_keys( $this->namedArgs ) as $key ) {
1560  $arguments[$key] = $this->getArgument( $key );
1561  }
1562  return $arguments;
1563  }
1564 
1569  public function getNumberedArgument( $index ) {
1570  if ( !isset( $this->numberedArgs[$index] ) ) {
1571  return false;
1572  }
1573  if ( !isset( $this->numberedExpansionCache[$index] ) ) {
1574  # No trimming for unnamed arguments
1575  $this->numberedExpansionCache[$index] = $this->parent->expand(
1576  $this->numberedArgs[$index],
1578  );
1579  }
1580  return $this->numberedExpansionCache[$index];
1581  }
1582 
1587  public function getNamedArgument( $name ) {
1588  if ( !isset( $this->namedArgs[$name] ) ) {
1589  return false;
1590  }
1591  if ( !isset( $this->namedExpansionCache[$name] ) ) {
1592  # Trim named arguments post-expand, for backwards compatibility
1593  $this->namedExpansionCache[$name] = trim(
1594  $this->parent->expand( $this->namedArgs[$name], PPFrame::STRIP_COMMENTS ) );
1595  }
1596  return $this->namedExpansionCache[$name];
1597  }
1598 
1603  public function getArgument( $name ) {
1604  $text = $this->getNumberedArgument( $name );
1605  if ( $text === false ) {
1606  $text = $this->getNamedArgument( $name );
1607  }
1608  return $text;
1609  }
1610 
1616  public function isTemplate() {
1617  return true;
1618  }
1619 
1620  public function setVolatile( $flag = true ) {
1621  parent::setVolatile( $flag );
1622  $this->parent->setVolatile( $flag );
1623  }
1624 
1625  public function setTTL( $ttl ) {
1626  parent::setTTL( $ttl );
1627  $this->parent->setTTL( $ttl );
1628  }
1629 }
1630 
1635 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
1637  // @codingStandardsIgnoreEnd
1638 
1639  public $args;
1640 
1641  public function __construct( $preprocessor, $args ) {
1642  parent::__construct( $preprocessor );
1643  $this->args = $args;
1644  }
1645 
1646  public function __toString() {
1647  $s = 'cstmframe{';
1648  $first = true;
1649  foreach ( $this->args as $name => $value ) {
1650  if ( $first ) {
1651  $first = false;
1652  } else {
1653  $s .= ', ';
1654  }
1655  $s .= "\"$name\":\"" .
1656  str_replace( '"', '\\"', $value->__toString() ) . '"';
1657  }
1658  $s .= '}';
1659  return $s;
1660  }
1661 
1665  public function isEmpty() {
1666  return !count( $this->args );
1667  }
1668 
1673  public function getArgument( $index ) {
1674  if ( !isset( $this->args[$index] ) ) {
1675  return false;
1676  }
1677  return $this->args[$index];
1678  }
1679 
1680  public function getArguments() {
1681  return $this->args;
1682  }
1683 }
1684 
1688 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
1689 class PPNode_Hash_Tree implements PPNode {
1690  // @codingStandardsIgnoreEnd
1691 
1693 
1694  public function __construct( $name ) {
1695  $this->name = $name;
1696  $this->firstChild = $this->lastChild = $this->nextSibling = false;
1697  }
1698 
1699  public function __toString() {
1700  $inner = '';
1701  $attribs = '';
1702  for ( $node = $this->firstChild; $node; $node = $node->nextSibling ) {
1703  if ( $node instanceof PPNode_Hash_Attr ) {
1704  $attribs .= ' ' . $node->name . '="' . htmlspecialchars( $node->value ) . '"';
1705  } else {
1706  $inner .= $node->__toString();
1707  }
1708  }
1709  if ( $inner === '' ) {
1710  return "<{$this->name}$attribs/>";
1711  } else {
1712  return "<{$this->name}$attribs>$inner</{$this->name}>";
1713  }
1714  }
1715 
1721  public static function newWithText( $name, $text ) {
1722  $obj = new self( $name );
1723  $obj->addChild( new PPNode_Hash_Text( $text ) );
1724  return $obj;
1725  }
1726 
1727  public function addChild( $node ) {
1728  if ( $this->lastChild === false ) {
1729  $this->firstChild = $this->lastChild = $node;
1730  } else {
1731  $this->lastChild->nextSibling = $node;
1732  $this->lastChild = $node;
1733  }
1734  }
1735 
1739  public function getChildren() {
1740  $children = [];
1741  for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
1742  $children[] = $child;
1743  }
1744  return new PPNode_Hash_Array( $children );
1745  }
1746 
1747  public function getFirstChild() {
1748  return $this->firstChild;
1749  }
1750 
1751  public function getNextSibling() {
1752  return $this->nextSibling;
1753  }
1754 
1755  public function getChildrenOfType( $name ) {
1756  $children = [];
1757  for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
1758  if ( isset( $child->name ) && $child->name === $name ) {
1759  $children[] = $child;
1760  }
1761  }
1762  return new PPNode_Hash_Array( $children );
1763  }
1764 
1768  public function getLength() {
1769  return false;
1770  }
1771 
1776  public function item( $i ) {
1777  return false;
1778  }
1779 
1783  public function getName() {
1784  return $this->name;
1785  }
1786 
1796  public function splitArg() {
1797  $bits = [];
1798  for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
1799  if ( !isset( $child->name ) ) {
1800  continue;
1801  }
1802  if ( $child->name === 'name' ) {
1803  $bits['name'] = $child;
1804  if ( $child->firstChild instanceof PPNode_Hash_Attr
1805  && $child->firstChild->name === 'index'
1806  ) {
1807  $bits['index'] = $child->firstChild->value;
1808  }
1809  } elseif ( $child->name === 'value' ) {
1810  $bits['value'] = $child;
1811  }
1812  }
1813 
1814  if ( !isset( $bits['name'] ) ) {
1815  throw new MWException( 'Invalid brace node passed to ' . __METHOD__ );
1816  }
1817  if ( !isset( $bits['index'] ) ) {
1818  $bits['index'] = '';
1819  }
1820  return $bits;
1821  }
1822 
1830  public function splitExt() {
1831  $bits = [];
1832  for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
1833  if ( !isset( $child->name ) ) {
1834  continue;
1835  }
1836  if ( $child->name == 'name' ) {
1837  $bits['name'] = $child;
1838  } elseif ( $child->name == 'attr' ) {
1839  $bits['attr'] = $child;
1840  } elseif ( $child->name == 'inner' ) {
1841  $bits['inner'] = $child;
1842  } elseif ( $child->name == 'close' ) {
1843  $bits['close'] = $child;
1844  }
1845  }
1846  if ( !isset( $bits['name'] ) ) {
1847  throw new MWException( 'Invalid ext node passed to ' . __METHOD__ );
1848  }
1849  return $bits;
1850  }
1851 
1858  public function splitHeading() {
1859  if ( $this->name !== 'h' ) {
1860  throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
1861  }
1862  $bits = [];
1863  for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
1864  if ( !isset( $child->name ) ) {
1865  continue;
1866  }
1867  if ( $child->name == 'i' ) {
1868  $bits['i'] = $child->value;
1869  } elseif ( $child->name == 'level' ) {
1870  $bits['level'] = $child->value;
1871  }
1872  }
1873  if ( !isset( $bits['i'] ) ) {
1874  throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
1875  }
1876  return $bits;
1877  }
1878 
1885  public function splitTemplate() {
1886  $parts = [];
1887  $bits = [ 'lineStart' => '' ];
1888  for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
1889  if ( !isset( $child->name ) ) {
1890  continue;
1891  }
1892  if ( $child->name == 'title' ) {
1893  $bits['title'] = $child;
1894  }
1895  if ( $child->name == 'part' ) {
1896  $parts[] = $child;
1897  }
1898  if ( $child->name == 'lineStart' ) {
1899  $bits['lineStart'] = '1';
1900  }
1901  }
1902  if ( !isset( $bits['title'] ) ) {
1903  throw new MWException( 'Invalid node passed to ' . __METHOD__ );
1904  }
1905  $bits['parts'] = new PPNode_Hash_Array( $parts );
1906  return $bits;
1907  }
1908 }
1909 
1913 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
1914 class PPNode_Hash_Text implements PPNode {
1915  // @codingStandardsIgnoreEnd
1916 
1918 
1919  public function __construct( $value ) {
1920  if ( is_object( $value ) ) {
1921  throw new MWException( __CLASS__ . ' given object instead of string' );
1922  }
1923  $this->value = $value;
1924  }
1925 
1926  public function __toString() {
1927  return htmlspecialchars( $this->value );
1928  }
1929 
1930  public function getNextSibling() {
1931  return $this->nextSibling;
1932  }
1933 
1934  public function getChildren() {
1935  return false;
1936  }
1937 
1938  public function getFirstChild() {
1939  return false;
1940  }
1941 
1942  public function getChildrenOfType( $name ) {
1943  return false;
1944  }
1945 
1946  public function getLength() {
1947  return false;
1948  }
1949 
1950  public function item( $i ) {
1951  return false;
1952  }
1953 
1954  public function getName() {
1955  return '#text';
1956  }
1957 
1958  public function splitArg() {
1959  throw new MWException( __METHOD__ . ': not supported' );
1960  }
1961 
1962  public function splitExt() {
1963  throw new MWException( __METHOD__ . ': not supported' );
1964  }
1965 
1966  public function splitHeading() {
1967  throw new MWException( __METHOD__ . ': not supported' );
1968  }
1969 }
1970 
1974 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
1975 class PPNode_Hash_Array implements PPNode {
1976  // @codingStandardsIgnoreEnd
1977 
1979 
1980  public function __construct( $value ) {
1981  $this->value = $value;
1982  }
1983 
1984  public function __toString() {
1985  return var_export( $this, true );
1986  }
1987 
1988  public function getLength() {
1989  return count( $this->value );
1990  }
1991 
1992  public function item( $i ) {
1993  return $this->value[$i];
1994  }
1995 
1996  public function getName() {
1997  return '#nodelist';
1998  }
1999 
2000  public function getNextSibling() {
2001  return $this->nextSibling;
2002  }
2003 
2004  public function getChildren() {
2005  return false;
2006  }
2007 
2008  public function getFirstChild() {
2009  return false;
2010  }
2011 
2012  public function getChildrenOfType( $name ) {
2013  return false;
2014  }
2015 
2016  public function splitArg() {
2017  throw new MWException( __METHOD__ . ': not supported' );
2018  }
2019 
2020  public function splitExt() {
2021  throw new MWException( __METHOD__ . ': not supported' );
2022  }
2023 
2024  public function splitHeading() {
2025  throw new MWException( __METHOD__ . ': not supported' );
2026  }
2027 }
2028 
2032 // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
2033 class PPNode_Hash_Attr implements PPNode {
2034  // @codingStandardsIgnoreEnd
2035 
2037 
2038  public function __construct( $name, $value ) {
2039  $this->name = $name;
2040  $this->value = $value;
2041  }
2042 
2043  public function __toString() {
2044  return "<@{$this->name}>" . htmlspecialchars( $this->value ) . "</@{$this->name}>";
2045  }
2046 
2047  public function getName() {
2048  return $this->name;
2049  }
2050 
2051  public function getNextSibling() {
2052  return $this->nextSibling;
2053  }
2054 
2055  public function getChildren() {
2056  return false;
2057  }
2058 
2059  public function getFirstChild() {
2060  return false;
2061  }
2062 
2063  public function getChildrenOfType( $name ) {
2064  return false;
2065  }
2066 
2067  public function getLength() {
2068  return false;
2069  }
2070 
2071  public function item( $i ) {
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 }
splitArg()
Split a "" node into an associative array containing:
const MARKER_PREFIX
Definition: Parser.php:141
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:762
magic word the default is to use $key to get the and $key value or $key value text $key value html to format the value $key
Definition: hooks.txt:2321
splitTemplate()
Split a "