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