Fix this broken crap some more
[lhc/web/wiklou.git] / includes / parser / Preprocessor_Hash.php
1 <?php
2
3 /**
4 * Differences from DOM schema:
5 * * attribute nodes are children
6 * * <h> nodes that aren't at the top are replaced with <possible-h>
7 * @ingroup Parser
8 */
9 class Preprocessor_Hash implements Preprocessor {
10 var $parser;
11
12 function __construct( $parser ) {
13 $this->parser = $parser;
14 }
15
16 function newFrame() {
17 return new PPFrame_Hash( $this );
18 }
19
20 function newCustomFrame( $args ) {
21 return new PPCustomFrame_Hash( $this, $args );
22 }
23
24 /**
25 * Preprocess some wikitext and return the document tree.
26 * This is the ghost of Parser::replace_variables().
27 *
28 * @param string $text The text to parse
29 * @param integer flags Bitwise combination of:
30 * Parser::PTD_FOR_INCLUSION Handle <noinclude>/<includeonly> as if the text is being
31 * included. Default is to assume a direct page view.
32 *
33 * The generated DOM tree must depend only on the input text and the flags.
34 * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of bug 4899.
35 *
36 * Any flag added to the $flags parameter here, or any other parameter liable to cause a
37 * change in the DOM tree for a given text, must be passed through the section identifier
38 * in the section edit link and thus back to extractSections().
39 *
40 * The output of this function is currently only cached in process memory, but a persistent
41 * cache may be implemented at a later date which takes further advantage of these strict
42 * dependency requirements.
43 *
44 * @private
45 */
46 function preprocessToObj( $text, $flags = 0 ) {
47 wfProfileIn( __METHOD__ );
48
49 $rules = array(
50 '{' => array(
51 'end' => '}',
52 'names' => array(
53 2 => 'template',
54 3 => 'tplarg',
55 ),
56 'min' => 2,
57 'max' => 3,
58 ),
59 '[' => array(
60 'end' => ']',
61 'names' => array( 2 => null ),
62 'min' => 2,
63 'max' => 2,
64 )
65 );
66
67 $forInclusion = $flags & Parser::PTD_FOR_INCLUSION;
68
69 $xmlishElements = $this->parser->getStripList();
70 $enableOnlyinclude = false;
71 if ( $forInclusion ) {
72 $ignoredTags = array( 'includeonly', '/includeonly' );
73 $ignoredElements = array( 'noinclude' );
74 $xmlishElements[] = 'noinclude';
75 if ( strpos( $text, '<onlyinclude>' ) !== false && strpos( $text, '</onlyinclude>' ) !== false ) {
76 $enableOnlyinclude = true;
77 }
78 } else {
79 $ignoredTags = array( 'noinclude', '/noinclude', 'onlyinclude', '/onlyinclude' );
80 $ignoredElements = array( 'includeonly' );
81 $xmlishElements[] = 'includeonly';
82 }
83 $xmlishRegex = implode( '|', array_merge( $xmlishElements, $ignoredTags ) );
84
85 // Use "A" modifier (anchored) instead of "^", because ^ doesn't work with an offset
86 $elementsRegex = "~($xmlishRegex)(?:\s|\/>|>)|(!--)~iA";
87
88 $stack = new PPDStack_Hash;
89
90 $searchBase = "[{<\n";
91 $revText = strrev( $text ); // For fast reverse searches
92
93 $i = 0; # Input pointer, starts out pointing to a pseudo-newline before the start
94 $accum =& $stack->getAccum(); # Current accumulator
95 $findEquals = false; # True to find equals signs in arguments
96 $findPipe = false; # True to take notice of pipe characters
97 $headingIndex = 1;
98 $inHeading = false; # True if $i is inside a possible heading
99 $noMoreGT = false; # True if there are no more greater-than (>) signs right of $i
100 $findOnlyinclude = $enableOnlyinclude; # True to ignore all input up to the next <onlyinclude>
101 $fakeLineStart = true; # Do a line-start run without outputting an LF character
102
103 while ( true ) {
104 //$this->memCheck();
105
106 if ( $findOnlyinclude ) {
107 // Ignore all input up to the next <onlyinclude>
108 $startPos = strpos( $text, '<onlyinclude>', $i );
109 if ( $startPos === false ) {
110 // Ignored section runs to the end
111 $accum->addNodeWithText( 'ignore', substr( $text, $i ) );
112 break;
113 }
114 $tagEndPos = $startPos + strlen( '<onlyinclude>' ); // past-the-end
115 $accum->addNodeWithText( 'ignore', substr( $text, $i, $tagEndPos - $i ) );
116 $i = $tagEndPos;
117 $findOnlyinclude = false;
118 }
119
120 if ( $fakeLineStart ) {
121 $found = 'line-start';
122 $curChar = '';
123 } else {
124 # Find next opening brace, closing brace or pipe
125 $search = $searchBase;
126 if ( $stack->top === false ) {
127 $currentClosing = '';
128 } else {
129 $currentClosing = $stack->top->close;
130 $search .= $currentClosing;
131 }
132 if ( $findPipe ) {
133 $search .= '|';
134 }
135 if ( $findEquals ) {
136 // First equals will be for the template
137 $search .= '=';
138 }
139 $rule = null;
140 # Output literal section, advance input counter
141 $literalLength = strcspn( $text, $search, $i );
142 if ( $literalLength > 0 ) {
143 $accum->addLiteral( substr( $text, $i, $literalLength ) );
144 $i += $literalLength;
145 }
146 if ( $i >= strlen( $text ) ) {
147 if ( $currentClosing == "\n" ) {
148 // Do a past-the-end run to finish off the heading
149 $curChar = '';
150 $found = 'line-end';
151 } else {
152 # All done
153 break;
154 }
155 } else {
156 $curChar = $text[$i];
157 if ( $curChar == '|' ) {
158 $found = 'pipe';
159 } elseif ( $curChar == '=' ) {
160 $found = 'equals';
161 } elseif ( $curChar == '<' ) {
162 $found = 'angle';
163 } elseif ( $curChar == "\n" ) {
164 if ( $inHeading ) {
165 $found = 'line-end';
166 } else {
167 $found = 'line-start';
168 }
169 } elseif ( $curChar == $currentClosing ) {
170 $found = 'close';
171 } elseif ( isset( $rules[$curChar] ) ) {
172 $found = 'open';
173 $rule = $rules[$curChar];
174 } else {
175 # Some versions of PHP have a strcspn which stops on null characters
176 # Ignore and continue
177 ++$i;
178 continue;
179 }
180 }
181 }
182
183 if ( $found == 'angle' ) {
184 $matches = false;
185 // Handle </onlyinclude>
186 if ( $enableOnlyinclude && substr( $text, $i, strlen( '</onlyinclude>' ) ) == '</onlyinclude>' ) {
187 $findOnlyinclude = true;
188 continue;
189 }
190
191 // Determine element name
192 if ( !preg_match( $elementsRegex, $text, $matches, 0, $i + 1 ) ) {
193 // Element name missing or not listed
194 $accum->addLiteral( '<' );
195 ++$i;
196 continue;
197 }
198 // Handle comments
199 if ( isset( $matches[2] ) && $matches[2] == '!--' ) {
200 // To avoid leaving blank lines, when a comment is both preceded
201 // and followed by a newline (ignoring spaces), trim leading and
202 // trailing spaces and one of the newlines.
203
204 // Find the end
205 $endPos = strpos( $text, '-->', $i + 4 );
206 if ( $endPos === false ) {
207 // Unclosed comment in input, runs to end
208 $inner = substr( $text, $i );
209 $accum->addNodeWithText( 'comment', $inner );
210 $i = strlen( $text );
211 } else {
212 // Search backwards for leading whitespace
213 $wsStart = $i ? ( $i - strspn( $revText, ' ', strlen( $text ) - $i ) ) : 0;
214 // Search forwards for trailing whitespace
215 // $wsEnd will be the position of the last space
216 $wsEnd = $endPos + 2 + strspn( $text, ' ', $endPos + 3 );
217 // Eat the line if possible
218 // TODO: This could theoretically be done if $wsStart == 0, i.e. for comments at
219 // the overall start. That's not how Sanitizer::removeHTMLcomments() did it, but
220 // it's a possible beneficial b/c break.
221 if ( $wsStart > 0 && substr( $text, $wsStart - 1, 1 ) == "\n"
222 && substr( $text, $wsEnd + 1, 1 ) == "\n" )
223 {
224 $startPos = $wsStart;
225 $endPos = $wsEnd + 1;
226 // Remove leading whitespace from the end of the accumulator
227 // Sanity check first though
228 $wsLength = $i - $wsStart;
229 if ( $wsLength > 0
230 && $accum->lastNode instanceof PPNode_Hash_Text
231 && substr( $accum->lastNode->value, -$wsLength ) === str_repeat( ' ', $wsLength ) )
232 {
233 $accum->lastNode->value = substr( $accum->lastNode->value, 0, -$wsLength );
234 }
235 // Do a line-start run next time to look for headings after the comment
236 $fakeLineStart = true;
237 } else {
238 // No line to eat, just take the comment itself
239 $startPos = $i;
240 $endPos += 2;
241 }
242
243 if ( $stack->top ) {
244 $part = $stack->top->getCurrentPart();
245 if ( isset( $part->commentEnd ) && $part->commentEnd == $wsStart - 1 ) {
246 // Comments abutting, no change in visual end
247 $part->commentEnd = $wsEnd;
248 } else {
249 $part->visualEnd = $wsStart;
250 $part->commentEnd = $endPos;
251 }
252 }
253 $i = $endPos + 1;
254 $inner = substr( $text, $startPos, $endPos - $startPos + 1 );
255 $accum->addNodeWithText( 'comment', $inner );
256 }
257 continue;
258 }
259 $name = $matches[1];
260 $lowerName = strtolower( $name );
261 $attrStart = $i + strlen( $name ) + 1;
262
263 // Find end of tag
264 $tagEndPos = $noMoreGT ? false : strpos( $text, '>', $attrStart );
265 if ( $tagEndPos === false ) {
266 // Infinite backtrack
267 // Disable tag search to prevent worst-case O(N^2) performance
268 $noMoreGT = true;
269 $accum->addLiteral( '<' );
270 ++$i;
271 continue;
272 }
273
274 // Handle ignored tags
275 if ( in_array( $lowerName, $ignoredTags ) ) {
276 $accum->addNodeWithText( 'ignore', substr( $text, $i, $tagEndPos - $i + 1 ) );
277 $i = $tagEndPos + 1;
278 continue;
279 }
280
281 $tagStartPos = $i;
282 if ( $text[$tagEndPos-1] == '/' ) {
283 // Short end tag
284 $attrEnd = $tagEndPos - 1;
285 $inner = null;
286 $i = $tagEndPos + 1;
287 $close = null;
288 } else {
289 $attrEnd = $tagEndPos;
290 // Find closing tag
291 if ( preg_match( "/<\/$name\s*>/i", $text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 ) ) {
292 $inner = substr( $text, $tagEndPos + 1, $matches[0][1] - $tagEndPos - 1 );
293 $i = $matches[0][1] + strlen( $matches[0][0] );
294 $close = $matches[0][0];
295 } else {
296 // No end tag -- let it run out to the end of the text.
297 $inner = substr( $text, $tagEndPos + 1 );
298 $i = strlen( $text );
299 $close = null;
300 }
301 }
302 // <includeonly> and <noinclude> just become <ignore> tags
303 if ( in_array( $lowerName, $ignoredElements ) ) {
304 $accum->addNodeWithText( 'ignore', substr( $text, $tagStartPos, $i - $tagStartPos ) );
305 continue;
306 }
307
308 if ( $attrEnd <= $attrStart ) {
309 $attr = '';
310 } else {
311 // Note that the attr element contains the whitespace between name and attribute,
312 // this is necessary for precise reconstruction during pre-save transform.
313 $attr = substr( $text, $attrStart, $attrEnd - $attrStart );
314 }
315
316 $extNode = new PPNode_Hash_Tree( 'ext' );
317 $extNode->addChild( PPNode_Hash_Tree::newWithText( 'name', $name ) );
318 $extNode->addChild( PPNode_Hash_Tree::newWithText( 'attr', $attr ) );
319 if ( $inner !== null ) {
320 $extNode->addChild( PPNode_Hash_Tree::newWithText( 'inner', $inner ) );
321 }
322 if ( $close !== null ) {
323 $extNode->addChild( PPNode_Hash_Tree::newWithText( 'close', $close ) );
324 }
325 $accum->addNode( $extNode );
326 }
327
328 elseif ( $found == 'line-start' ) {
329 // Is this the start of a heading?
330 // Line break belongs before the heading element in any case
331 if ( $fakeLineStart ) {
332 $fakeLineStart = false;
333 } else {
334 $accum->addLiteral( $curChar );
335 $i++;
336 }
337
338 $count = strspn( $text, '=', $i, 6 );
339 if ( $count == 1 && $findEquals ) {
340 // DWIM: This looks kind of like a name/value separator
341 // Let's let the equals handler have it and break the potential heading
342 // This is heuristic, but AFAICT the methods for completely correct disambiguation are very complex.
343 } elseif ( $count > 0 ) {
344 $piece = array(
345 'open' => "\n",
346 'close' => "\n",
347 'parts' => array( new PPDPart_Hash( str_repeat( '=', $count ) ) ),
348 'startPos' => $i,
349 'count' => $count );
350 $stack->push( $piece );
351 $accum =& $stack->getAccum();
352 extract( $stack->getFlags() );
353 $i += $count;
354 }
355 }
356
357 elseif ( $found == 'line-end' ) {
358 $piece = $stack->top;
359 // A heading must be open, otherwise \n wouldn't have been in the search list
360 assert( $piece->open == "\n" );
361 $part = $piece->getCurrentPart();
362 // Search back through the input to see if it has a proper close
363 // Do this using the reversed string since the other solutions (end anchor, etc.) are inefficient
364 $wsLength = strspn( $revText, " \t", strlen( $text ) - $i );
365 $searchStart = $i - $wsLength;
366 if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) {
367 // Comment found at line end
368 // Search for equals signs before the comment
369 $searchStart = $part->visualEnd;
370 $searchStart -= strspn( $revText, " \t", strlen( $text ) - $searchStart );
371 }
372 $count = $piece->count;
373 $equalsLength = strspn( $revText, '=', strlen( $text ) - $searchStart );
374 if ( $equalsLength > 0 ) {
375 if ( $i - $equalsLength == $piece->startPos ) {
376 // This is just a single string of equals signs on its own line
377 // Replicate the doHeadings behaviour /={count}(.+)={count}/
378 // First find out how many equals signs there really are (don't stop at 6)
379 $count = $equalsLength;
380 if ( $count < 3 ) {
381 $count = 0;
382 } else {
383 $count = min( 6, intval( ( $count - 1 ) / 2 ) );
384 }
385 } else {
386 $count = min( $equalsLength, $count );
387 }
388 if ( $count > 0 ) {
389 // Normal match, output <h>
390 $element = new PPNode_Hash_Tree( 'possible-h' );
391 $element->addChild( new PPNode_Hash_Attr( 'level', $count ) );
392 $element->addChild( new PPNode_Hash_Attr( 'i', $headingIndex++ ) );
393 $element->lastChild->nextSibling = $accum->firstNode;
394 $element->lastChild = $accum->lastNode;
395 } else {
396 // Single equals sign on its own line, count=0
397 $element = $accum;
398 }
399 } else {
400 // No match, no <h>, just pass down the inner text
401 $element = $accum;
402 }
403 // Unwind the stack
404 $stack->pop();
405 $accum =& $stack->getAccum();
406 extract( $stack->getFlags() );
407
408 // Append the result to the enclosing accumulator
409 if ( $element instanceof PPNode ) {
410 $accum->addNode( $element );
411 } else {
412 $accum->addAccum( $element );
413 }
414 // Note that we do NOT increment the input pointer.
415 // This is because the closing linebreak could be the opening linebreak of
416 // another heading. Infinite loops are avoided because the next iteration MUST
417 // hit the heading open case above, which unconditionally increments the
418 // input pointer.
419 }
420
421 elseif ( $found == 'open' ) {
422 # count opening brace characters
423 $count = strspn( $text, $curChar, $i );
424
425 # we need to add to stack only if opening brace count is enough for one of the rules
426 if ( $count >= $rule['min'] ) {
427 # Add it to the stack
428 $piece = array(
429 'open' => $curChar,
430 'close' => $rule['end'],
431 'count' => $count,
432 'lineStart' => ($i > 0 && $text[$i-1] == "\n"),
433 );
434
435 $stack->push( $piece );
436 $accum =& $stack->getAccum();
437 extract( $stack->getFlags() );
438 } else {
439 # Add literal brace(s)
440 $accum->addLiteral( str_repeat( $curChar, $count ) );
441 }
442 $i += $count;
443 }
444
445 elseif ( $found == 'close' ) {
446 $piece = $stack->top;
447 # lets check if there are enough characters for closing brace
448 $maxCount = $piece->count;
449 $count = strspn( $text, $curChar, $i, $maxCount );
450
451 # check for maximum matching characters (if there are 5 closing
452 # characters, we will probably need only 3 - depending on the rules)
453 $matchingCount = 0;
454 $rule = $rules[$piece->open];
455 if ( $count > $rule['max'] ) {
456 # The specified maximum exists in the callback array, unless the caller
457 # has made an error
458 $matchingCount = $rule['max'];
459 } else {
460 # Count is less than the maximum
461 # Skip any gaps in the callback array to find the true largest match
462 # Need to use array_key_exists not isset because the callback can be null
463 $matchingCount = $count;
464 while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $rule['names'] ) ) {
465 --$matchingCount;
466 }
467 }
468
469 if ($matchingCount <= 0) {
470 # No matching element found in callback array
471 # Output a literal closing brace and continue
472 $accum->addLiteral( str_repeat( $curChar, $count ) );
473 $i += $count;
474 continue;
475 }
476 $name = $rule['names'][$matchingCount];
477 if ( $name === null ) {
478 // No element, just literal text
479 $element = $piece->breakSyntax( $matchingCount );
480 $element->addLiteral( str_repeat( $rule['end'], $matchingCount ) );
481 } else {
482 # Create XML element
483 # Note: $parts is already XML, does not need to be encoded further
484 $parts = $piece->parts;
485 $titleAccum = $parts[0]->out;
486 unset( $parts[0] );
487
488 $element = new PPNode_Hash_Tree( $name );
489
490 # The invocation is at the start of the line if lineStart is set in
491 # the stack, and all opening brackets are used up.
492 if ( $maxCount == $matchingCount && !empty( $piece->lineStart ) ) {
493 $element->addChild( new PPNode_Hash_Attr( 'lineStart', 1 ) );
494 }
495 $titleNode = new PPNode_Hash_Tree( 'title' );
496 $titleNode->firstChild = $titleAccum->firstNode;
497 $titleNode->lastChild = $titleAccum->lastNode;
498 $element->addChild( $titleNode );
499 $argIndex = 1;
500 foreach ( $parts as $partIndex => $part ) {
501 if ( isset( $part->eqpos ) ) {
502 // Find equals
503 $lastNode = false;
504 for ( $node = $part->out->firstNode; $node; $node = $node->nextSibling ) {
505 if ( $node === $part->eqpos ) {
506 break;
507 }
508 $lastNode = $node;
509 }
510 if ( !$node ) {
511 throw new MWException( __METHOD__. ': eqpos not found' );
512 }
513 if ( $node->name !== 'equals' ) {
514 throw new MWException( __METHOD__ .': eqpos is not equals' );
515 }
516 $equalsNode = $node;
517
518 // Construct name node
519 $nameNode = new PPNode_Hash_Tree( 'name' );
520 if ( $lastNode !== false ) {
521 $lastNode->nextSibling = false;
522 $nameNode->firstChild = $part->out->firstNode;
523 $nameNode->lastChild = $lastNode;
524 }
525
526 // Construct value node
527 $valueNode = new PPNode_Hash_Tree( 'value' );
528 if ( $equalsNode->nextSibling !== false ) {
529 $valueNode->firstChild = $equalsNode->nextSibling;
530 $valueNode->lastChild = $part->out->lastNode;
531 }
532 $partNode = new PPNode_Hash_Tree( 'part' );
533 $partNode->addChild( $nameNode );
534 $partNode->addChild( $equalsNode->firstChild );
535 $partNode->addChild( $valueNode );
536 $element->addChild( $partNode );
537 } else {
538 $partNode = new PPNode_Hash_Tree( 'part' );
539 $nameNode = new PPNode_Hash_Tree( 'name' );
540 $nameNode->addChild( new PPNode_Hash_Attr( 'index', $argIndex++ ) );
541 $valueNode = new PPNode_Hash_Tree( 'value' );
542 $valueNode->firstChild = $part->out->firstNode;
543 $valueNode->lastChild = $part->out->lastNode;
544 $partNode->addChild( $nameNode );
545 $partNode->addChild( $valueNode );
546 $element->addChild( $partNode );
547 }
548 }
549 }
550
551 # Advance input pointer
552 $i += $matchingCount;
553
554 # Unwind the stack
555 $stack->pop();
556 $accum =& $stack->getAccum();
557
558 # Re-add the old stack element if it still has unmatched opening characters remaining
559 if ($matchingCount < $piece->count) {
560 $piece->parts = array( new PPDPart_Hash );
561 $piece->count -= $matchingCount;
562 # do we still qualify for any callback with remaining count?
563 $names = $rules[$piece->open]['names'];
564 $skippedBraces = 0;
565 $enclosingAccum =& $accum;
566 while ( $piece->count ) {
567 if ( array_key_exists( $piece->count, $names ) ) {
568 $stack->push( $piece );
569 $accum =& $stack->getAccum();
570 break;
571 }
572 --$piece->count;
573 $skippedBraces ++;
574 }
575 $enclosingAccum->addLiteral( str_repeat( $piece->open, $skippedBraces ) );
576 }
577
578 extract( $stack->getFlags() );
579
580 # Add XML element to the enclosing accumulator
581 if ( $element instanceof PPNode ) {
582 $accum->addNode( $element );
583 } else {
584 $accum->addAccum( $element );
585 }
586 }
587
588 elseif ( $found == 'pipe' ) {
589 $findEquals = true; // shortcut for getFlags()
590 $stack->addPart();
591 $accum =& $stack->getAccum();
592 ++$i;
593 }
594
595 elseif ( $found == 'equals' ) {
596 $findEquals = false; // shortcut for getFlags()
597 $accum->addNodeWithText( 'equals', '=' );
598 $stack->getCurrentPart()->eqpos = $accum->lastNode;
599 ++$i;
600 }
601 }
602
603 # Output any remaining unclosed brackets
604 foreach ( $stack->stack as $piece ) {
605 $stack->rootAccum->addAccum( $piece->breakSyntax() );
606 }
607
608 # Enable top-level headings
609 for ( $node = $stack->rootAccum->firstNode; $node; $node = $node->nextSibling ) {
610 if ( isset( $node->name ) && $node->name === 'possible-h' ) {
611 $node->name = 'h';
612 }
613 }
614
615 $rootNode = new PPNode_Hash_Tree( 'root' );
616 $rootNode->firstChild = $stack->rootAccum->firstNode;
617 $rootNode->lastChild = $stack->rootAccum->lastNode;
618 wfProfileOut( __METHOD__ );
619 return $rootNode;
620 }
621 }
622
623 /**
624 * Stack class to help Preprocessor::preprocessToObj()
625 * @ingroup Parser
626 */
627 class PPDStack_Hash extends PPDStack {
628 function __construct() {
629 $this->elementClass = 'PPDStackElement_Hash';
630 parent::__construct();
631 $this->rootAccum = new PPDAccum_Hash;
632 }
633 }
634
635 /**
636 * @ingroup Parser
637 */
638 class PPDStackElement_Hash extends PPDStackElement {
639 function __construct( $data = array() ) {
640 $this->partClass = 'PPDPart_Hash';
641 parent::__construct( $data );
642 }
643
644 /**
645 * Get the accumulator that would result if the close is not found.
646 */
647 function breakSyntax( $openingCount = false ) {
648 if ( $this->open == "\n" ) {
649 $accum = $this->parts[0]->out;
650 } else {
651 if ( $openingCount === false ) {
652 $openingCount = $this->count;
653 }
654 $accum = new PPDAccum_Hash;
655 $accum->addLiteral( str_repeat( $this->open, $openingCount ) );
656 $first = true;
657 foreach ( $this->parts as $part ) {
658 if ( $first ) {
659 $first = false;
660 } else {
661 $accum->addLiteral( '|' );
662 }
663 $accum->addAccum( $part->out );
664 }
665 }
666 return $accum;
667 }
668 }
669
670 /**
671 * @ingroup Parser
672 */
673 class PPDPart_Hash extends PPDPart {
674 function __construct( $out = '' ) {
675 $accum = new PPDAccum_Hash;
676 if ( $out !== '' ) {
677 $accum->addLiteral( $out );
678 }
679 parent::__construct( $accum );
680 }
681 }
682
683 /**
684 * @ingroup Parser
685 */
686 class PPDAccum_Hash {
687 var $firstNode, $lastNode;
688
689 function __construct() {
690 $this->firstNode = $this->lastNode = false;
691 }
692
693 /**
694 * Append a string literal
695 */
696 function addLiteral( $s ) {
697 if ( $this->lastNode === false ) {
698 $this->firstNode = $this->lastNode = new PPNode_Hash_Text( $s );
699 } elseif ( $this->lastNode instanceof PPNode_Hash_Text ) {
700 $this->lastNode->value .= $s;
701 } else {
702 $this->lastNode->nextSibling = new PPNode_Hash_Text( $s );
703 $this->lastNode = $this->lastNode->nextSibling;
704 }
705 }
706
707 /**
708 * Append a PPNode
709 */
710 function addNode( PPNode $node ) {
711 if ( $this->lastNode === false ) {
712 $this->firstNode = $this->lastNode = $node;
713 } else {
714 $this->lastNode->nextSibling = $node;
715 $this->lastNode = $node;
716 }
717 }
718
719 /**
720 * Append a tree node with text contents
721 */
722 function addNodeWithText( $name, $value ) {
723 $node = PPNode_Hash_Tree::newWithText( $name, $value );
724 $this->addNode( $node );
725 }
726
727 /**
728 * Append a PPAccum_Hash
729 * Takes over ownership of the nodes in the source argument. These nodes may
730 * subsequently be modified, especially nextSibling.
731 */
732 function addAccum( $accum ) {
733 if ( $accum->lastNode === false ) {
734 // nothing to add
735 } elseif ( $this->lastNode === false ) {
736 $this->firstNode = $accum->firstNode;
737 $this->lastNode = $accum->lastNode;
738 } else {
739 $this->lastNode->nextSibling = $accum->firstNode;
740 $this->lastNode = $accum->lastNode;
741 }
742 }
743 }
744
745 /**
746 * An expansion frame, used as a context to expand the result of preprocessToObj()
747 * @ingroup Parser
748 */
749 class PPFrame_Hash implements PPFrame {
750 var $preprocessor, $parser, $title;
751 var $titleCache;
752
753 /**
754 * Hashtable listing templates which are disallowed for expansion in this frame,
755 * having been encountered previously in parent frames.
756 */
757 var $loopCheckHash;
758
759 /**
760 * Recursion depth of this frame, top = 0
761 * Note that this is NOT the same as expansion depth in expand()
762 */
763 var $depth;
764
765
766 /**
767 * Construct a new preprocessor frame.
768 * @param Preprocessor $preprocessor The parent preprocessor
769 */
770 function __construct( $preprocessor ) {
771 $this->preprocessor = $preprocessor;
772 $this->parser = $preprocessor->parser;
773 $this->title = $this->parser->mTitle;
774 $this->titleCache = array( $this->title ? $this->title->getPrefixedDBkey() : false );
775 $this->loopCheckHash = array();
776 $this->depth = 0;
777 }
778
779 /**
780 * Create a new child frame
781 * $args is optionally a multi-root PPNode or array containing the template arguments
782 */
783 function newChild( $args = false, $title = false ) {
784 $namedArgs = array();
785 $numberedArgs = array();
786 if ( $title === false ) {
787 $title = $this->title;
788 }
789 if ( $args !== false ) {
790 $xpath = false;
791 if ( $args instanceof PPNode_Hash_Array ) {
792 $args = $args->value;
793 } elseif ( !is_array( $args ) ) {
794 throw new MWException( __METHOD__ . ': $args must be array or PPNode_Hash_Array' );
795 }
796 foreach ( $args as $arg ) {
797 $bits = $arg->splitArg();
798 if ( $bits['index'] !== '' ) {
799 // Numbered parameter
800 $numberedArgs[$bits['index']] = $bits['value'];
801 unset( $namedArgs[$bits['index']] );
802 } else {
803 // Named parameter
804 $name = trim( $this->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
805 $namedArgs[$name] = $bits['value'];
806 unset( $numberedArgs[$name] );
807 }
808 }
809 }
810 return new PPTemplateFrame_Hash( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title );
811 }
812
813 function expand( $root, $flags = 0 ) {
814 static $expansionDepth = 0;
815 if ( is_string( $root ) ) {
816 return $root;
817 }
818
819 if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->mMaxPPNodeCount )
820 {
821 return '<span class="error">Node-count limit exceeded</span>';
822 }
823 if ( $expansionDepth > $this->parser->mOptions->mMaxPPExpandDepth ) {
824 return '<span class="error">Expansion depth limit exceeded</span>';
825 }
826 ++$expansionDepth;
827
828 $outStack = array( '', '' );
829 $iteratorStack = array( false, $root );
830 $indexStack = array( 0, 0 );
831
832 while ( count( $iteratorStack ) > 1 ) {
833 $level = count( $outStack ) - 1;
834 $iteratorNode =& $iteratorStack[ $level ];
835 $out =& $outStack[$level];
836 $index =& $indexStack[$level];
837
838 if ( is_array( $iteratorNode ) ) {
839 if ( $index >= count( $iteratorNode ) ) {
840 // All done with this iterator
841 $iteratorStack[$level] = false;
842 $contextNode = false;
843 } else {
844 $contextNode = $iteratorNode[$index];
845 $index++;
846 }
847 } elseif ( $iteratorNode instanceof PPNode_Hash_Array ) {
848 if ( $index >= $iteratorNode->getLength() ) {
849 // All done with this iterator
850 $iteratorStack[$level] = false;
851 $contextNode = false;
852 } else {
853 $contextNode = $iteratorNode->item( $index );
854 $index++;
855 }
856 } else {
857 // Copy to $contextNode and then delete from iterator stack,
858 // because this is not an iterator but we do have to execute it once
859 $contextNode = $iteratorStack[$level];
860 $iteratorStack[$level] = false;
861 }
862
863 $newIterator = false;
864
865 if ( $contextNode === false ) {
866 // nothing to do
867 } elseif ( is_string( $contextNode ) ) {
868 $out .= $contextNode;
869 } elseif ( is_array( $contextNode ) || $contextNode instanceof PPNode_Hash_Array ) {
870 $newIterator = $contextNode;
871 } elseif ( $contextNode instanceof PPNode_Hash_Attr ) {
872 // No output
873 } elseif ( $contextNode instanceof PPNode_Hash_Text ) {
874 $out .= $contextNode->value;
875 } elseif ( $contextNode instanceof PPNode_Hash_Tree ) {
876 if ( $contextNode->name == 'template' ) {
877 # Double-brace expansion
878 $bits = $contextNode->splitTemplate();
879 if ( $flags & self::NO_TEMPLATES ) {
880 $newIterator = $this->virtualBracketedImplode( '{{', '|', '}}', $bits['title'], $bits['parts'] );
881 } else {
882 $ret = $this->parser->braceSubstitution( $bits, $this );
883 if ( isset( $ret['object'] ) ) {
884 $newIterator = $ret['object'];
885 } else {
886 $out .= $ret['text'];
887 }
888 }
889 } elseif ( $contextNode->name == 'tplarg' ) {
890 # Triple-brace expansion
891 $bits = $contextNode->splitTemplate();
892 if ( $flags & self::NO_ARGS ) {
893 $newIterator = $this->virtualBracketedImplode( '{{{', '|', '}}}', $bits['title'], $bits['parts'] );
894 } else {
895 $ret = $this->parser->argSubstitution( $bits, $this );
896 if ( isset( $ret['object'] ) ) {
897 $newIterator = $ret['object'];
898 } else {
899 $out .= $ret['text'];
900 }
901 }
902 } elseif ( $contextNode->name == 'comment' ) {
903 # HTML-style comment
904 # Remove it in HTML, pre+remove and STRIP_COMMENTS modes
905 if ( $this->parser->ot['html']
906 || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() )
907 || ( $flags & self::STRIP_COMMENTS ) )
908 {
909 $out .= '';
910 }
911 # Add a strip marker in PST mode so that pstPass2() can run some old-fashioned regexes on the result
912 # Not in RECOVER_COMMENTS mode (extractSections) though
913 elseif ( $this->parser->ot['wiki'] && ! ( $flags & self::RECOVER_COMMENTS ) ) {
914 $out .= $this->parser->insertStripItem( $contextNode->firstChild->value );
915 }
916 # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
917 else {
918 $out .= $contextNode->firstChild->value;
919 }
920 } elseif ( $contextNode->name == 'ignore' ) {
921 # Output suppression used by <includeonly> etc.
922 # OT_WIKI will only respect <ignore> in substed templates.
923 # The other output types respect it unless NO_IGNORE is set.
924 # extractSections() sets NO_IGNORE and so never respects it.
925 if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] ) || ( $flags & self::NO_IGNORE ) ) {
926 $out .= $contextNode->firstChild->value;
927 } else {
928 //$out .= '';
929 }
930 } elseif ( $contextNode->name == 'ext' ) {
931 # Extension tag
932 $bits = $contextNode->splitExt() + array( 'attr' => null, 'inner' => null, 'close' => null );
933 $out .= $this->parser->extensionSubstitution( $bits, $this );
934 } elseif ( $contextNode->name == 'h' ) {
935 # Heading
936 if ( $this->parser->ot['html'] ) {
937 # Expand immediately and insert heading index marker
938 $s = '';
939 for ( $node = $contextNode->firstChild; $node; $node = $node->nextSibling ) {
940 $s .= $this->expand( $node, $flags );
941 }
942
943 $bits = $contextNode->splitHeading();
944 $titleText = $this->title->getPrefixedDBkey();
945 $this->parser->mHeadings[] = array( $titleText, $bits['i'] );
946 $serial = count( $this->parser->mHeadings ) - 1;
947 $marker = "{$this->parser->mUniqPrefix}-h-$serial-" . Parser::MARKER_SUFFIX;
948 $s = substr( $s, 0, $bits['level'] ) . $marker . substr( $s, $bits['level'] );
949 $this->parser->mStripState->general->setPair( $marker, '' );
950 $out .= $s;
951 } else {
952 # Expand in virtual stack
953 $newIterator = $contextNode->getChildren();
954 }
955 } else {
956 # Generic recursive expansion
957 $newIterator = $contextNode->getChildren();
958 }
959 } else {
960 throw new MWException( __METHOD__.': Invalid parameter type' );
961 }
962
963 if ( $newIterator !== false ) {
964 $outStack[] = '';
965 $iteratorStack[] = $newIterator;
966 $indexStack[] = 0;
967 } elseif ( $iteratorStack[$level] === false ) {
968 // Return accumulated value to parent
969 // With tail recursion
970 while ( $iteratorStack[$level] === false && $level > 0 ) {
971 $outStack[$level - 1] .= $out;
972 array_pop( $outStack );
973 array_pop( $iteratorStack );
974 array_pop( $indexStack );
975 $level--;
976 }
977 }
978 }
979 --$expansionDepth;
980 return $outStack[0];
981 }
982
983 function implodeWithFlags( $sep, $flags /*, ... */ ) {
984 $args = array_slice( func_get_args(), 2 );
985
986 $first = true;
987 $s = '';
988 foreach ( $args as $root ) {
989 if ( $root instanceof PPNode_Hash_Array ) {
990 $root = $root->value;
991 }
992 if ( !is_array( $root ) ) {
993 $root = array( $root );
994 }
995 foreach ( $root as $node ) {
996 if ( $first ) {
997 $first = false;
998 } else {
999 $s .= $sep;
1000 }
1001 $s .= $this->expand( $node, $flags );
1002 }
1003 }
1004 return $s;
1005 }
1006
1007 /**
1008 * Implode with no flags specified
1009 * This previously called implodeWithFlags but has now been inlined to reduce stack depth
1010 */
1011 function implode( $sep /*, ... */ ) {
1012 $args = array_slice( func_get_args(), 1 );
1013
1014 $first = true;
1015 $s = '';
1016 foreach ( $args as $root ) {
1017 if ( $root instanceof PPNode_Hash_Array ) {
1018 $root = $root->value;
1019 }
1020 if ( !is_array( $root ) ) {
1021 $root = array( $root );
1022 }
1023 foreach ( $root as $node ) {
1024 if ( $first ) {
1025 $first = false;
1026 } else {
1027 $s .= $sep;
1028 }
1029 $s .= $this->expand( $node );
1030 }
1031 }
1032 return $s;
1033 }
1034
1035 /**
1036 * Makes an object that, when expand()ed, will be the same as one obtained
1037 * with implode()
1038 */
1039 function virtualImplode( $sep /*, ... */ ) {
1040 $args = array_slice( func_get_args(), 1 );
1041 $out = array();
1042 $first = true;
1043
1044 foreach ( $args as $root ) {
1045 if ( $root instanceof PPNode_Hash_Array ) {
1046 $root = $root->value;
1047 }
1048 if ( !is_array( $root ) ) {
1049 $root = array( $root );
1050 }
1051 foreach ( $root as $node ) {
1052 if ( $first ) {
1053 $first = false;
1054 } else {
1055 $out[] = $sep;
1056 }
1057 $out[] = $node;
1058 }
1059 }
1060 return new PPNode_Hash_Array( $out );
1061 }
1062
1063 /**
1064 * Virtual implode with brackets
1065 */
1066 function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) {
1067 $args = array_slice( func_get_args(), 3 );
1068 $out = array( $start );
1069 $first = true;
1070
1071 foreach ( $args as $root ) {
1072 if ( $root instanceof PPNode_Hash_Array ) {
1073 $root = $root->value;
1074 }
1075 if ( !is_array( $root ) ) {
1076 $root = array( $root );
1077 }
1078 foreach ( $root as $node ) {
1079 if ( $first ) {
1080 $first = false;
1081 } else {
1082 $out[] = $sep;
1083 }
1084 $out[] = $node;
1085 }
1086 }
1087 $out[] = $end;
1088 return new PPNode_Hash_Array( $out );
1089 }
1090
1091 function __toString() {
1092 return 'frame{}';
1093 }
1094
1095 function getPDBK( $level = false ) {
1096 if ( $level === false ) {
1097 return $this->title->getPrefixedDBkey();
1098 } else {
1099 return isset( $this->titleCache[$level] ) ? $this->titleCache[$level] : false;
1100 }
1101 }
1102
1103 /**
1104 * Returns true if there are no arguments in this frame
1105 */
1106 function isEmpty() {
1107 return true;
1108 }
1109
1110 function getArgument( $name ) {
1111 return false;
1112 }
1113
1114 /**
1115 * Returns true if the infinite loop check is OK, false if a loop is detected
1116 */
1117 function loopCheck( $title ) {
1118 return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] );
1119 }
1120
1121 /**
1122 * Return true if the frame is a template frame
1123 */
1124 function isTemplate() {
1125 return false;
1126 }
1127 }
1128
1129 /**
1130 * Expansion frame with template arguments
1131 * @ingroup Parser
1132 */
1133 class PPTemplateFrame_Hash extends PPFrame_Hash {
1134 var $numberedArgs, $namedArgs, $parent;
1135 var $numberedExpansionCache, $namedExpansionCache;
1136
1137 function __construct( $preprocessor, $parent = false, $numberedArgs = array(), $namedArgs = array(), $title = false ) {
1138 $this->preprocessor = $preprocessor;
1139 $this->parser = $preprocessor->parser;
1140 $this->parent = $parent;
1141 $this->numberedArgs = $numberedArgs;
1142 $this->namedArgs = $namedArgs;
1143 $this->title = $title;
1144 $pdbk = $title ? $title->getPrefixedDBkey() : false;
1145 $this->titleCache = $parent->titleCache;
1146 $this->titleCache[] = $pdbk;
1147 $this->loopCheckHash = /*clone*/ $parent->loopCheckHash;
1148 if ( $pdbk !== false ) {
1149 $this->loopCheckHash[$pdbk] = true;
1150 }
1151 $this->depth = $parent->depth + 1;
1152 $this->numberedExpansionCache = $this->namedExpansionCache = array();
1153 }
1154
1155 function __toString() {
1156 $s = 'tplframe{';
1157 $first = true;
1158 $args = $this->numberedArgs + $this->namedArgs;
1159 foreach ( $args as $name => $value ) {
1160 if ( $first ) {
1161 $first = false;
1162 } else {
1163 $s .= ', ';
1164 }
1165 $s .= "\"$name\":\"" .
1166 str_replace( '"', '\\"', $value->__toString() ) . '"';
1167 }
1168 $s .= '}';
1169 return $s;
1170 }
1171 /**
1172 * Returns true if there are no arguments in this frame
1173 */
1174 function isEmpty() {
1175 return !count( $this->numberedArgs ) && !count( $this->namedArgs );
1176 }
1177
1178 function getArguments() {
1179 $arguments = array();
1180 foreach ( array_merge(
1181 array_keys($this->numberedArgs),
1182 array_keys($this->namedArgs)) as $key ) {
1183 $arguments[$key] = $this->getArgument($key);
1184 }
1185 return $arguments;
1186 }
1187
1188 function getNumberedArguments() {
1189 $arguments = array();
1190 foreach ( array_keys($this->numberedArgs) as $key ) {
1191 $arguments[$key] = $this->getArgument($key);
1192 }
1193 return $arguments;
1194 }
1195
1196 function getNamedArguments() {
1197 $arguments = array();
1198 foreach ( array_keys($this->namedArgs) as $key ) {
1199 $arguments[$key] = $this->getArgument($key);
1200 }
1201 return $arguments;
1202 }
1203
1204 function getNumberedArgument( $index ) {
1205 if ( !isset( $this->numberedArgs[$index] ) ) {
1206 return false;
1207 }
1208 if ( !isset( $this->numberedExpansionCache[$index] ) ) {
1209 # No trimming for unnamed arguments
1210 $this->numberedExpansionCache[$index] = $this->parent->expand( $this->numberedArgs[$index], self::STRIP_COMMENTS );
1211 }
1212 return $this->numberedExpansionCache[$index];
1213 }
1214
1215 function getNamedArgument( $name ) {
1216 if ( !isset( $this->namedArgs[$name] ) ) {
1217 return false;
1218 }
1219 if ( !isset( $this->namedExpansionCache[$name] ) ) {
1220 # Trim named arguments post-expand, for backwards compatibility
1221 $this->namedExpansionCache[$name] = trim(
1222 $this->parent->expand( $this->namedArgs[$name], self::STRIP_COMMENTS ) );
1223 }
1224 return $this->namedExpansionCache[$name];
1225 }
1226
1227 function getArgument( $name ) {
1228 $text = $this->getNumberedArgument( $name );
1229 if ( $text === false ) {
1230 $text = $this->getNamedArgument( $name );
1231 }
1232 return $text;
1233 }
1234
1235 /**
1236 * Return true if the frame is a template frame
1237 */
1238 function isTemplate() {
1239 return true;
1240 }
1241 }
1242
1243 /**
1244 * Expansion frame with custom arguments
1245 * @ingroup Parser
1246 */
1247 class PPCustomFrame_Hash extends PPFrame_Hash {
1248 var $args;
1249
1250 function __construct( $preprocessor, $args ) {
1251 $this->preprocessor = $preprocessor;
1252 $this->parser = $preprocessor->parser;
1253 $this->args = $args;
1254 }
1255
1256 function __toString() {
1257 $s = 'cstmframe{';
1258 $first = true;
1259 foreach ( $this->args as $name => $value ) {
1260 if ( $first ) {
1261 $first = false;
1262 } else {
1263 $s .= ', ';
1264 }
1265 $s .= "\"$name\":\"" .
1266 str_replace( '"', '\\"', $value->__toString() ) . '"';
1267 }
1268 $s .= '}';
1269 return $s;
1270 }
1271
1272 function isEmpty() {
1273 return !count( $this->args );
1274 }
1275
1276 function getArgument( $index ) {
1277 if ( !isset( $this->args[$index] ) ) {
1278 return false;
1279 }
1280 return $this->args[$index];
1281 }
1282 }
1283
1284 /**
1285 * @ingroup Parser
1286 */
1287 class PPNode_Hash_Tree implements PPNode {
1288 var $name, $firstChild, $lastChild, $nextSibling;
1289
1290 function __construct( $name ) {
1291 $this->name = $name;
1292 $this->firstChild = $this->lastChild = $this->nextSibling = false;
1293 }
1294
1295 function __toString() {
1296 $inner = '';
1297 $attribs = '';
1298 for ( $node = $this->firstChild; $node; $node = $node->nextSibling ) {
1299 if ( $node instanceof PPNode_Hash_Attr ) {
1300 $attribs .= ' ' . $node->name . '="' . htmlspecialchars( $node->value ) . '"';
1301 } else {
1302 $inner .= $node->__toString();
1303 }
1304 }
1305 if ( $inner === '' ) {
1306 return "<{$this->name}$attribs/>";
1307 } else {
1308 return "<{$this->name}$attribs>$inner</{$this->name}>";
1309 }
1310 }
1311
1312 static function newWithText( $name, $text ) {
1313 $obj = new self( $name );
1314 $obj->addChild( new PPNode_Hash_Text( $text ) );
1315 return $obj;
1316 }
1317
1318 function addChild( $node ) {
1319 if ( $this->lastChild === false ) {
1320 $this->firstChild = $this->lastChild = $node;
1321 } else {
1322 $this->lastChild->nextSibling = $node;
1323 $this->lastChild = $node;
1324 }
1325 }
1326
1327 function getChildren() {
1328 $children = array();
1329 for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
1330 $children[] = $child;
1331 }
1332 return new PPNode_Hash_Array( $children );
1333 }
1334
1335 function getFirstChild() {
1336 return $this->firstChild;
1337 }
1338
1339 function getNextSibling() {
1340 return $this->nextSibling;
1341 }
1342
1343 function getChildrenOfType( $name ) {
1344 $children = array();
1345 for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
1346 if ( isset( $child->name ) && $child->name === $name ) {
1347 $children[] = $name;
1348 }
1349 }
1350 return $children;
1351 }
1352
1353 function getLength() { return false; }
1354 function item( $i ) { return false; }
1355
1356 function getName() {
1357 return $this->name;
1358 }
1359
1360 /**
1361 * Split a <part> node into an associative array containing:
1362 * name PPNode name
1363 * index String index
1364 * value PPNode value
1365 */
1366 function splitArg() {
1367 $bits = array();
1368 for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
1369 if ( !isset( $child->name ) ) {
1370 continue;
1371 }
1372 if ( $child->name === 'name' ) {
1373 $bits['name'] = $child;
1374 if ( $child->firstChild instanceof PPNode_Hash_Attr
1375 && $child->firstChild->name === 'index' )
1376 {
1377 $bits['index'] = $child->firstChild->value;
1378 }
1379 } elseif ( $child->name === 'value' ) {
1380 $bits['value'] = $child;
1381 }
1382 }
1383
1384 if ( !isset( $bits['name'] ) ) {
1385 throw new MWException( 'Invalid brace node passed to ' . __METHOD__ );
1386 }
1387 if ( !isset( $bits['index'] ) ) {
1388 $bits['index'] = '';
1389 }
1390 return $bits;
1391 }
1392
1393 /**
1394 * Split an <ext> node into an associative array containing name, attr, inner and close
1395 * All values in the resulting array are PPNodes. Inner and close are optional.
1396 */
1397 function splitExt() {
1398 $bits = array();
1399 for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
1400 if ( !isset( $child->name ) ) {
1401 continue;
1402 }
1403 if ( $child->name == 'name' ) {
1404 $bits['name'] = $child;
1405 } elseif ( $child->name == 'attr' ) {
1406 $bits['attr'] = $child;
1407 } elseif ( $child->name == 'inner' ) {
1408 $bits['inner'] = $child;
1409 } elseif ( $child->name == 'close' ) {
1410 $bits['close'] = $child;
1411 }
1412 }
1413 if ( !isset( $bits['name'] ) ) {
1414 throw new MWException( 'Invalid ext node passed to ' . __METHOD__ );
1415 }
1416 return $bits;
1417 }
1418
1419 /**
1420 * Split an <h> node
1421 */
1422 function splitHeading() {
1423 if ( $this->name !== 'h' ) {
1424 throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
1425 }
1426 $bits = array();
1427 for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
1428 if ( !isset( $child->name ) ) {
1429 continue;
1430 }
1431 if ( $child->name == 'i' ) {
1432 $bits['i'] = $child->value;
1433 } elseif ( $child->name == 'level' ) {
1434 $bits['level'] = $child->value;
1435 }
1436 }
1437 if ( !isset( $bits['i'] ) ) {
1438 throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
1439 }
1440 return $bits;
1441 }
1442
1443 /**
1444 * Split a <template> or <tplarg> node
1445 */
1446 function splitTemplate() {
1447 $parts = array();
1448 $bits = array( 'lineStart' => '' );
1449 for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
1450 if ( !isset( $child->name ) ) {
1451 continue;
1452 }
1453 if ( $child->name == 'title' ) {
1454 $bits['title'] = $child;
1455 }
1456 if ( $child->name == 'part' ) {
1457 $parts[] = $child;
1458 }
1459 if ( $child->name == 'lineStart' ) {
1460 $bits['lineStart'] = '1';
1461 }
1462 }
1463 if ( !isset( $bits['title'] ) ) {
1464 throw new MWException( 'Invalid node passed to ' . __METHOD__ );
1465 }
1466 $bits['parts'] = new PPNode_Hash_Array( $parts );
1467 return $bits;
1468 }
1469 }
1470
1471 /**
1472 * @ingroup Parser
1473 */
1474 class PPNode_Hash_Text implements PPNode {
1475 var $value, $nextSibling;
1476
1477 function __construct( $value ) {
1478 if ( is_object( $value ) ) {
1479 throw new MWException( __CLASS__ . ' given object instead of string' );
1480 }
1481 $this->value = $value;
1482 }
1483
1484 function __toString() {
1485 return htmlspecialchars( $this->value );
1486 }
1487
1488 function getNextSibling() {
1489 return $this->nextSibling;
1490 }
1491
1492 function getChildren() { return false; }
1493 function getFirstChild() { return false; }
1494 function getChildrenOfType( $name ) { return false; }
1495 function getLength() { return false; }
1496 function item( $i ) { return false; }
1497 function getName() { return '#text'; }
1498 function splitArg() { throw new MWException( __METHOD__ . ': not supported' ); }
1499 function splitExt() { throw new MWException( __METHOD__ . ': not supported' ); }
1500 function splitHeading() { throw new MWException( __METHOD__ . ': not supported' ); }
1501 }
1502
1503 /**
1504 * @ingroup Parser
1505 */
1506 class PPNode_Hash_Array implements PPNode {
1507 var $value, $nextSibling;
1508
1509 function __construct( $value ) {
1510 $this->value = $value;
1511 }
1512
1513 function __toString() {
1514 return var_export( $this, true );
1515 }
1516
1517 function getLength() {
1518 return count( $this->value );
1519 }
1520
1521 function item( $i ) {
1522 return $this->value[$i];
1523 }
1524
1525 function getName() { return '#nodelist'; }
1526
1527 function getNextSibling() {
1528 return $this->nextSibling;
1529 }
1530
1531 function getChildren() { return false; }
1532 function getFirstChild() { return false; }
1533 function getChildrenOfType( $name ) { return false; }
1534 function splitArg() { throw new MWException( __METHOD__ . ': not supported' ); }
1535 function splitExt() { throw new MWException( __METHOD__ . ': not supported' ); }
1536 function splitHeading() { throw new MWException( __METHOD__ . ': not supported' ); }
1537 }
1538
1539 /**
1540 * @ingroup Parser
1541 */
1542 class PPNode_Hash_Attr implements PPNode {
1543 var $name, $value, $nextSibling;
1544
1545 function __construct( $name, $value ) {
1546 $this->name = $name;
1547 $this->value = $value;
1548 }
1549
1550 function __toString() {
1551 return "<@{$this->name}>" . htmlspecialchars( $this->value ) . "</@{$this->name}>";
1552 }
1553
1554 function getName() {
1555 return $this->name;
1556 }
1557
1558 function getNextSibling() {
1559 return $this->nextSibling;
1560 }
1561
1562 function getChildren() { return false; }
1563 function getFirstChild() { return false; }
1564 function getChildrenOfType( $name ) { return false; }
1565 function getLength() { return false; }
1566 function item( $i ) { return false; }
1567 function splitArg() { throw new MWException( __METHOD__ . ': not supported' ); }
1568 function splitExt() { throw new MWException( __METHOD__ . ': not supported' ); }
1569 function splitHeading() { throw new MWException( __METHOD__ . ': not supported' ); }
1570 }