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