MediaWiki
REL1_23
|
00001 <?php 00030 class Preprocessor_Hash implements Preprocessor { 00034 var $parser; 00035 00036 const CACHE_VERSION = 1; 00037 00038 function __construct( $parser ) { 00039 $this->parser = $parser; 00040 } 00041 00045 function newFrame() { 00046 return new PPFrame_Hash( $this ); 00047 } 00048 00053 function newCustomFrame( $args ) { 00054 return new PPCustomFrame_Hash( $this, $args ); 00055 } 00056 00061 function newPartNodeArray( $values ) { 00062 $list = array(); 00063 00064 foreach ( $values as $k => $val ) { 00065 $partNode = new PPNode_Hash_Tree( 'part' ); 00066 $nameNode = new PPNode_Hash_Tree( 'name' ); 00067 00068 if ( is_int( $k ) ) { 00069 $nameNode->addChild( new PPNode_Hash_Attr( 'index', $k ) ); 00070 $partNode->addChild( $nameNode ); 00071 } else { 00072 $nameNode->addChild( new PPNode_Hash_Text( $k ) ); 00073 $partNode->addChild( $nameNode ); 00074 $partNode->addChild( new PPNode_Hash_Text( '=' ) ); 00075 } 00076 00077 $valueNode = new PPNode_Hash_Tree( 'value' ); 00078 $valueNode->addChild( new PPNode_Hash_Text( $val ) ); 00079 $partNode->addChild( $valueNode ); 00080 00081 $list[] = $partNode; 00082 } 00083 00084 $node = new PPNode_Hash_Array( $list ); 00085 return $node; 00086 } 00087 00111 function preprocessToObj( $text, $flags = 0 ) { 00112 wfProfileIn( __METHOD__ ); 00113 00114 // Check cache. 00115 global $wgMemc, $wgPreprocessorCacheThreshold; 00116 00117 $cacheable = $wgPreprocessorCacheThreshold !== false && strlen( $text ) > $wgPreprocessorCacheThreshold; 00118 if ( $cacheable ) { 00119 wfProfileIn( __METHOD__ . '-cacheable' ); 00120 00121 $cacheKey = wfMemcKey( 'preprocess-hash', md5( $text ), $flags ); 00122 $cacheValue = $wgMemc->get( $cacheKey ); 00123 if ( $cacheValue ) { 00124 $version = substr( $cacheValue, 0, 8 ); 00125 if ( intval( $version ) == self::CACHE_VERSION ) { 00126 $hash = unserialize( substr( $cacheValue, 8 ) ); 00127 // From the cache 00128 wfDebugLog( "Preprocessor", 00129 "Loaded preprocessor hash from memcached (key $cacheKey)" ); 00130 wfProfileOut( __METHOD__ . '-cacheable' ); 00131 wfProfileOut( __METHOD__ ); 00132 return $hash; 00133 } 00134 } 00135 wfProfileIn( __METHOD__ . '-cache-miss' ); 00136 } 00137 00138 $rules = array( 00139 '{' => array( 00140 'end' => '}', 00141 'names' => array( 00142 2 => 'template', 00143 3 => 'tplarg', 00144 ), 00145 'min' => 2, 00146 'max' => 3, 00147 ), 00148 '[' => array( 00149 'end' => ']', 00150 'names' => array( 2 => null ), 00151 'min' => 2, 00152 'max' => 2, 00153 ) 00154 ); 00155 00156 $forInclusion = $flags & Parser::PTD_FOR_INCLUSION; 00157 00158 $xmlishElements = $this->parser->getStripList(); 00159 $enableOnlyinclude = false; 00160 if ( $forInclusion ) { 00161 $ignoredTags = array( 'includeonly', '/includeonly' ); 00162 $ignoredElements = array( 'noinclude' ); 00163 $xmlishElements[] = 'noinclude'; 00164 if ( strpos( $text, '<onlyinclude>' ) !== false && strpos( $text, '</onlyinclude>' ) !== false ) { 00165 $enableOnlyinclude = true; 00166 } 00167 } else { 00168 $ignoredTags = array( 'noinclude', '/noinclude', 'onlyinclude', '/onlyinclude' ); 00169 $ignoredElements = array( 'includeonly' ); 00170 $xmlishElements[] = 'includeonly'; 00171 } 00172 $xmlishRegex = implode( '|', array_merge( $xmlishElements, $ignoredTags ) ); 00173 00174 // Use "A" modifier (anchored) instead of "^", because ^ doesn't work with an offset 00175 $elementsRegex = "~($xmlishRegex)(?:\s|\/>|>)|(!--)~iA"; 00176 00177 $stack = new PPDStack_Hash; 00178 00179 $searchBase = "[{<\n"; 00180 $revText = strrev( $text ); // For fast reverse searches 00181 $lengthText = strlen( $text ); 00182 00183 $i = 0; # Input pointer, starts out pointing to a pseudo-newline before the start 00184 $accum =& $stack->getAccum(); # Current accumulator 00185 $findEquals = false; # True to find equals signs in arguments 00186 $findPipe = false; # True to take notice of pipe characters 00187 $headingIndex = 1; 00188 $inHeading = false; # True if $i is inside a possible heading 00189 $noMoreGT = false; # True if there are no more greater-than (>) signs right of $i 00190 $findOnlyinclude = $enableOnlyinclude; # True to ignore all input up to the next <onlyinclude> 00191 $fakeLineStart = true; # Do a line-start run without outputting an LF character 00192 00193 while ( true ) { 00194 //$this->memCheck(); 00195 00196 if ( $findOnlyinclude ) { 00197 // Ignore all input up to the next <onlyinclude> 00198 $startPos = strpos( $text, '<onlyinclude>', $i ); 00199 if ( $startPos === false ) { 00200 // Ignored section runs to the end 00201 $accum->addNodeWithText( 'ignore', substr( $text, $i ) ); 00202 break; 00203 } 00204 $tagEndPos = $startPos + strlen( '<onlyinclude>' ); // past-the-end 00205 $accum->addNodeWithText( 'ignore', substr( $text, $i, $tagEndPos - $i ) ); 00206 $i = $tagEndPos; 00207 $findOnlyinclude = false; 00208 } 00209 00210 if ( $fakeLineStart ) { 00211 $found = 'line-start'; 00212 $curChar = ''; 00213 } else { 00214 # Find next opening brace, closing brace or pipe 00215 $search = $searchBase; 00216 if ( $stack->top === false ) { 00217 $currentClosing = ''; 00218 } else { 00219 $currentClosing = $stack->top->close; 00220 $search .= $currentClosing; 00221 } 00222 if ( $findPipe ) { 00223 $search .= '|'; 00224 } 00225 if ( $findEquals ) { 00226 // First equals will be for the template 00227 $search .= '='; 00228 } 00229 $rule = null; 00230 # Output literal section, advance input counter 00231 $literalLength = strcspn( $text, $search, $i ); 00232 if ( $literalLength > 0 ) { 00233 $accum->addLiteral( substr( $text, $i, $literalLength ) ); 00234 $i += $literalLength; 00235 } 00236 if ( $i >= $lengthText ) { 00237 if ( $currentClosing == "\n" ) { 00238 // Do a past-the-end run to finish off the heading 00239 $curChar = ''; 00240 $found = 'line-end'; 00241 } else { 00242 # All done 00243 break; 00244 } 00245 } else { 00246 $curChar = $text[$i]; 00247 if ( $curChar == '|' ) { 00248 $found = 'pipe'; 00249 } elseif ( $curChar == '=' ) { 00250 $found = 'equals'; 00251 } elseif ( $curChar == '<' ) { 00252 $found = 'angle'; 00253 } elseif ( $curChar == "\n" ) { 00254 if ( $inHeading ) { 00255 $found = 'line-end'; 00256 } else { 00257 $found = 'line-start'; 00258 } 00259 } elseif ( $curChar == $currentClosing ) { 00260 $found = 'close'; 00261 } elseif ( isset( $rules[$curChar] ) ) { 00262 $found = 'open'; 00263 $rule = $rules[$curChar]; 00264 } else { 00265 # Some versions of PHP have a strcspn which stops on null characters 00266 # Ignore and continue 00267 ++$i; 00268 continue; 00269 } 00270 } 00271 } 00272 00273 if ( $found == 'angle' ) { 00274 $matches = false; 00275 // Handle </onlyinclude> 00276 if ( $enableOnlyinclude && substr( $text, $i, strlen( '</onlyinclude>' ) ) == '</onlyinclude>' ) { 00277 $findOnlyinclude = true; 00278 continue; 00279 } 00280 00281 // Determine element name 00282 if ( !preg_match( $elementsRegex, $text, $matches, 0, $i + 1 ) ) { 00283 // Element name missing or not listed 00284 $accum->addLiteral( '<' ); 00285 ++$i; 00286 continue; 00287 } 00288 // Handle comments 00289 if ( isset( $matches[2] ) && $matches[2] == '!--' ) { 00290 00291 // To avoid leaving blank lines, when a sequence of 00292 // space-separated comments is both preceded and followed by 00293 // a newline (ignoring spaces), then 00294 // trim leading and trailing spaces and the trailing newline. 00295 00296 // Find the end 00297 $endPos = strpos( $text, '-->', $i + 4 ); 00298 if ( $endPos === false ) { 00299 // Unclosed comment in input, runs to end 00300 $inner = substr( $text, $i ); 00301 $accum->addNodeWithText( 'comment', $inner ); 00302 $i = $lengthText; 00303 } else { 00304 // Search backwards for leading whitespace 00305 $wsStart = $i ? ( $i - strspn( $revText, " \t", $lengthText - $i ) ) : 0; 00306 00307 // Search forwards for trailing whitespace 00308 // $wsEnd will be the position of the last space (or the '>' if there's none) 00309 $wsEnd = $endPos + 2 + strspn( $text, " \t", $endPos + 3 ); 00310 00311 // Keep looking forward as long as we're finding more 00312 // comments. 00313 $comments = array( array( $wsStart, $wsEnd ) ); 00314 while ( substr( $text, $wsEnd + 1, 4 ) == '<!--' ) { 00315 $c = strpos( $text, '-->', $wsEnd + 4 ); 00316 if ( $c === false ) { 00317 break; 00318 } 00319 $c = $c + 2 + strspn( $text, " \t", $c + 3 ); 00320 $comments[] = array( $wsEnd + 1, $c ); 00321 $wsEnd = $c; 00322 } 00323 00324 // Eat the line if possible 00325 // TODO: This could theoretically be done if $wsStart == 0, i.e. for comments at 00326 // the overall start. That's not how Sanitizer::removeHTMLcomments() did it, but 00327 // it's a possible beneficial b/c break. 00328 if ( $wsStart > 0 && substr( $text, $wsStart - 1, 1 ) == "\n" 00329 && substr( $text, $wsEnd + 1, 1 ) == "\n" 00330 ) { 00331 // Remove leading whitespace from the end of the accumulator 00332 // Sanity check first though 00333 $wsLength = $i - $wsStart; 00334 if ( $wsLength > 0 00335 && $accum->lastNode instanceof PPNode_Hash_Text 00336 && strspn( $accum->lastNode->value, " \t", -$wsLength ) === $wsLength 00337 ) { 00338 $accum->lastNode->value = substr( $accum->lastNode->value, 0, -$wsLength ); 00339 } 00340 00341 // Dump all but the last comment to the accumulator 00342 foreach ( $comments as $j => $com ) { 00343 $startPos = $com[0]; 00344 $endPos = $com[1] + 1; 00345 if ( $j == ( count( $comments ) - 1 ) ) { 00346 break; 00347 } 00348 $inner = substr( $text, $startPos, $endPos - $startPos ); 00349 $accum->addNodeWithText( 'comment', $inner ); 00350 } 00351 00352 // Do a line-start run next time to look for headings after the comment 00353 $fakeLineStart = true; 00354 } else { 00355 // No line to eat, just take the comment itself 00356 $startPos = $i; 00357 $endPos += 2; 00358 } 00359 00360 if ( $stack->top ) { 00361 $part = $stack->top->getCurrentPart(); 00362 if ( !( isset( $part->commentEnd ) && $part->commentEnd == $wsStart - 1 ) ) { 00363 $part->visualEnd = $wsStart; 00364 } 00365 // Else comments abutting, no change in visual end 00366 $part->commentEnd = $endPos; 00367 } 00368 $i = $endPos + 1; 00369 $inner = substr( $text, $startPos, $endPos - $startPos + 1 ); 00370 $accum->addNodeWithText( 'comment', $inner ); 00371 } 00372 continue; 00373 } 00374 $name = $matches[1]; 00375 $lowerName = strtolower( $name ); 00376 $attrStart = $i + strlen( $name ) + 1; 00377 00378 // Find end of tag 00379 $tagEndPos = $noMoreGT ? false : strpos( $text, '>', $attrStart ); 00380 if ( $tagEndPos === false ) { 00381 // Infinite backtrack 00382 // Disable tag search to prevent worst-case O(N^2) performance 00383 $noMoreGT = true; 00384 $accum->addLiteral( '<' ); 00385 ++$i; 00386 continue; 00387 } 00388 00389 // Handle ignored tags 00390 if ( in_array( $lowerName, $ignoredTags ) ) { 00391 $accum->addNodeWithText( 'ignore', substr( $text, $i, $tagEndPos - $i + 1 ) ); 00392 $i = $tagEndPos + 1; 00393 continue; 00394 } 00395 00396 $tagStartPos = $i; 00397 if ( $text[$tagEndPos - 1] == '/' ) { 00398 // Short end tag 00399 $attrEnd = $tagEndPos - 1; 00400 $inner = null; 00401 $i = $tagEndPos + 1; 00402 $close = null; 00403 } else { 00404 $attrEnd = $tagEndPos; 00405 // Find closing tag 00406 if ( preg_match( "/<\/" . preg_quote( $name, '/' ) . "\s*>/i", 00407 $text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 ) 00408 ) { 00409 $inner = substr( $text, $tagEndPos + 1, $matches[0][1] - $tagEndPos - 1 ); 00410 $i = $matches[0][1] + strlen( $matches[0][0] ); 00411 $close = $matches[0][0]; 00412 } else { 00413 // No end tag -- let it run out to the end of the text. 00414 $inner = substr( $text, $tagEndPos + 1 ); 00415 $i = $lengthText; 00416 $close = null; 00417 } 00418 } 00419 // <includeonly> and <noinclude> just become <ignore> tags 00420 if ( in_array( $lowerName, $ignoredElements ) ) { 00421 $accum->addNodeWithText( 'ignore', substr( $text, $tagStartPos, $i - $tagStartPos ) ); 00422 continue; 00423 } 00424 00425 if ( $attrEnd <= $attrStart ) { 00426 $attr = ''; 00427 } else { 00428 // Note that the attr element contains the whitespace between name and attribute, 00429 // this is necessary for precise reconstruction during pre-save transform. 00430 $attr = substr( $text, $attrStart, $attrEnd - $attrStart ); 00431 } 00432 00433 $extNode = new PPNode_Hash_Tree( 'ext' ); 00434 $extNode->addChild( PPNode_Hash_Tree::newWithText( 'name', $name ) ); 00435 $extNode->addChild( PPNode_Hash_Tree::newWithText( 'attr', $attr ) ); 00436 if ( $inner !== null ) { 00437 $extNode->addChild( PPNode_Hash_Tree::newWithText( 'inner', $inner ) ); 00438 } 00439 if ( $close !== null ) { 00440 $extNode->addChild( PPNode_Hash_Tree::newWithText( 'close', $close ) ); 00441 } 00442 $accum->addNode( $extNode ); 00443 } elseif ( $found == 'line-start' ) { 00444 // Is this the start of a heading? 00445 // Line break belongs before the heading element in any case 00446 if ( $fakeLineStart ) { 00447 $fakeLineStart = false; 00448 } else { 00449 $accum->addLiteral( $curChar ); 00450 $i++; 00451 } 00452 00453 $count = strspn( $text, '=', $i, 6 ); 00454 if ( $count == 1 && $findEquals ) { 00455 // DWIM: This looks kind of like a name/value separator 00456 // Let's let the equals handler have it and break the potential heading 00457 // This is heuristic, but AFAICT the methods for completely correct disambiguation are very complex. 00458 } elseif ( $count > 0 ) { 00459 $piece = array( 00460 'open' => "\n", 00461 'close' => "\n", 00462 'parts' => array( new PPDPart_Hash( str_repeat( '=', $count ) ) ), 00463 'startPos' => $i, 00464 'count' => $count ); 00465 $stack->push( $piece ); 00466 $accum =& $stack->getAccum(); 00467 extract( $stack->getFlags() ); 00468 $i += $count; 00469 } 00470 } elseif ( $found == 'line-end' ) { 00471 $piece = $stack->top; 00472 // A heading must be open, otherwise \n wouldn't have been in the search list 00473 assert( '$piece->open == "\n"' ); 00474 $part = $piece->getCurrentPart(); 00475 // Search back through the input to see if it has a proper close 00476 // Do this using the reversed string since the other solutions (end anchor, etc.) are inefficient 00477 $wsLength = strspn( $revText, " \t", $lengthText - $i ); 00478 $searchStart = $i - $wsLength; 00479 if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) { 00480 // Comment found at line end 00481 // Search for equals signs before the comment 00482 $searchStart = $part->visualEnd; 00483 $searchStart -= strspn( $revText, " \t", $lengthText - $searchStart ); 00484 } 00485 $count = $piece->count; 00486 $equalsLength = strspn( $revText, '=', $lengthText - $searchStart ); 00487 if ( $equalsLength > 0 ) { 00488 if ( $searchStart - $equalsLength == $piece->startPos ) { 00489 // This is just a single string of equals signs on its own line 00490 // Replicate the doHeadings behavior /={count}(.+)={count}/ 00491 // First find out how many equals signs there really are (don't stop at 6) 00492 $count = $equalsLength; 00493 if ( $count < 3 ) { 00494 $count = 0; 00495 } else { 00496 $count = min( 6, intval( ( $count - 1 ) / 2 ) ); 00497 } 00498 } else { 00499 $count = min( $equalsLength, $count ); 00500 } 00501 if ( $count > 0 ) { 00502 // Normal match, output <h> 00503 $element = new PPNode_Hash_Tree( 'possible-h' ); 00504 $element->addChild( new PPNode_Hash_Attr( 'level', $count ) ); 00505 $element->addChild( new PPNode_Hash_Attr( 'i', $headingIndex++ ) ); 00506 $element->lastChild->nextSibling = $accum->firstNode; 00507 $element->lastChild = $accum->lastNode; 00508 } else { 00509 // Single equals sign on its own line, count=0 00510 $element = $accum; 00511 } 00512 } else { 00513 // No match, no <h>, just pass down the inner text 00514 $element = $accum; 00515 } 00516 // Unwind the stack 00517 $stack->pop(); 00518 $accum =& $stack->getAccum(); 00519 extract( $stack->getFlags() ); 00520 00521 // Append the result to the enclosing accumulator 00522 if ( $element instanceof PPNode ) { 00523 $accum->addNode( $element ); 00524 } else { 00525 $accum->addAccum( $element ); 00526 } 00527 // Note that we do NOT increment the input pointer. 00528 // This is because the closing linebreak could be the opening linebreak of 00529 // another heading. Infinite loops are avoided because the next iteration MUST 00530 // hit the heading open case above, which unconditionally increments the 00531 // input pointer. 00532 } elseif ( $found == 'open' ) { 00533 # count opening brace characters 00534 $count = strspn( $text, $curChar, $i ); 00535 00536 # we need to add to stack only if opening brace count is enough for one of the rules 00537 if ( $count >= $rule['min'] ) { 00538 # Add it to the stack 00539 $piece = array( 00540 'open' => $curChar, 00541 'close' => $rule['end'], 00542 'count' => $count, 00543 'lineStart' => ( $i > 0 && $text[$i - 1] == "\n" ), 00544 ); 00545 00546 $stack->push( $piece ); 00547 $accum =& $stack->getAccum(); 00548 extract( $stack->getFlags() ); 00549 } else { 00550 # Add literal brace(s) 00551 $accum->addLiteral( str_repeat( $curChar, $count ) ); 00552 } 00553 $i += $count; 00554 } elseif ( $found == 'close' ) { 00555 $piece = $stack->top; 00556 # lets check if there are enough characters for closing brace 00557 $maxCount = $piece->count; 00558 $count = strspn( $text, $curChar, $i, $maxCount ); 00559 00560 # check for maximum matching characters (if there are 5 closing 00561 # characters, we will probably need only 3 - depending on the rules) 00562 $rule = $rules[$piece->open]; 00563 if ( $count > $rule['max'] ) { 00564 # The specified maximum exists in the callback array, unless the caller 00565 # has made an error 00566 $matchingCount = $rule['max']; 00567 } else { 00568 # Count is less than the maximum 00569 # Skip any gaps in the callback array to find the true largest match 00570 # Need to use array_key_exists not isset because the callback can be null 00571 $matchingCount = $count; 00572 while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $rule['names'] ) ) { 00573 --$matchingCount; 00574 } 00575 } 00576 00577 if ( $matchingCount <= 0 ) { 00578 # No matching element found in callback array 00579 # Output a literal closing brace and continue 00580 $accum->addLiteral( str_repeat( $curChar, $count ) ); 00581 $i += $count; 00582 continue; 00583 } 00584 $name = $rule['names'][$matchingCount]; 00585 if ( $name === null ) { 00586 // No element, just literal text 00587 $element = $piece->breakSyntax( $matchingCount ); 00588 $element->addLiteral( str_repeat( $rule['end'], $matchingCount ) ); 00589 } else { 00590 # Create XML element 00591 # Note: $parts is already XML, does not need to be encoded further 00592 $parts = $piece->parts; 00593 $titleAccum = $parts[0]->out; 00594 unset( $parts[0] ); 00595 00596 $element = new PPNode_Hash_Tree( $name ); 00597 00598 # The invocation is at the start of the line if lineStart is set in 00599 # the stack, and all opening brackets are used up. 00600 if ( $maxCount == $matchingCount && !empty( $piece->lineStart ) ) { 00601 $element->addChild( new PPNode_Hash_Attr( 'lineStart', 1 ) ); 00602 } 00603 $titleNode = new PPNode_Hash_Tree( 'title' ); 00604 $titleNode->firstChild = $titleAccum->firstNode; 00605 $titleNode->lastChild = $titleAccum->lastNode; 00606 $element->addChild( $titleNode ); 00607 $argIndex = 1; 00608 foreach ( $parts as $part ) { 00609 if ( isset( $part->eqpos ) ) { 00610 // Find equals 00611 $lastNode = false; 00612 for ( $node = $part->out->firstNode; $node; $node = $node->nextSibling ) { 00613 if ( $node === $part->eqpos ) { 00614 break; 00615 } 00616 $lastNode = $node; 00617 } 00618 if ( !$node ) { 00619 if ( $cacheable ) { 00620 wfProfileOut( __METHOD__ . '-cache-miss' ); 00621 wfProfileOut( __METHOD__ . '-cacheable' ); 00622 } 00623 wfProfileOut( __METHOD__ ); 00624 throw new MWException( __METHOD__ . ': eqpos not found' ); 00625 } 00626 if ( $node->name !== 'equals' ) { 00627 if ( $cacheable ) { 00628 wfProfileOut( __METHOD__ . '-cache-miss' ); 00629 wfProfileOut( __METHOD__ . '-cacheable' ); 00630 } 00631 wfProfileOut( __METHOD__ ); 00632 throw new MWException( __METHOD__ . ': eqpos is not equals' ); 00633 } 00634 $equalsNode = $node; 00635 00636 // Construct name node 00637 $nameNode = new PPNode_Hash_Tree( 'name' ); 00638 if ( $lastNode !== false ) { 00639 $lastNode->nextSibling = false; 00640 $nameNode->firstChild = $part->out->firstNode; 00641 $nameNode->lastChild = $lastNode; 00642 } 00643 00644 // Construct value node 00645 $valueNode = new PPNode_Hash_Tree( 'value' ); 00646 if ( $equalsNode->nextSibling !== false ) { 00647 $valueNode->firstChild = $equalsNode->nextSibling; 00648 $valueNode->lastChild = $part->out->lastNode; 00649 } 00650 $partNode = new PPNode_Hash_Tree( 'part' ); 00651 $partNode->addChild( $nameNode ); 00652 $partNode->addChild( $equalsNode->firstChild ); 00653 $partNode->addChild( $valueNode ); 00654 $element->addChild( $partNode ); 00655 } else { 00656 $partNode = new PPNode_Hash_Tree( 'part' ); 00657 $nameNode = new PPNode_Hash_Tree( 'name' ); 00658 $nameNode->addChild( new PPNode_Hash_Attr( 'index', $argIndex++ ) ); 00659 $valueNode = new PPNode_Hash_Tree( 'value' ); 00660 $valueNode->firstChild = $part->out->firstNode; 00661 $valueNode->lastChild = $part->out->lastNode; 00662 $partNode->addChild( $nameNode ); 00663 $partNode->addChild( $valueNode ); 00664 $element->addChild( $partNode ); 00665 } 00666 } 00667 } 00668 00669 # Advance input pointer 00670 $i += $matchingCount; 00671 00672 # Unwind the stack 00673 $stack->pop(); 00674 $accum =& $stack->getAccum(); 00675 00676 # Re-add the old stack element if it still has unmatched opening characters remaining 00677 if ( $matchingCount < $piece->count ) { 00678 $piece->parts = array( new PPDPart_Hash ); 00679 $piece->count -= $matchingCount; 00680 # do we still qualify for any callback with remaining count? 00681 $min = $rules[$piece->open]['min']; 00682 if ( $piece->count >= $min ) { 00683 $stack->push( $piece ); 00684 $accum =& $stack->getAccum(); 00685 } else { 00686 $accum->addLiteral( str_repeat( $piece->open, $piece->count ) ); 00687 } 00688 } 00689 00690 extract( $stack->getFlags() ); 00691 00692 # Add XML element to the enclosing accumulator 00693 if ( $element instanceof PPNode ) { 00694 $accum->addNode( $element ); 00695 } else { 00696 $accum->addAccum( $element ); 00697 } 00698 } elseif ( $found == 'pipe' ) { 00699 $findEquals = true; // shortcut for getFlags() 00700 $stack->addPart(); 00701 $accum =& $stack->getAccum(); 00702 ++$i; 00703 } elseif ( $found == 'equals' ) { 00704 $findEquals = false; // shortcut for getFlags() 00705 $accum->addNodeWithText( 'equals', '=' ); 00706 $stack->getCurrentPart()->eqpos = $accum->lastNode; 00707 ++$i; 00708 } 00709 } 00710 00711 # Output any remaining unclosed brackets 00712 foreach ( $stack->stack as $piece ) { 00713 $stack->rootAccum->addAccum( $piece->breakSyntax() ); 00714 } 00715 00716 # Enable top-level headings 00717 for ( $node = $stack->rootAccum->firstNode; $node; $node = $node->nextSibling ) { 00718 if ( isset( $node->name ) && $node->name === 'possible-h' ) { 00719 $node->name = 'h'; 00720 } 00721 } 00722 00723 $rootNode = new PPNode_Hash_Tree( 'root' ); 00724 $rootNode->firstChild = $stack->rootAccum->firstNode; 00725 $rootNode->lastChild = $stack->rootAccum->lastNode; 00726 00727 // Cache 00728 if ( $cacheable ) { 00729 $cacheValue = sprintf( "%08d", self::CACHE_VERSION ) . serialize( $rootNode ); 00730 $wgMemc->set( $cacheKey, $cacheValue, 86400 ); 00731 wfProfileOut( __METHOD__ . '-cache-miss' ); 00732 wfProfileOut( __METHOD__ . '-cacheable' ); 00733 wfDebugLog( "Preprocessor", "Saved preprocessor Hash to memcached (key $cacheKey)" ); 00734 } 00735 00736 wfProfileOut( __METHOD__ ); 00737 return $rootNode; 00738 } 00739 } 00740 00745 class PPDStack_Hash extends PPDStack { 00746 function __construct() { 00747 $this->elementClass = 'PPDStackElement_Hash'; 00748 parent::__construct(); 00749 $this->rootAccum = new PPDAccum_Hash; 00750 } 00751 } 00752 00756 class PPDStackElement_Hash extends PPDStackElement { 00757 function __construct( $data = array() ) { 00758 $this->partClass = 'PPDPart_Hash'; 00759 parent::__construct( $data ); 00760 } 00761 00767 function breakSyntax( $openingCount = false ) { 00768 if ( $this->open == "\n" ) { 00769 $accum = $this->parts[0]->out; 00770 } else { 00771 if ( $openingCount === false ) { 00772 $openingCount = $this->count; 00773 } 00774 $accum = new PPDAccum_Hash; 00775 $accum->addLiteral( str_repeat( $this->open, $openingCount ) ); 00776 $first = true; 00777 foreach ( $this->parts as $part ) { 00778 if ( $first ) { 00779 $first = false; 00780 } else { 00781 $accum->addLiteral( '|' ); 00782 } 00783 $accum->addAccum( $part->out ); 00784 } 00785 } 00786 return $accum; 00787 } 00788 } 00789 00793 class PPDPart_Hash extends PPDPart { 00794 function __construct( $out = '' ) { 00795 $accum = new PPDAccum_Hash; 00796 if ( $out !== '' ) { 00797 $accum->addLiteral( $out ); 00798 } 00799 parent::__construct( $accum ); 00800 } 00801 } 00802 00806 class PPDAccum_Hash { 00807 var $firstNode, $lastNode; 00808 00809 function __construct() { 00810 $this->firstNode = $this->lastNode = false; 00811 } 00812 00816 function addLiteral( $s ) { 00817 if ( $this->lastNode === false ) { 00818 $this->firstNode = $this->lastNode = new PPNode_Hash_Text( $s ); 00819 } elseif ( $this->lastNode instanceof PPNode_Hash_Text ) { 00820 $this->lastNode->value .= $s; 00821 } else { 00822 $this->lastNode->nextSibling = new PPNode_Hash_Text( $s ); 00823 $this->lastNode = $this->lastNode->nextSibling; 00824 } 00825 } 00826 00830 function addNode( PPNode $node ) { 00831 if ( $this->lastNode === false ) { 00832 $this->firstNode = $this->lastNode = $node; 00833 } else { 00834 $this->lastNode->nextSibling = $node; 00835 $this->lastNode = $node; 00836 } 00837 } 00838 00842 function addNodeWithText( $name, $value ) { 00843 $node = PPNode_Hash_Tree::newWithText( $name, $value ); 00844 $this->addNode( $node ); 00845 } 00846 00852 function addAccum( $accum ) { 00853 if ( $accum->lastNode === false ) { 00854 // nothing to add 00855 } elseif ( $this->lastNode === false ) { 00856 $this->firstNode = $accum->firstNode; 00857 $this->lastNode = $accum->lastNode; 00858 } else { 00859 $this->lastNode->nextSibling = $accum->firstNode; 00860 $this->lastNode = $accum->lastNode; 00861 } 00862 } 00863 } 00864 00869 class PPFrame_Hash implements PPFrame { 00870 00874 var $parser; 00875 00879 var $preprocessor; 00880 00884 var $title; 00885 var $titleCache; 00886 00891 var $loopCheckHash; 00892 00897 var $depth; 00898 00903 function __construct( $preprocessor ) { 00904 $this->preprocessor = $preprocessor; 00905 $this->parser = $preprocessor->parser; 00906 $this->title = $this->parser->mTitle; 00907 $this->titleCache = array( $this->title ? $this->title->getPrefixedDBkey() : false ); 00908 $this->loopCheckHash = array(); 00909 $this->depth = 0; 00910 } 00911 00923 function newChild( $args = false, $title = false, $indexOffset = 0 ) { 00924 $namedArgs = array(); 00925 $numberedArgs = array(); 00926 if ( $title === false ) { 00927 $title = $this->title; 00928 } 00929 if ( $args !== false ) { 00930 if ( $args instanceof PPNode_Hash_Array ) { 00931 $args = $args->value; 00932 } elseif ( !is_array( $args ) ) { 00933 throw new MWException( __METHOD__ . ': $args must be array or PPNode_Hash_Array' ); 00934 } 00935 foreach ( $args as $arg ) { 00936 $bits = $arg->splitArg(); 00937 if ( $bits['index'] !== '' ) { 00938 // Numbered parameter 00939 $index = $bits['index'] - $indexOffset; 00940 $numberedArgs[$index] = $bits['value']; 00941 unset( $namedArgs[$index] ); 00942 } else { 00943 // Named parameter 00944 $name = trim( $this->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) ); 00945 $namedArgs[$name] = $bits['value']; 00946 unset( $numberedArgs[$name] ); 00947 } 00948 } 00949 } 00950 return new PPTemplateFrame_Hash( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title ); 00951 } 00952 00959 function expand( $root, $flags = 0 ) { 00960 static $expansionDepth = 0; 00961 if ( is_string( $root ) ) { 00962 return $root; 00963 } 00964 00965 if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->getMaxPPNodeCount() ) { 00966 $this->parser->limitationWarn( 'node-count-exceeded', 00967 $this->parser->mPPNodeCount, 00968 $this->parser->mOptions->getMaxPPNodeCount() 00969 ); 00970 return '<span class="error">Node-count limit exceeded</span>'; 00971 } 00972 if ( $expansionDepth > $this->parser->mOptions->getMaxPPExpandDepth() ) { 00973 $this->parser->limitationWarn( 'expansion-depth-exceeded', 00974 $expansionDepth, 00975 $this->parser->mOptions->getMaxPPExpandDepth() 00976 ); 00977 return '<span class="error">Expansion depth limit exceeded</span>'; 00978 } 00979 ++$expansionDepth; 00980 if ( $expansionDepth > $this->parser->mHighestExpansionDepth ) { 00981 $this->parser->mHighestExpansionDepth = $expansionDepth; 00982 } 00983 00984 $outStack = array( '', '' ); 00985 $iteratorStack = array( false, $root ); 00986 $indexStack = array( 0, 0 ); 00987 00988 while ( count( $iteratorStack ) > 1 ) { 00989 $level = count( $outStack ) - 1; 00990 $iteratorNode =& $iteratorStack[$level]; 00991 $out =& $outStack[$level]; 00992 $index =& $indexStack[$level]; 00993 00994 if ( is_array( $iteratorNode ) ) { 00995 if ( $index >= count( $iteratorNode ) ) { 00996 // All done with this iterator 00997 $iteratorStack[$level] = false; 00998 $contextNode = false; 00999 } else { 01000 $contextNode = $iteratorNode[$index]; 01001 $index++; 01002 } 01003 } elseif ( $iteratorNode instanceof PPNode_Hash_Array ) { 01004 if ( $index >= $iteratorNode->getLength() ) { 01005 // All done with this iterator 01006 $iteratorStack[$level] = false; 01007 $contextNode = false; 01008 } else { 01009 $contextNode = $iteratorNode->item( $index ); 01010 $index++; 01011 } 01012 } else { 01013 // Copy to $contextNode and then delete from iterator stack, 01014 // because this is not an iterator but we do have to execute it once 01015 $contextNode = $iteratorStack[$level]; 01016 $iteratorStack[$level] = false; 01017 } 01018 01019 $newIterator = false; 01020 01021 if ( $contextNode === false ) { 01022 // nothing to do 01023 } elseif ( is_string( $contextNode ) ) { 01024 $out .= $contextNode; 01025 } elseif ( is_array( $contextNode ) || $contextNode instanceof PPNode_Hash_Array ) { 01026 $newIterator = $contextNode; 01027 } elseif ( $contextNode instanceof PPNode_Hash_Attr ) { 01028 // No output 01029 } elseif ( $contextNode instanceof PPNode_Hash_Text ) { 01030 $out .= $contextNode->value; 01031 } elseif ( $contextNode instanceof PPNode_Hash_Tree ) { 01032 if ( $contextNode->name == 'template' ) { 01033 # Double-brace expansion 01034 $bits = $contextNode->splitTemplate(); 01035 if ( $flags & PPFrame::NO_TEMPLATES ) { 01036 $newIterator = $this->virtualBracketedImplode( '{{', '|', '}}', $bits['title'], $bits['parts'] ); 01037 } else { 01038 $ret = $this->parser->braceSubstitution( $bits, $this ); 01039 if ( isset( $ret['object'] ) ) { 01040 $newIterator = $ret['object']; 01041 } else { 01042 $out .= $ret['text']; 01043 } 01044 } 01045 } elseif ( $contextNode->name == 'tplarg' ) { 01046 # Triple-brace expansion 01047 $bits = $contextNode->splitTemplate(); 01048 if ( $flags & PPFrame::NO_ARGS ) { 01049 $newIterator = $this->virtualBracketedImplode( '{{{', '|', '}}}', $bits['title'], $bits['parts'] ); 01050 } else { 01051 $ret = $this->parser->argSubstitution( $bits, $this ); 01052 if ( isset( $ret['object'] ) ) { 01053 $newIterator = $ret['object']; 01054 } else { 01055 $out .= $ret['text']; 01056 } 01057 } 01058 } elseif ( $contextNode->name == 'comment' ) { 01059 # HTML-style comment 01060 # Remove it in HTML, pre+remove and STRIP_COMMENTS modes 01061 if ( $this->parser->ot['html'] 01062 || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() ) 01063 || ( $flags & PPFrame::STRIP_COMMENTS ) 01064 ) { 01065 $out .= ''; 01066 } elseif ( $this->parser->ot['wiki'] && !( $flags & PPFrame::RECOVER_COMMENTS ) ) { 01067 # Add a strip marker in PST mode so that pstPass2() can run some old-fashioned regexes on the result 01068 # Not in RECOVER_COMMENTS mode (extractSections) though 01069 $out .= $this->parser->insertStripItem( $contextNode->firstChild->value ); 01070 } else { 01071 # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove 01072 $out .= $contextNode->firstChild->value; 01073 } 01074 } elseif ( $contextNode->name == 'ignore' ) { 01075 # Output suppression used by <includeonly> etc. 01076 # OT_WIKI will only respect <ignore> in substed templates. 01077 # The other output types respect it unless NO_IGNORE is set. 01078 # extractSections() sets NO_IGNORE and so never respects it. 01079 if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] ) || ( $flags & PPFrame::NO_IGNORE ) ) { 01080 $out .= $contextNode->firstChild->value; 01081 } else { 01082 //$out .= ''; 01083 } 01084 } elseif ( $contextNode->name == 'ext' ) { 01085 # Extension tag 01086 $bits = $contextNode->splitExt() + array( 'attr' => null, 'inner' => null, 'close' => null ); 01087 $out .= $this->parser->extensionSubstitution( $bits, $this ); 01088 } elseif ( $contextNode->name == 'h' ) { 01089 # Heading 01090 if ( $this->parser->ot['html'] ) { 01091 # Expand immediately and insert heading index marker 01092 $s = ''; 01093 for ( $node = $contextNode->firstChild; $node; $node = $node->nextSibling ) { 01094 $s .= $this->expand( $node, $flags ); 01095 } 01096 01097 $bits = $contextNode->splitHeading(); 01098 $titleText = $this->title->getPrefixedDBkey(); 01099 $this->parser->mHeadings[] = array( $titleText, $bits['i'] ); 01100 $serial = count( $this->parser->mHeadings ) - 1; 01101 $marker = "{$this->parser->mUniqPrefix}-h-$serial-" . Parser::MARKER_SUFFIX; 01102 $s = substr( $s, 0, $bits['level'] ) . $marker . substr( $s, $bits['level'] ); 01103 $this->parser->mStripState->addGeneral( $marker, '' ); 01104 $out .= $s; 01105 } else { 01106 # Expand in virtual stack 01107 $newIterator = $contextNode->getChildren(); 01108 } 01109 } else { 01110 # Generic recursive expansion 01111 $newIterator = $contextNode->getChildren(); 01112 } 01113 } else { 01114 throw new MWException( __METHOD__ . ': Invalid parameter type' ); 01115 } 01116 01117 if ( $newIterator !== false ) { 01118 $outStack[] = ''; 01119 $iteratorStack[] = $newIterator; 01120 $indexStack[] = 0; 01121 } elseif ( $iteratorStack[$level] === false ) { 01122 // Return accumulated value to parent 01123 // With tail recursion 01124 while ( $iteratorStack[$level] === false && $level > 0 ) { 01125 $outStack[$level - 1] .= $out; 01126 array_pop( $outStack ); 01127 array_pop( $iteratorStack ); 01128 array_pop( $indexStack ); 01129 $level--; 01130 } 01131 } 01132 } 01133 --$expansionDepth; 01134 return $outStack[0]; 01135 } 01136 01142 function implodeWithFlags( $sep, $flags /*, ... */ ) { 01143 $args = array_slice( func_get_args(), 2 ); 01144 01145 $first = true; 01146 $s = ''; 01147 foreach ( $args as $root ) { 01148 if ( $root instanceof PPNode_Hash_Array ) { 01149 $root = $root->value; 01150 } 01151 if ( !is_array( $root ) ) { 01152 $root = array( $root ); 01153 } 01154 foreach ( $root as $node ) { 01155 if ( $first ) { 01156 $first = false; 01157 } else { 01158 $s .= $sep; 01159 } 01160 $s .= $this->expand( $node, $flags ); 01161 } 01162 } 01163 return $s; 01164 } 01165 01171 function implode( $sep /*, ... */ ) { 01172 $args = array_slice( func_get_args(), 1 ); 01173 01174 $first = true; 01175 $s = ''; 01176 foreach ( $args as $root ) { 01177 if ( $root instanceof PPNode_Hash_Array ) { 01178 $root = $root->value; 01179 } 01180 if ( !is_array( $root ) ) { 01181 $root = array( $root ); 01182 } 01183 foreach ( $root as $node ) { 01184 if ( $first ) { 01185 $first = false; 01186 } else { 01187 $s .= $sep; 01188 } 01189 $s .= $this->expand( $node ); 01190 } 01191 } 01192 return $s; 01193 } 01194 01201 function virtualImplode( $sep /*, ... */ ) { 01202 $args = array_slice( func_get_args(), 1 ); 01203 $out = array(); 01204 $first = true; 01205 01206 foreach ( $args as $root ) { 01207 if ( $root instanceof PPNode_Hash_Array ) { 01208 $root = $root->value; 01209 } 01210 if ( !is_array( $root ) ) { 01211 $root = array( $root ); 01212 } 01213 foreach ( $root as $node ) { 01214 if ( $first ) { 01215 $first = false; 01216 } else { 01217 $out[] = $sep; 01218 } 01219 $out[] = $node; 01220 } 01221 } 01222 return new PPNode_Hash_Array( $out ); 01223 } 01224 01230 function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) { 01231 $args = array_slice( func_get_args(), 3 ); 01232 $out = array( $start ); 01233 $first = true; 01234 01235 foreach ( $args as $root ) { 01236 if ( $root instanceof PPNode_Hash_Array ) { 01237 $root = $root->value; 01238 } 01239 if ( !is_array( $root ) ) { 01240 $root = array( $root ); 01241 } 01242 foreach ( $root as $node ) { 01243 if ( $first ) { 01244 $first = false; 01245 } else { 01246 $out[] = $sep; 01247 } 01248 $out[] = $node; 01249 } 01250 } 01251 $out[] = $end; 01252 return new PPNode_Hash_Array( $out ); 01253 } 01254 01255 function __toString() { 01256 return 'frame{}'; 01257 } 01258 01263 function getPDBK( $level = false ) { 01264 if ( $level === false ) { 01265 return $this->title->getPrefixedDBkey(); 01266 } else { 01267 return isset( $this->titleCache[$level] ) ? $this->titleCache[$level] : false; 01268 } 01269 } 01270 01274 function getArguments() { 01275 return array(); 01276 } 01277 01281 function getNumberedArguments() { 01282 return array(); 01283 } 01284 01288 function getNamedArguments() { 01289 return array(); 01290 } 01291 01297 function isEmpty() { 01298 return true; 01299 } 01300 01305 function getArgument( $name ) { 01306 return false; 01307 } 01308 01316 function loopCheck( $title ) { 01317 return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] ); 01318 } 01319 01325 function isTemplate() { 01326 return false; 01327 } 01328 01334 function getTitle() { 01335 return $this->title; 01336 } 01337 } 01338 01343 class PPTemplateFrame_Hash extends PPFrame_Hash { 01344 var $numberedArgs, $namedArgs, $parent; 01345 var $numberedExpansionCache, $namedExpansionCache; 01346 01354 function __construct( $preprocessor, $parent = false, $numberedArgs = array(), $namedArgs = array(), $title = false ) { 01355 parent::__construct( $preprocessor ); 01356 01357 $this->parent = $parent; 01358 $this->numberedArgs = $numberedArgs; 01359 $this->namedArgs = $namedArgs; 01360 $this->title = $title; 01361 $pdbk = $title ? $title->getPrefixedDBkey() : false; 01362 $this->titleCache = $parent->titleCache; 01363 $this->titleCache[] = $pdbk; 01364 $this->loopCheckHash = /*clone*/ $parent->loopCheckHash; 01365 if ( $pdbk !== false ) { 01366 $this->loopCheckHash[$pdbk] = true; 01367 } 01368 $this->depth = $parent->depth + 1; 01369 $this->numberedExpansionCache = $this->namedExpansionCache = array(); 01370 } 01371 01372 function __toString() { 01373 $s = 'tplframe{'; 01374 $first = true; 01375 $args = $this->numberedArgs + $this->namedArgs; 01376 foreach ( $args as $name => $value ) { 01377 if ( $first ) { 01378 $first = false; 01379 } else { 01380 $s .= ', '; 01381 } 01382 $s .= "\"$name\":\"" . 01383 str_replace( '"', '\\"', $value->__toString() ) . '"'; 01384 } 01385 $s .= '}'; 01386 return $s; 01387 } 01388 01394 function isEmpty() { 01395 return !count( $this->numberedArgs ) && !count( $this->namedArgs ); 01396 } 01397 01401 function getArguments() { 01402 $arguments = array(); 01403 foreach ( array_merge( 01404 array_keys( $this->numberedArgs ), 01405 array_keys( $this->namedArgs ) ) as $key ) { 01406 $arguments[$key] = $this->getArgument( $key ); 01407 } 01408 return $arguments; 01409 } 01410 01414 function getNumberedArguments() { 01415 $arguments = array(); 01416 foreach ( array_keys( $this->numberedArgs ) as $key ) { 01417 $arguments[$key] = $this->getArgument( $key ); 01418 } 01419 return $arguments; 01420 } 01421 01425 function getNamedArguments() { 01426 $arguments = array(); 01427 foreach ( array_keys( $this->namedArgs ) as $key ) { 01428 $arguments[$key] = $this->getArgument( $key ); 01429 } 01430 return $arguments; 01431 } 01432 01437 function getNumberedArgument( $index ) { 01438 if ( !isset( $this->numberedArgs[$index] ) ) { 01439 return false; 01440 } 01441 if ( !isset( $this->numberedExpansionCache[$index] ) ) { 01442 # No trimming for unnamed arguments 01443 $this->numberedExpansionCache[$index] = $this->parent->expand( $this->numberedArgs[$index], PPFrame::STRIP_COMMENTS ); 01444 } 01445 return $this->numberedExpansionCache[$index]; 01446 } 01447 01452 function getNamedArgument( $name ) { 01453 if ( !isset( $this->namedArgs[$name] ) ) { 01454 return false; 01455 } 01456 if ( !isset( $this->namedExpansionCache[$name] ) ) { 01457 # Trim named arguments post-expand, for backwards compatibility 01458 $this->namedExpansionCache[$name] = trim( 01459 $this->parent->expand( $this->namedArgs[$name], PPFrame::STRIP_COMMENTS ) ); 01460 } 01461 return $this->namedExpansionCache[$name]; 01462 } 01463 01468 function getArgument( $name ) { 01469 $text = $this->getNumberedArgument( $name ); 01470 if ( $text === false ) { 01471 $text = $this->getNamedArgument( $name ); 01472 } 01473 return $text; 01474 } 01475 01481 function isTemplate() { 01482 return true; 01483 } 01484 } 01485 01490 class PPCustomFrame_Hash extends PPFrame_Hash { 01491 var $args; 01492 01493 function __construct( $preprocessor, $args ) { 01494 parent::__construct( $preprocessor ); 01495 $this->args = $args; 01496 } 01497 01498 function __toString() { 01499 $s = 'cstmframe{'; 01500 $first = true; 01501 foreach ( $this->args as $name => $value ) { 01502 if ( $first ) { 01503 $first = false; 01504 } else { 01505 $s .= ', '; 01506 } 01507 $s .= "\"$name\":\"" . 01508 str_replace( '"', '\\"', $value->__toString() ) . '"'; 01509 } 01510 $s .= '}'; 01511 return $s; 01512 } 01513 01517 function isEmpty() { 01518 return !count( $this->args ); 01519 } 01520 01525 function getArgument( $index ) { 01526 if ( !isset( $this->args[$index] ) ) { 01527 return false; 01528 } 01529 return $this->args[$index]; 01530 } 01531 01532 function getArguments() { 01533 return $this->args; 01534 } 01535 } 01536 01540 class PPNode_Hash_Tree implements PPNode { 01541 var $name, $firstChild, $lastChild, $nextSibling; 01542 01543 function __construct( $name ) { 01544 $this->name = $name; 01545 $this->firstChild = $this->lastChild = $this->nextSibling = false; 01546 } 01547 01548 function __toString() { 01549 $inner = ''; 01550 $attribs = ''; 01551 for ( $node = $this->firstChild; $node; $node = $node->nextSibling ) { 01552 if ( $node instanceof PPNode_Hash_Attr ) { 01553 $attribs .= ' ' . $node->name . '="' . htmlspecialchars( $node->value ) . '"'; 01554 } else { 01555 $inner .= $node->__toString(); 01556 } 01557 } 01558 if ( $inner === '' ) { 01559 return "<{$this->name}$attribs/>"; 01560 } else { 01561 return "<{$this->name}$attribs>$inner</{$this->name}>"; 01562 } 01563 } 01564 01570 static function newWithText( $name, $text ) { 01571 $obj = new self( $name ); 01572 $obj->addChild( new PPNode_Hash_Text( $text ) ); 01573 return $obj; 01574 } 01575 01576 function addChild( $node ) { 01577 if ( $this->lastChild === false ) { 01578 $this->firstChild = $this->lastChild = $node; 01579 } else { 01580 $this->lastChild->nextSibling = $node; 01581 $this->lastChild = $node; 01582 } 01583 } 01584 01588 function getChildren() { 01589 $children = array(); 01590 for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) { 01591 $children[] = $child; 01592 } 01593 return new PPNode_Hash_Array( $children ); 01594 } 01595 01596 function getFirstChild() { 01597 return $this->firstChild; 01598 } 01599 01600 function getNextSibling() { 01601 return $this->nextSibling; 01602 } 01603 01604 function getChildrenOfType( $name ) { 01605 $children = array(); 01606 for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) { 01607 if ( isset( $child->name ) && $child->name === $name ) { 01608 $children[] = $child; 01609 } 01610 } 01611 return $children; 01612 } 01613 01617 function getLength() { 01618 return false; 01619 } 01620 01625 function item( $i ) { 01626 return false; 01627 } 01628 01632 function getName() { 01633 return $this->name; 01634 } 01635 01645 function splitArg() { 01646 $bits = array(); 01647 for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) { 01648 if ( !isset( $child->name ) ) { 01649 continue; 01650 } 01651 if ( $child->name === 'name' ) { 01652 $bits['name'] = $child; 01653 if ( $child->firstChild instanceof PPNode_Hash_Attr 01654 && $child->firstChild->name === 'index' 01655 ) { 01656 $bits['index'] = $child->firstChild->value; 01657 } 01658 } elseif ( $child->name === 'value' ) { 01659 $bits['value'] = $child; 01660 } 01661 } 01662 01663 if ( !isset( $bits['name'] ) ) { 01664 throw new MWException( 'Invalid brace node passed to ' . __METHOD__ ); 01665 } 01666 if ( !isset( $bits['index'] ) ) { 01667 $bits['index'] = ''; 01668 } 01669 return $bits; 01670 } 01671 01679 function splitExt() { 01680 $bits = array(); 01681 for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) { 01682 if ( !isset( $child->name ) ) { 01683 continue; 01684 } 01685 if ( $child->name == 'name' ) { 01686 $bits['name'] = $child; 01687 } elseif ( $child->name == 'attr' ) { 01688 $bits['attr'] = $child; 01689 } elseif ( $child->name == 'inner' ) { 01690 $bits['inner'] = $child; 01691 } elseif ( $child->name == 'close' ) { 01692 $bits['close'] = $child; 01693 } 01694 } 01695 if ( !isset( $bits['name'] ) ) { 01696 throw new MWException( 'Invalid ext node passed to ' . __METHOD__ ); 01697 } 01698 return $bits; 01699 } 01700 01707 function splitHeading() { 01708 if ( $this->name !== 'h' ) { 01709 throw new MWException( 'Invalid h node passed to ' . __METHOD__ ); 01710 } 01711 $bits = array(); 01712 for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) { 01713 if ( !isset( $child->name ) ) { 01714 continue; 01715 } 01716 if ( $child->name == 'i' ) { 01717 $bits['i'] = $child->value; 01718 } elseif ( $child->name == 'level' ) { 01719 $bits['level'] = $child->value; 01720 } 01721 } 01722 if ( !isset( $bits['i'] ) ) { 01723 throw new MWException( 'Invalid h node passed to ' . __METHOD__ ); 01724 } 01725 return $bits; 01726 } 01727 01734 function splitTemplate() { 01735 $parts = array(); 01736 $bits = array( 'lineStart' => '' ); 01737 for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) { 01738 if ( !isset( $child->name ) ) { 01739 continue; 01740 } 01741 if ( $child->name == 'title' ) { 01742 $bits['title'] = $child; 01743 } 01744 if ( $child->name == 'part' ) { 01745 $parts[] = $child; 01746 } 01747 if ( $child->name == 'lineStart' ) { 01748 $bits['lineStart'] = '1'; 01749 } 01750 } 01751 if ( !isset( $bits['title'] ) ) { 01752 throw new MWException( 'Invalid node passed to ' . __METHOD__ ); 01753 } 01754 $bits['parts'] = new PPNode_Hash_Array( $parts ); 01755 return $bits; 01756 } 01757 } 01758 01762 class PPNode_Hash_Text implements PPNode { 01763 var $value, $nextSibling; 01764 01765 function __construct( $value ) { 01766 if ( is_object( $value ) ) { 01767 throw new MWException( __CLASS__ . ' given object instead of string' ); 01768 } 01769 $this->value = $value; 01770 } 01771 01772 function __toString() { 01773 return htmlspecialchars( $this->value ); 01774 } 01775 01776 function getNextSibling() { 01777 return $this->nextSibling; 01778 } 01779 01780 function getChildren() { return false; } 01781 function getFirstChild() { return false; } 01782 function getChildrenOfType( $name ) { return false; } 01783 function getLength() { return false; } 01784 function item( $i ) { return false; } 01785 function getName() { return '#text'; } 01786 function splitArg() { throw new MWException( __METHOD__ . ': not supported' ); } 01787 function splitExt() { throw new MWException( __METHOD__ . ': not supported' ); } 01788 function splitHeading() { throw new MWException( __METHOD__ . ': not supported' ); } 01789 } 01790 01794 class PPNode_Hash_Array implements PPNode { 01795 var $value, $nextSibling; 01796 01797 function __construct( $value ) { 01798 $this->value = $value; 01799 } 01800 01801 function __toString() { 01802 return var_export( $this, true ); 01803 } 01804 01805 function getLength() { 01806 return count( $this->value ); 01807 } 01808 01809 function item( $i ) { 01810 return $this->value[$i]; 01811 } 01812 01813 function getName() { return '#nodelist'; } 01814 01815 function getNextSibling() { 01816 return $this->nextSibling; 01817 } 01818 01819 function getChildren() { return false; } 01820 function getFirstChild() { return false; } 01821 function getChildrenOfType( $name ) { return false; } 01822 function splitArg() { throw new MWException( __METHOD__ . ': not supported' ); } 01823 function splitExt() { throw new MWException( __METHOD__ . ': not supported' ); } 01824 function splitHeading() { throw new MWException( __METHOD__ . ': not supported' ); } 01825 } 01826 01830 class PPNode_Hash_Attr implements PPNode { 01831 var $name, $value, $nextSibling; 01832 01833 function __construct( $name, $value ) { 01834 $this->name = $name; 01835 $this->value = $value; 01836 } 01837 01838 function __toString() { 01839 return "<@{$this->name}>" . htmlspecialchars( $this->value ) . "</@{$this->name}>"; 01840 } 01841 01842 function getName() { 01843 return $this->name; 01844 } 01845 01846 function getNextSibling() { 01847 return $this->nextSibling; 01848 } 01849 01850 function getChildren() { return false; } 01851 function getFirstChild() { return false; } 01852 function getChildrenOfType( $name ) { return false; } 01853 function getLength() { return false; } 01854 function item( $i ) { return false; } 01855 function splitArg() { throw new MWException( __METHOD__ . ': not supported' ); } 01856 function splitExt() { throw new MWException( __METHOD__ . ': not supported' ); } 01857 function splitHeading() { throw new MWException( __METHOD__ . ': not supported' ); } 01858 }