MediaWiki
master
|
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 // To avoid leaving blank lines, when a comment is both preceded 00291 // and followed by a newline (ignoring spaces), trim leading and 00292 // trailing spaces and one of the newlines. 00293 00294 // Find the end 00295 $endPos = strpos( $text, '-->', $i + 4 ); 00296 if ( $endPos === false ) { 00297 // Unclosed comment in input, runs to end 00298 $inner = substr( $text, $i ); 00299 $accum->addNodeWithText( 'comment', $inner ); 00300 $i = $lengthText; 00301 } else { 00302 // Search backwards for leading whitespace 00303 $wsStart = $i ? ( $i - strspn( $revText, ' ', $lengthText - $i ) ) : 0; 00304 // Search forwards for trailing whitespace 00305 // $wsEnd will be the position of the last space (or the '>' if there's none) 00306 $wsEnd = $endPos + 2 + strspn( $text, ' ', $endPos + 3 ); 00307 // Eat the line if possible 00308 // TODO: This could theoretically be done if $wsStart == 0, i.e. for comments at 00309 // the overall start. That's not how Sanitizer::removeHTMLcomments() did it, but 00310 // it's a possible beneficial b/c break. 00311 if ( $wsStart > 0 && substr( $text, $wsStart - 1, 1 ) == "\n" 00312 && substr( $text, $wsEnd + 1, 1 ) == "\n" ) 00313 { 00314 $startPos = $wsStart; 00315 $endPos = $wsEnd + 1; 00316 // Remove leading whitespace from the end of the accumulator 00317 // Sanity check first though 00318 $wsLength = $i - $wsStart; 00319 if ( $wsLength > 0 00320 && $accum->lastNode instanceof PPNode_Hash_Text 00321 && substr( $accum->lastNode->value, -$wsLength ) === str_repeat( ' ', $wsLength ) ) 00322 { 00323 $accum->lastNode->value = substr( $accum->lastNode->value, 0, -$wsLength ); 00324 } 00325 // Do a line-start run next time to look for headings after the comment 00326 $fakeLineStart = true; 00327 } else { 00328 // No line to eat, just take the comment itself 00329 $startPos = $i; 00330 $endPos += 2; 00331 } 00332 00333 if ( $stack->top ) { 00334 $part = $stack->top->getCurrentPart(); 00335 if ( ! (isset( $part->commentEnd ) && $part->commentEnd == $wsStart - 1 )) { 00336 $part->visualEnd = $wsStart; 00337 } 00338 // Else comments abutting, no change in visual end 00339 $part->commentEnd = $endPos; 00340 } 00341 $i = $endPos + 1; 00342 $inner = substr( $text, $startPos, $endPos - $startPos + 1 ); 00343 $accum->addNodeWithText( 'comment', $inner ); 00344 } 00345 continue; 00346 } 00347 $name = $matches[1]; 00348 $lowerName = strtolower( $name ); 00349 $attrStart = $i + strlen( $name ) + 1; 00350 00351 // Find end of tag 00352 $tagEndPos = $noMoreGT ? false : strpos( $text, '>', $attrStart ); 00353 if ( $tagEndPos === false ) { 00354 // Infinite backtrack 00355 // Disable tag search to prevent worst-case O(N^2) performance 00356 $noMoreGT = true; 00357 $accum->addLiteral( '<' ); 00358 ++$i; 00359 continue; 00360 } 00361 00362 // Handle ignored tags 00363 if ( in_array( $lowerName, $ignoredTags ) ) { 00364 $accum->addNodeWithText( 'ignore', substr( $text, $i, $tagEndPos - $i + 1 ) ); 00365 $i = $tagEndPos + 1; 00366 continue; 00367 } 00368 00369 $tagStartPos = $i; 00370 if ( $text[$tagEndPos-1] == '/' ) { 00371 // Short end tag 00372 $attrEnd = $tagEndPos - 1; 00373 $inner = null; 00374 $i = $tagEndPos + 1; 00375 $close = null; 00376 } else { 00377 $attrEnd = $tagEndPos; 00378 // Find closing tag 00379 if ( preg_match( "/<\/" . preg_quote( $name, '/' ) . "\s*>/i", 00380 $text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 ) ) 00381 { 00382 $inner = substr( $text, $tagEndPos + 1, $matches[0][1] - $tagEndPos - 1 ); 00383 $i = $matches[0][1] + strlen( $matches[0][0] ); 00384 $close = $matches[0][0]; 00385 } else { 00386 // No end tag -- let it run out to the end of the text. 00387 $inner = substr( $text, $tagEndPos + 1 ); 00388 $i = $lengthText; 00389 $close = null; 00390 } 00391 } 00392 // <includeonly> and <noinclude> just become <ignore> tags 00393 if ( in_array( $lowerName, $ignoredElements ) ) { 00394 $accum->addNodeWithText( 'ignore', substr( $text, $tagStartPos, $i - $tagStartPos ) ); 00395 continue; 00396 } 00397 00398 if ( $attrEnd <= $attrStart ) { 00399 $attr = ''; 00400 } else { 00401 // Note that the attr element contains the whitespace between name and attribute, 00402 // this is necessary for precise reconstruction during pre-save transform. 00403 $attr = substr( $text, $attrStart, $attrEnd - $attrStart ); 00404 } 00405 00406 $extNode = new PPNode_Hash_Tree( 'ext' ); 00407 $extNode->addChild( PPNode_Hash_Tree::newWithText( 'name', $name ) ); 00408 $extNode->addChild( PPNode_Hash_Tree::newWithText( 'attr', $attr ) ); 00409 if ( $inner !== null ) { 00410 $extNode->addChild( PPNode_Hash_Tree::newWithText( 'inner', $inner ) ); 00411 } 00412 if ( $close !== null ) { 00413 $extNode->addChild( PPNode_Hash_Tree::newWithText( 'close', $close ) ); 00414 } 00415 $accum->addNode( $extNode ); 00416 } 00417 00418 elseif ( $found == 'line-start' ) { 00419 // Is this the start of a heading? 00420 // Line break belongs before the heading element in any case 00421 if ( $fakeLineStart ) { 00422 $fakeLineStart = false; 00423 } else { 00424 $accum->addLiteral( $curChar ); 00425 $i++; 00426 } 00427 00428 $count = strspn( $text, '=', $i, 6 ); 00429 if ( $count == 1 && $findEquals ) { 00430 // DWIM: This looks kind of like a name/value separator 00431 // Let's let the equals handler have it and break the potential heading 00432 // This is heuristic, but AFAICT the methods for completely correct disambiguation are very complex. 00433 } elseif ( $count > 0 ) { 00434 $piece = array( 00435 'open' => "\n", 00436 'close' => "\n", 00437 'parts' => array( new PPDPart_Hash( str_repeat( '=', $count ) ) ), 00438 'startPos' => $i, 00439 'count' => $count ); 00440 $stack->push( $piece ); 00441 $accum =& $stack->getAccum(); 00442 extract( $stack->getFlags() ); 00443 $i += $count; 00444 } 00445 } elseif ( $found == 'line-end' ) { 00446 $piece = $stack->top; 00447 // A heading must be open, otherwise \n wouldn't have been in the search list 00448 assert( '$piece->open == "\n"' ); 00449 $part = $piece->getCurrentPart(); 00450 // Search back through the input to see if it has a proper close 00451 // Do this using the reversed string since the other solutions (end anchor, etc.) are inefficient 00452 $wsLength = strspn( $revText, " \t", $lengthText - $i ); 00453 $searchStart = $i - $wsLength; 00454 if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) { 00455 // Comment found at line end 00456 // Search for equals signs before the comment 00457 $searchStart = $part->visualEnd; 00458 $searchStart -= strspn( $revText, " \t", $lengthText - $searchStart ); 00459 } 00460 $count = $piece->count; 00461 $equalsLength = strspn( $revText, '=', $lengthText - $searchStart ); 00462 if ( $equalsLength > 0 ) { 00463 if ( $searchStart - $equalsLength == $piece->startPos ) { 00464 // This is just a single string of equals signs on its own line 00465 // Replicate the doHeadings behaviour /={count}(.+)={count}/ 00466 // First find out how many equals signs there really are (don't stop at 6) 00467 $count = $equalsLength; 00468 if ( $count < 3 ) { 00469 $count = 0; 00470 } else { 00471 $count = min( 6, intval( ( $count - 1 ) / 2 ) ); 00472 } 00473 } else { 00474 $count = min( $equalsLength, $count ); 00475 } 00476 if ( $count > 0 ) { 00477 // Normal match, output <h> 00478 $element = new PPNode_Hash_Tree( 'possible-h' ); 00479 $element->addChild( new PPNode_Hash_Attr( 'level', $count ) ); 00480 $element->addChild( new PPNode_Hash_Attr( 'i', $headingIndex++ ) ); 00481 $element->lastChild->nextSibling = $accum->firstNode; 00482 $element->lastChild = $accum->lastNode; 00483 } else { 00484 // Single equals sign on its own line, count=0 00485 $element = $accum; 00486 } 00487 } else { 00488 // No match, no <h>, just pass down the inner text 00489 $element = $accum; 00490 } 00491 // Unwind the stack 00492 $stack->pop(); 00493 $accum =& $stack->getAccum(); 00494 extract( $stack->getFlags() ); 00495 00496 // Append the result to the enclosing accumulator 00497 if ( $element instanceof PPNode ) { 00498 $accum->addNode( $element ); 00499 } else { 00500 $accum->addAccum( $element ); 00501 } 00502 // Note that we do NOT increment the input pointer. 00503 // This is because the closing linebreak could be the opening linebreak of 00504 // another heading. Infinite loops are avoided because the next iteration MUST 00505 // hit the heading open case above, which unconditionally increments the 00506 // input pointer. 00507 } elseif ( $found == 'open' ) { 00508 # count opening brace characters 00509 $count = strspn( $text, $curChar, $i ); 00510 00511 # we need to add to stack only if opening brace count is enough for one of the rules 00512 if ( $count >= $rule['min'] ) { 00513 # Add it to the stack 00514 $piece = array( 00515 'open' => $curChar, 00516 'close' => $rule['end'], 00517 'count' => $count, 00518 'lineStart' => ($i > 0 && $text[$i-1] == "\n"), 00519 ); 00520 00521 $stack->push( $piece ); 00522 $accum =& $stack->getAccum(); 00523 extract( $stack->getFlags() ); 00524 } else { 00525 # Add literal brace(s) 00526 $accum->addLiteral( str_repeat( $curChar, $count ) ); 00527 } 00528 $i += $count; 00529 } elseif ( $found == 'close' ) { 00530 $piece = $stack->top; 00531 # lets check if there are enough characters for closing brace 00532 $maxCount = $piece->count; 00533 $count = strspn( $text, $curChar, $i, $maxCount ); 00534 00535 # check for maximum matching characters (if there are 5 closing 00536 # characters, we will probably need only 3 - depending on the rules) 00537 $rule = $rules[$piece->open]; 00538 if ( $count > $rule['max'] ) { 00539 # The specified maximum exists in the callback array, unless the caller 00540 # has made an error 00541 $matchingCount = $rule['max']; 00542 } else { 00543 # Count is less than the maximum 00544 # Skip any gaps in the callback array to find the true largest match 00545 # Need to use array_key_exists not isset because the callback can be null 00546 $matchingCount = $count; 00547 while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $rule['names'] ) ) { 00548 --$matchingCount; 00549 } 00550 } 00551 00552 if ($matchingCount <= 0) { 00553 # No matching element found in callback array 00554 # Output a literal closing brace and continue 00555 $accum->addLiteral( str_repeat( $curChar, $count ) ); 00556 $i += $count; 00557 continue; 00558 } 00559 $name = $rule['names'][$matchingCount]; 00560 if ( $name === null ) { 00561 // No element, just literal text 00562 $element = $piece->breakSyntax( $matchingCount ); 00563 $element->addLiteral( str_repeat( $rule['end'], $matchingCount ) ); 00564 } else { 00565 # Create XML element 00566 # Note: $parts is already XML, does not need to be encoded further 00567 $parts = $piece->parts; 00568 $titleAccum = $parts[0]->out; 00569 unset( $parts[0] ); 00570 00571 $element = new PPNode_Hash_Tree( $name ); 00572 00573 # The invocation is at the start of the line if lineStart is set in 00574 # the stack, and all opening brackets are used up. 00575 if ( $maxCount == $matchingCount && !empty( $piece->lineStart ) ) { 00576 $element->addChild( new PPNode_Hash_Attr( 'lineStart', 1 ) ); 00577 } 00578 $titleNode = new PPNode_Hash_Tree( 'title' ); 00579 $titleNode->firstChild = $titleAccum->firstNode; 00580 $titleNode->lastChild = $titleAccum->lastNode; 00581 $element->addChild( $titleNode ); 00582 $argIndex = 1; 00583 foreach ( $parts as $part ) { 00584 if ( isset( $part->eqpos ) ) { 00585 // Find equals 00586 $lastNode = false; 00587 for ( $node = $part->out->firstNode; $node; $node = $node->nextSibling ) { 00588 if ( $node === $part->eqpos ) { 00589 break; 00590 } 00591 $lastNode = $node; 00592 } 00593 if ( !$node ) { 00594 throw new MWException( __METHOD__. ': eqpos not found' ); 00595 } 00596 if ( $node->name !== 'equals' ) { 00597 throw new MWException( __METHOD__ .': eqpos is not equals' ); 00598 } 00599 $equalsNode = $node; 00600 00601 // Construct name node 00602 $nameNode = new PPNode_Hash_Tree( 'name' ); 00603 if ( $lastNode !== false ) { 00604 $lastNode->nextSibling = false; 00605 $nameNode->firstChild = $part->out->firstNode; 00606 $nameNode->lastChild = $lastNode; 00607 } 00608 00609 // Construct value node 00610 $valueNode = new PPNode_Hash_Tree( 'value' ); 00611 if ( $equalsNode->nextSibling !== false ) { 00612 $valueNode->firstChild = $equalsNode->nextSibling; 00613 $valueNode->lastChild = $part->out->lastNode; 00614 } 00615 $partNode = new PPNode_Hash_Tree( 'part' ); 00616 $partNode->addChild( $nameNode ); 00617 $partNode->addChild( $equalsNode->firstChild ); 00618 $partNode->addChild( $valueNode ); 00619 $element->addChild( $partNode ); 00620 } else { 00621 $partNode = new PPNode_Hash_Tree( 'part' ); 00622 $nameNode = new PPNode_Hash_Tree( 'name' ); 00623 $nameNode->addChild( new PPNode_Hash_Attr( 'index', $argIndex++ ) ); 00624 $valueNode = new PPNode_Hash_Tree( 'value' ); 00625 $valueNode->firstChild = $part->out->firstNode; 00626 $valueNode->lastChild = $part->out->lastNode; 00627 $partNode->addChild( $nameNode ); 00628 $partNode->addChild( $valueNode ); 00629 $element->addChild( $partNode ); 00630 } 00631 } 00632 } 00633 00634 # Advance input pointer 00635 $i += $matchingCount; 00636 00637 # Unwind the stack 00638 $stack->pop(); 00639 $accum =& $stack->getAccum(); 00640 00641 # Re-add the old stack element if it still has unmatched opening characters remaining 00642 if ($matchingCount < $piece->count) { 00643 $piece->parts = array( new PPDPart_Hash ); 00644 $piece->count -= $matchingCount; 00645 # do we still qualify for any callback with remaining count? 00646 $names = $rules[$piece->open]['names']; 00647 $skippedBraces = 0; 00648 $enclosingAccum =& $accum; 00649 while ( $piece->count ) { 00650 if ( array_key_exists( $piece->count, $names ) ) { 00651 $stack->push( $piece ); 00652 $accum =& $stack->getAccum(); 00653 break; 00654 } 00655 --$piece->count; 00656 $skippedBraces ++; 00657 } 00658 $enclosingAccum->addLiteral( str_repeat( $piece->open, $skippedBraces ) ); 00659 } 00660 00661 extract( $stack->getFlags() ); 00662 00663 # Add XML element to the enclosing accumulator 00664 if ( $element instanceof PPNode ) { 00665 $accum->addNode( $element ); 00666 } else { 00667 $accum->addAccum( $element ); 00668 } 00669 } elseif ( $found == 'pipe' ) { 00670 $findEquals = true; // shortcut for getFlags() 00671 $stack->addPart(); 00672 $accum =& $stack->getAccum(); 00673 ++$i; 00674 } elseif ( $found == 'equals' ) { 00675 $findEquals = false; // shortcut for getFlags() 00676 $accum->addNodeWithText( 'equals', '=' ); 00677 $stack->getCurrentPart()->eqpos = $accum->lastNode; 00678 ++$i; 00679 } 00680 } 00681 00682 # Output any remaining unclosed brackets 00683 foreach ( $stack->stack as $piece ) { 00684 $stack->rootAccum->addAccum( $piece->breakSyntax() ); 00685 } 00686 00687 # Enable top-level headings 00688 for ( $node = $stack->rootAccum->firstNode; $node; $node = $node->nextSibling ) { 00689 if ( isset( $node->name ) && $node->name === 'possible-h' ) { 00690 $node->name = 'h'; 00691 } 00692 } 00693 00694 $rootNode = new PPNode_Hash_Tree( 'root' ); 00695 $rootNode->firstChild = $stack->rootAccum->firstNode; 00696 $rootNode->lastChild = $stack->rootAccum->lastNode; 00697 00698 // Cache 00699 if ($cacheable) { 00700 $cacheValue = sprintf( "%08d", self::CACHE_VERSION ) . serialize( $rootNode ); 00701 $wgMemc->set( $cacheKey, $cacheValue, 86400 ); 00702 wfProfileOut( __METHOD__.'-cache-miss' ); 00703 wfProfileOut( __METHOD__.'-cacheable' ); 00704 wfDebugLog( "Preprocessor", "Saved preprocessor Hash to memcached (key $cacheKey)" ); 00705 } 00706 00707 wfProfileOut( __METHOD__ ); 00708 return $rootNode; 00709 } 00710 } 00711 00716 class PPDStack_Hash extends PPDStack { 00717 function __construct() { 00718 $this->elementClass = 'PPDStackElement_Hash'; 00719 parent::__construct(); 00720 $this->rootAccum = new PPDAccum_Hash; 00721 } 00722 } 00723 00727 class PPDStackElement_Hash extends PPDStackElement { 00728 function __construct( $data = array() ) { 00729 $this->partClass = 'PPDPart_Hash'; 00730 parent::__construct( $data ); 00731 } 00732 00738 function breakSyntax( $openingCount = false ) { 00739 if ( $this->open == "\n" ) { 00740 $accum = $this->parts[0]->out; 00741 } else { 00742 if ( $openingCount === false ) { 00743 $openingCount = $this->count; 00744 } 00745 $accum = new PPDAccum_Hash; 00746 $accum->addLiteral( str_repeat( $this->open, $openingCount ) ); 00747 $first = true; 00748 foreach ( $this->parts as $part ) { 00749 if ( $first ) { 00750 $first = false; 00751 } else { 00752 $accum->addLiteral( '|' ); 00753 } 00754 $accum->addAccum( $part->out ); 00755 } 00756 } 00757 return $accum; 00758 } 00759 } 00760 00764 class PPDPart_Hash extends PPDPart { 00765 function __construct( $out = '' ) { 00766 $accum = new PPDAccum_Hash; 00767 if ( $out !== '' ) { 00768 $accum->addLiteral( $out ); 00769 } 00770 parent::__construct( $accum ); 00771 } 00772 } 00773 00777 class PPDAccum_Hash { 00778 var $firstNode, $lastNode; 00779 00780 function __construct() { 00781 $this->firstNode = $this->lastNode = false; 00782 } 00783 00787 function addLiteral( $s ) { 00788 if ( $this->lastNode === false ) { 00789 $this->firstNode = $this->lastNode = new PPNode_Hash_Text( $s ); 00790 } elseif ( $this->lastNode instanceof PPNode_Hash_Text ) { 00791 $this->lastNode->value .= $s; 00792 } else { 00793 $this->lastNode->nextSibling = new PPNode_Hash_Text( $s ); 00794 $this->lastNode = $this->lastNode->nextSibling; 00795 } 00796 } 00797 00801 function addNode( PPNode $node ) { 00802 if ( $this->lastNode === false ) { 00803 $this->firstNode = $this->lastNode = $node; 00804 } else { 00805 $this->lastNode->nextSibling = $node; 00806 $this->lastNode = $node; 00807 } 00808 } 00809 00813 function addNodeWithText( $name, $value ) { 00814 $node = PPNode_Hash_Tree::newWithText( $name, $value ); 00815 $this->addNode( $node ); 00816 } 00817 00823 function addAccum( $accum ) { 00824 if ( $accum->lastNode === false ) { 00825 // nothing to add 00826 } elseif ( $this->lastNode === false ) { 00827 $this->firstNode = $accum->firstNode; 00828 $this->lastNode = $accum->lastNode; 00829 } else { 00830 $this->lastNode->nextSibling = $accum->firstNode; 00831 $this->lastNode = $accum->lastNode; 00832 } 00833 } 00834 } 00835 00840 class PPFrame_Hash implements PPFrame { 00841 00845 var $parser; 00846 00850 var $preprocessor; 00851 00855 var $title; 00856 var $titleCache; 00857 00862 var $loopCheckHash; 00863 00868 var $depth; 00869 00870 00875 function __construct( $preprocessor ) { 00876 $this->preprocessor = $preprocessor; 00877 $this->parser = $preprocessor->parser; 00878 $this->title = $this->parser->mTitle; 00879 $this->titleCache = array( $this->title ? $this->title->getPrefixedDBkey() : false ); 00880 $this->loopCheckHash = array(); 00881 $this->depth = 0; 00882 } 00883 00895 function newChild( $args = false, $title = false, $indexOffset = 0 ) { 00896 $namedArgs = array(); 00897 $numberedArgs = array(); 00898 if ( $title === false ) { 00899 $title = $this->title; 00900 } 00901 if ( $args !== false ) { 00902 if ( $args instanceof PPNode_Hash_Array ) { 00903 $args = $args->value; 00904 } elseif ( !is_array( $args ) ) { 00905 throw new MWException( __METHOD__ . ': $args must be array or PPNode_Hash_Array' ); 00906 } 00907 foreach ( $args as $arg ) { 00908 $bits = $arg->splitArg(); 00909 if ( $bits['index'] !== '' ) { 00910 // Numbered parameter 00911 $index = $bits['index'] - $indexOffset; 00912 $numberedArgs[$index] = $bits['value']; 00913 unset( $namedArgs[$index] ); 00914 } else { 00915 // Named parameter 00916 $name = trim( $this->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) ); 00917 $namedArgs[$name] = $bits['value']; 00918 unset( $numberedArgs[$name] ); 00919 } 00920 } 00921 } 00922 return new PPTemplateFrame_Hash( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title ); 00923 } 00924 00931 function expand( $root, $flags = 0 ) { 00932 static $expansionDepth = 0; 00933 if ( is_string( $root ) ) { 00934 return $root; 00935 } 00936 00937 if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->getMaxPPNodeCount() ) { 00938 $this->parser->limitationWarn( 'node-count-exceeded', 00939 $this->parser->mPPNodeCount, 00940 $this->parser->mOptions->getMaxPPNodeCount() 00941 ); 00942 return '<span class="error">Node-count limit exceeded</span>'; 00943 } 00944 if ( $expansionDepth > $this->parser->mOptions->getMaxPPExpandDepth() ) { 00945 $this->parser->limitationWarn( 'expansion-depth-exceeded', 00946 $expansionDepth, 00947 $this->parser->mOptions->getMaxPPExpandDepth() 00948 ); 00949 return '<span class="error">Expansion depth limit exceeded</span>'; 00950 } 00951 ++$expansionDepth; 00952 if ( $expansionDepth > $this->parser->mHighestExpansionDepth ) { 00953 $this->parser->mHighestExpansionDepth = $expansionDepth; 00954 } 00955 00956 $outStack = array( '', '' ); 00957 $iteratorStack = array( false, $root ); 00958 $indexStack = array( 0, 0 ); 00959 00960 while ( count( $iteratorStack ) > 1 ) { 00961 $level = count( $outStack ) - 1; 00962 $iteratorNode =& $iteratorStack[ $level ]; 00963 $out =& $outStack[$level]; 00964 $index =& $indexStack[$level]; 00965 00966 if ( is_array( $iteratorNode ) ) { 00967 if ( $index >= count( $iteratorNode ) ) { 00968 // All done with this iterator 00969 $iteratorStack[$level] = false; 00970 $contextNode = false; 00971 } else { 00972 $contextNode = $iteratorNode[$index]; 00973 $index++; 00974 } 00975 } elseif ( $iteratorNode instanceof PPNode_Hash_Array ) { 00976 if ( $index >= $iteratorNode->getLength() ) { 00977 // All done with this iterator 00978 $iteratorStack[$level] = false; 00979 $contextNode = false; 00980 } else { 00981 $contextNode = $iteratorNode->item( $index ); 00982 $index++; 00983 } 00984 } else { 00985 // Copy to $contextNode and then delete from iterator stack, 00986 // because this is not an iterator but we do have to execute it once 00987 $contextNode = $iteratorStack[$level]; 00988 $iteratorStack[$level] = false; 00989 } 00990 00991 $newIterator = false; 00992 00993 if ( $contextNode === false ) { 00994 // nothing to do 00995 } elseif ( is_string( $contextNode ) ) { 00996 $out .= $contextNode; 00997 } elseif ( is_array( $contextNode ) || $contextNode instanceof PPNode_Hash_Array ) { 00998 $newIterator = $contextNode; 00999 } elseif ( $contextNode instanceof PPNode_Hash_Attr ) { 01000 // No output 01001 } elseif ( $contextNode instanceof PPNode_Hash_Text ) { 01002 $out .= $contextNode->value; 01003 } elseif ( $contextNode instanceof PPNode_Hash_Tree ) { 01004 if ( $contextNode->name == 'template' ) { 01005 # Double-brace expansion 01006 $bits = $contextNode->splitTemplate(); 01007 if ( $flags & PPFrame::NO_TEMPLATES ) { 01008 $newIterator = $this->virtualBracketedImplode( '{{', '|', '}}', $bits['title'], $bits['parts'] ); 01009 } else { 01010 $ret = $this->parser->braceSubstitution( $bits, $this ); 01011 if ( isset( $ret['object'] ) ) { 01012 $newIterator = $ret['object']; 01013 } else { 01014 $out .= $ret['text']; 01015 } 01016 } 01017 } elseif ( $contextNode->name == 'tplarg' ) { 01018 # Triple-brace expansion 01019 $bits = $contextNode->splitTemplate(); 01020 if ( $flags & PPFrame::NO_ARGS ) { 01021 $newIterator = $this->virtualBracketedImplode( '{{{', '|', '}}}', $bits['title'], $bits['parts'] ); 01022 } else { 01023 $ret = $this->parser->argSubstitution( $bits, $this ); 01024 if ( isset( $ret['object'] ) ) { 01025 $newIterator = $ret['object']; 01026 } else { 01027 $out .= $ret['text']; 01028 } 01029 } 01030 } elseif ( $contextNode->name == 'comment' ) { 01031 # HTML-style comment 01032 # Remove it in HTML, pre+remove and STRIP_COMMENTS modes 01033 if ( $this->parser->ot['html'] 01034 || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() ) 01035 || ( $flags & PPFrame::STRIP_COMMENTS ) ) 01036 { 01037 $out .= ''; 01038 } 01039 # Add a strip marker in PST mode so that pstPass2() can run some old-fashioned regexes on the result 01040 # Not in RECOVER_COMMENTS mode (extractSections) though 01041 elseif ( $this->parser->ot['wiki'] && ! ( $flags & PPFrame::RECOVER_COMMENTS ) ) { 01042 $out .= $this->parser->insertStripItem( $contextNode->firstChild->value ); 01043 } 01044 # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove 01045 else { 01046 $out .= $contextNode->firstChild->value; 01047 } 01048 } elseif ( $contextNode->name == 'ignore' ) { 01049 # Output suppression used by <includeonly> etc. 01050 # OT_WIKI will only respect <ignore> in substed templates. 01051 # The other output types respect it unless NO_IGNORE is set. 01052 # extractSections() sets NO_IGNORE and so never respects it. 01053 if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] ) || ( $flags & PPFrame::NO_IGNORE ) ) { 01054 $out .= $contextNode->firstChild->value; 01055 } else { 01056 //$out .= ''; 01057 } 01058 } elseif ( $contextNode->name == 'ext' ) { 01059 # Extension tag 01060 $bits = $contextNode->splitExt() + array( 'attr' => null, 'inner' => null, 'close' => null ); 01061 $out .= $this->parser->extensionSubstitution( $bits, $this ); 01062 } elseif ( $contextNode->name == 'h' ) { 01063 # Heading 01064 if ( $this->parser->ot['html'] ) { 01065 # Expand immediately and insert heading index marker 01066 $s = ''; 01067 for ( $node = $contextNode->firstChild; $node; $node = $node->nextSibling ) { 01068 $s .= $this->expand( $node, $flags ); 01069 } 01070 01071 $bits = $contextNode->splitHeading(); 01072 $titleText = $this->title->getPrefixedDBkey(); 01073 $this->parser->mHeadings[] = array( $titleText, $bits['i'] ); 01074 $serial = count( $this->parser->mHeadings ) - 1; 01075 $marker = "{$this->parser->mUniqPrefix}-h-$serial-" . Parser::MARKER_SUFFIX; 01076 $s = substr( $s, 0, $bits['level'] ) . $marker . substr( $s, $bits['level'] ); 01077 $this->parser->mStripState->addGeneral( $marker, '' ); 01078 $out .= $s; 01079 } else { 01080 # Expand in virtual stack 01081 $newIterator = $contextNode->getChildren(); 01082 } 01083 } else { 01084 # Generic recursive expansion 01085 $newIterator = $contextNode->getChildren(); 01086 } 01087 } else { 01088 throw new MWException( __METHOD__.': Invalid parameter type' ); 01089 } 01090 01091 if ( $newIterator !== false ) { 01092 $outStack[] = ''; 01093 $iteratorStack[] = $newIterator; 01094 $indexStack[] = 0; 01095 } elseif ( $iteratorStack[$level] === false ) { 01096 // Return accumulated value to parent 01097 // With tail recursion 01098 while ( $iteratorStack[$level] === false && $level > 0 ) { 01099 $outStack[$level - 1] .= $out; 01100 array_pop( $outStack ); 01101 array_pop( $iteratorStack ); 01102 array_pop( $indexStack ); 01103 $level--; 01104 } 01105 } 01106 } 01107 --$expansionDepth; 01108 return $outStack[0]; 01109 } 01110 01116 function implodeWithFlags( $sep, $flags /*, ... */ ) { 01117 $args = array_slice( func_get_args(), 2 ); 01118 01119 $first = true; 01120 $s = ''; 01121 foreach ( $args as $root ) { 01122 if ( $root instanceof PPNode_Hash_Array ) { 01123 $root = $root->value; 01124 } 01125 if ( !is_array( $root ) ) { 01126 $root = array( $root ); 01127 } 01128 foreach ( $root as $node ) { 01129 if ( $first ) { 01130 $first = false; 01131 } else { 01132 $s .= $sep; 01133 } 01134 $s .= $this->expand( $node, $flags ); 01135 } 01136 } 01137 return $s; 01138 } 01139 01145 function implode( $sep /*, ... */ ) { 01146 $args = array_slice( func_get_args(), 1 ); 01147 01148 $first = true; 01149 $s = ''; 01150 foreach ( $args as $root ) { 01151 if ( $root instanceof PPNode_Hash_Array ) { 01152 $root = $root->value; 01153 } 01154 if ( !is_array( $root ) ) { 01155 $root = array( $root ); 01156 } 01157 foreach ( $root as $node ) { 01158 if ( $first ) { 01159 $first = false; 01160 } else { 01161 $s .= $sep; 01162 } 01163 $s .= $this->expand( $node ); 01164 } 01165 } 01166 return $s; 01167 } 01168 01175 function virtualImplode( $sep /*, ... */ ) { 01176 $args = array_slice( func_get_args(), 1 ); 01177 $out = array(); 01178 $first = true; 01179 01180 foreach ( $args as $root ) { 01181 if ( $root instanceof PPNode_Hash_Array ) { 01182 $root = $root->value; 01183 } 01184 if ( !is_array( $root ) ) { 01185 $root = array( $root ); 01186 } 01187 foreach ( $root as $node ) { 01188 if ( $first ) { 01189 $first = false; 01190 } else { 01191 $out[] = $sep; 01192 } 01193 $out[] = $node; 01194 } 01195 } 01196 return new PPNode_Hash_Array( $out ); 01197 } 01198 01204 function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) { 01205 $args = array_slice( func_get_args(), 3 ); 01206 $out = array( $start ); 01207 $first = true; 01208 01209 foreach ( $args as $root ) { 01210 if ( $root instanceof PPNode_Hash_Array ) { 01211 $root = $root->value; 01212 } 01213 if ( !is_array( $root ) ) { 01214 $root = array( $root ); 01215 } 01216 foreach ( $root as $node ) { 01217 if ( $first ) { 01218 $first = false; 01219 } else { 01220 $out[] = $sep; 01221 } 01222 $out[] = $node; 01223 } 01224 } 01225 $out[] = $end; 01226 return new PPNode_Hash_Array( $out ); 01227 } 01228 01229 function __toString() { 01230 return 'frame{}'; 01231 } 01232 01237 function getPDBK( $level = false ) { 01238 if ( $level === false ) { 01239 return $this->title->getPrefixedDBkey(); 01240 } else { 01241 return isset( $this->titleCache[$level] ) ? $this->titleCache[$level] : false; 01242 } 01243 } 01244 01248 function getArguments() { 01249 return array(); 01250 } 01251 01255 function getNumberedArguments() { 01256 return array(); 01257 } 01258 01262 function getNamedArguments() { 01263 return array(); 01264 } 01265 01271 function isEmpty() { 01272 return true; 01273 } 01274 01279 function getArgument( $name ) { 01280 return false; 01281 } 01282 01290 function loopCheck( $title ) { 01291 return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] ); 01292 } 01293 01299 function isTemplate() { 01300 return false; 01301 } 01302 01308 function getTitle() { 01309 return $this->title; 01310 } 01311 } 01312 01317 class PPTemplateFrame_Hash extends PPFrame_Hash { 01318 var $numberedArgs, $namedArgs, $parent; 01319 var $numberedExpansionCache, $namedExpansionCache; 01320 01328 function __construct( $preprocessor, $parent = false, $numberedArgs = array(), $namedArgs = array(), $title = false ) { 01329 parent::__construct( $preprocessor ); 01330 01331 $this->parent = $parent; 01332 $this->numberedArgs = $numberedArgs; 01333 $this->namedArgs = $namedArgs; 01334 $this->title = $title; 01335 $pdbk = $title ? $title->getPrefixedDBkey() : false; 01336 $this->titleCache = $parent->titleCache; 01337 $this->titleCache[] = $pdbk; 01338 $this->loopCheckHash = /*clone*/ $parent->loopCheckHash; 01339 if ( $pdbk !== false ) { 01340 $this->loopCheckHash[$pdbk] = true; 01341 } 01342 $this->depth = $parent->depth + 1; 01343 $this->numberedExpansionCache = $this->namedExpansionCache = array(); 01344 } 01345 01346 function __toString() { 01347 $s = 'tplframe{'; 01348 $first = true; 01349 $args = $this->numberedArgs + $this->namedArgs; 01350 foreach ( $args as $name => $value ) { 01351 if ( $first ) { 01352 $first = false; 01353 } else { 01354 $s .= ', '; 01355 } 01356 $s .= "\"$name\":\"" . 01357 str_replace( '"', '\\"', $value->__toString() ) . '"'; 01358 } 01359 $s .= '}'; 01360 return $s; 01361 } 01367 function isEmpty() { 01368 return !count( $this->numberedArgs ) && !count( $this->namedArgs ); 01369 } 01370 01374 function getArguments() { 01375 $arguments = array(); 01376 foreach ( array_merge( 01377 array_keys($this->numberedArgs), 01378 array_keys($this->namedArgs)) as $key ) { 01379 $arguments[$key] = $this->getArgument($key); 01380 } 01381 return $arguments; 01382 } 01383 01387 function getNumberedArguments() { 01388 $arguments = array(); 01389 foreach ( array_keys($this->numberedArgs) as $key ) { 01390 $arguments[$key] = $this->getArgument($key); 01391 } 01392 return $arguments; 01393 } 01394 01398 function getNamedArguments() { 01399 $arguments = array(); 01400 foreach ( array_keys($this->namedArgs) as $key ) { 01401 $arguments[$key] = $this->getArgument($key); 01402 } 01403 return $arguments; 01404 } 01405 01410 function getNumberedArgument( $index ) { 01411 if ( !isset( $this->numberedArgs[$index] ) ) { 01412 return false; 01413 } 01414 if ( !isset( $this->numberedExpansionCache[$index] ) ) { 01415 # No trimming for unnamed arguments 01416 $this->numberedExpansionCache[$index] = $this->parent->expand( $this->numberedArgs[$index], PPFrame::STRIP_COMMENTS ); 01417 } 01418 return $this->numberedExpansionCache[$index]; 01419 } 01420 01425 function getNamedArgument( $name ) { 01426 if ( !isset( $this->namedArgs[$name] ) ) { 01427 return false; 01428 } 01429 if ( !isset( $this->namedExpansionCache[$name] ) ) { 01430 # Trim named arguments post-expand, for backwards compatibility 01431 $this->namedExpansionCache[$name] = trim( 01432 $this->parent->expand( $this->namedArgs[$name], PPFrame::STRIP_COMMENTS ) ); 01433 } 01434 return $this->namedExpansionCache[$name]; 01435 } 01436 01441 function getArgument( $name ) { 01442 $text = $this->getNumberedArgument( $name ); 01443 if ( $text === false ) { 01444 $text = $this->getNamedArgument( $name ); 01445 } 01446 return $text; 01447 } 01448 01454 function isTemplate() { 01455 return true; 01456 } 01457 } 01458 01463 class PPCustomFrame_Hash extends PPFrame_Hash { 01464 var $args; 01465 01466 function __construct( $preprocessor, $args ) { 01467 parent::__construct( $preprocessor ); 01468 $this->args = $args; 01469 } 01470 01471 function __toString() { 01472 $s = 'cstmframe{'; 01473 $first = true; 01474 foreach ( $this->args as $name => $value ) { 01475 if ( $first ) { 01476 $first = false; 01477 } else { 01478 $s .= ', '; 01479 } 01480 $s .= "\"$name\":\"" . 01481 str_replace( '"', '\\"', $value->__toString() ) . '"'; 01482 } 01483 $s .= '}'; 01484 return $s; 01485 } 01486 01490 function isEmpty() { 01491 return !count( $this->args ); 01492 } 01493 01498 function getArgument( $index ) { 01499 if ( !isset( $this->args[$index] ) ) { 01500 return false; 01501 } 01502 return $this->args[$index]; 01503 } 01504 01505 function getArguments() { 01506 return $this->args; 01507 } 01508 } 01509 01513 class PPNode_Hash_Tree implements PPNode { 01514 var $name, $firstChild, $lastChild, $nextSibling; 01515 01516 function __construct( $name ) { 01517 $this->name = $name; 01518 $this->firstChild = $this->lastChild = $this->nextSibling = false; 01519 } 01520 01521 function __toString() { 01522 $inner = ''; 01523 $attribs = ''; 01524 for ( $node = $this->firstChild; $node; $node = $node->nextSibling ) { 01525 if ( $node instanceof PPNode_Hash_Attr ) { 01526 $attribs .= ' ' . $node->name . '="' . htmlspecialchars( $node->value ) . '"'; 01527 } else { 01528 $inner .= $node->__toString(); 01529 } 01530 } 01531 if ( $inner === '' ) { 01532 return "<{$this->name}$attribs/>"; 01533 } else { 01534 return "<{$this->name}$attribs>$inner</{$this->name}>"; 01535 } 01536 } 01537 01543 static function newWithText( $name, $text ) { 01544 $obj = new self( $name ); 01545 $obj->addChild( new PPNode_Hash_Text( $text ) ); 01546 return $obj; 01547 } 01548 01549 function addChild( $node ) { 01550 if ( $this->lastChild === false ) { 01551 $this->firstChild = $this->lastChild = $node; 01552 } else { 01553 $this->lastChild->nextSibling = $node; 01554 $this->lastChild = $node; 01555 } 01556 } 01557 01561 function getChildren() { 01562 $children = array(); 01563 for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) { 01564 $children[] = $child; 01565 } 01566 return new PPNode_Hash_Array( $children ); 01567 } 01568 01569 function getFirstChild() { 01570 return $this->firstChild; 01571 } 01572 01573 function getNextSibling() { 01574 return $this->nextSibling; 01575 } 01576 01577 function getChildrenOfType( $name ) { 01578 $children = array(); 01579 for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) { 01580 if ( isset( $child->name ) && $child->name === $name ) { 01581 $children[] = $child; 01582 } 01583 } 01584 return $children; 01585 } 01586 01590 function getLength() { 01591 return false; 01592 } 01593 01598 function item( $i ) { 01599 return false; 01600 } 01601 01605 function getName() { 01606 return $this->name; 01607 } 01608 01618 function splitArg() { 01619 $bits = array(); 01620 for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) { 01621 if ( !isset( $child->name ) ) { 01622 continue; 01623 } 01624 if ( $child->name === 'name' ) { 01625 $bits['name'] = $child; 01626 if ( $child->firstChild instanceof PPNode_Hash_Attr 01627 && $child->firstChild->name === 'index' ) 01628 { 01629 $bits['index'] = $child->firstChild->value; 01630 } 01631 } elseif ( $child->name === 'value' ) { 01632 $bits['value'] = $child; 01633 } 01634 } 01635 01636 if ( !isset( $bits['name'] ) ) { 01637 throw new MWException( 'Invalid brace node passed to ' . __METHOD__ ); 01638 } 01639 if ( !isset( $bits['index'] ) ) { 01640 $bits['index'] = ''; 01641 } 01642 return $bits; 01643 } 01644 01652 function splitExt() { 01653 $bits = array(); 01654 for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) { 01655 if ( !isset( $child->name ) ) { 01656 continue; 01657 } 01658 if ( $child->name == 'name' ) { 01659 $bits['name'] = $child; 01660 } elseif ( $child->name == 'attr' ) { 01661 $bits['attr'] = $child; 01662 } elseif ( $child->name == 'inner' ) { 01663 $bits['inner'] = $child; 01664 } elseif ( $child->name == 'close' ) { 01665 $bits['close'] = $child; 01666 } 01667 } 01668 if ( !isset( $bits['name'] ) ) { 01669 throw new MWException( 'Invalid ext node passed to ' . __METHOD__ ); 01670 } 01671 return $bits; 01672 } 01673 01680 function splitHeading() { 01681 if ( $this->name !== 'h' ) { 01682 throw new MWException( 'Invalid h node passed to ' . __METHOD__ ); 01683 } 01684 $bits = array(); 01685 for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) { 01686 if ( !isset( $child->name ) ) { 01687 continue; 01688 } 01689 if ( $child->name == 'i' ) { 01690 $bits['i'] = $child->value; 01691 } elseif ( $child->name == 'level' ) { 01692 $bits['level'] = $child->value; 01693 } 01694 } 01695 if ( !isset( $bits['i'] ) ) { 01696 throw new MWException( 'Invalid h node passed to ' . __METHOD__ ); 01697 } 01698 return $bits; 01699 } 01700 01707 function splitTemplate() { 01708 $parts = array(); 01709 $bits = array( 'lineStart' => '' ); 01710 for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) { 01711 if ( !isset( $child->name ) ) { 01712 continue; 01713 } 01714 if ( $child->name == 'title' ) { 01715 $bits['title'] = $child; 01716 } 01717 if ( $child->name == 'part' ) { 01718 $parts[] = $child; 01719 } 01720 if ( $child->name == 'lineStart' ) { 01721 $bits['lineStart'] = '1'; 01722 } 01723 } 01724 if ( !isset( $bits['title'] ) ) { 01725 throw new MWException( 'Invalid node passed to ' . __METHOD__ ); 01726 } 01727 $bits['parts'] = new PPNode_Hash_Array( $parts ); 01728 return $bits; 01729 } 01730 } 01731 01735 class PPNode_Hash_Text implements PPNode { 01736 var $value, $nextSibling; 01737 01738 function __construct( $value ) { 01739 if ( is_object( $value ) ) { 01740 throw new MWException( __CLASS__ . ' given object instead of string' ); 01741 } 01742 $this->value = $value; 01743 } 01744 01745 function __toString() { 01746 return htmlspecialchars( $this->value ); 01747 } 01748 01749 function getNextSibling() { 01750 return $this->nextSibling; 01751 } 01752 01753 function getChildren() { return false; } 01754 function getFirstChild() { return false; } 01755 function getChildrenOfType( $name ) { return false; } 01756 function getLength() { return false; } 01757 function item( $i ) { return false; } 01758 function getName() { return '#text'; } 01759 function splitArg() { throw new MWException( __METHOD__ . ': not supported' ); } 01760 function splitExt() { throw new MWException( __METHOD__ . ': not supported' ); } 01761 function splitHeading() { throw new MWException( __METHOD__ . ': not supported' ); } 01762 } 01763 01767 class PPNode_Hash_Array implements PPNode { 01768 var $value, $nextSibling; 01769 01770 function __construct( $value ) { 01771 $this->value = $value; 01772 } 01773 01774 function __toString() { 01775 return var_export( $this, true ); 01776 } 01777 01778 function getLength() { 01779 return count( $this->value ); 01780 } 01781 01782 function item( $i ) { 01783 return $this->value[$i]; 01784 } 01785 01786 function getName() { return '#nodelist'; } 01787 01788 function getNextSibling() { 01789 return $this->nextSibling; 01790 } 01791 01792 function getChildren() { return false; } 01793 function getFirstChild() { return false; } 01794 function getChildrenOfType( $name ) { return false; } 01795 function splitArg() { throw new MWException( __METHOD__ . ': not supported' ); } 01796 function splitExt() { throw new MWException( __METHOD__ . ': not supported' ); } 01797 function splitHeading() { throw new MWException( __METHOD__ . ': not supported' ); } 01798 } 01799 01803 class PPNode_Hash_Attr implements PPNode { 01804 var $name, $value, $nextSibling; 01805 01806 function __construct( $name, $value ) { 01807 $this->name = $name; 01808 $this->value = $value; 01809 } 01810 01811 function __toString() { 01812 return "<@{$this->name}>" . htmlspecialchars( $this->value ) . "</@{$this->name}>"; 01813 } 01814 01815 function getName() { 01816 return $this->name; 01817 } 01818 01819 function getNextSibling() { 01820 return $this->nextSibling; 01821 } 01822 01823 function getChildren() { return false; } 01824 function getFirstChild() { return false; } 01825 function getChildrenOfType( $name ) { return false; } 01826 function getLength() { return false; } 01827 function item( $i ) { return false; } 01828 function splitArg() { throw new MWException( __METHOD__ . ': not supported' ); } 01829 function splitExt() { throw new MWException( __METHOD__ . ': not supported' ); } 01830 function splitHeading() { throw new MWException( __METHOD__ . ': not supported' ); } 01831 }