MediaWiki
master
|
00001 <?php 00027 class Preprocessor_DOM implements Preprocessor { 00028 00032 var $parser; 00033 00034 var $memoryLimit; 00035 00036 const CACHE_VERSION = 1; 00037 00038 function __construct( $parser ) { 00039 $this->parser = $parser; 00040 $mem = ini_get( 'memory_limit' ); 00041 $this->memoryLimit = false; 00042 if ( strval( $mem ) !== '' && $mem != -1 ) { 00043 if ( preg_match( '/^\d+$/', $mem ) ) { 00044 $this->memoryLimit = $mem; 00045 } elseif ( preg_match( '/^(\d+)M$/i', $mem, $m ) ) { 00046 $this->memoryLimit = $m[1] * 1048576; 00047 } 00048 } 00049 } 00050 00054 function newFrame() { 00055 return new PPFrame_DOM( $this ); 00056 } 00057 00062 function newCustomFrame( $args ) { 00063 return new PPCustomFrame_DOM( $this, $args ); 00064 } 00065 00070 function newPartNodeArray( $values ) { 00071 //NOTE: DOM manipulation is slower than building & parsing XML! (or so Tim sais) 00072 $xml = "<list>"; 00073 00074 foreach ( $values as $k => $val ) { 00075 00076 if ( is_int( $k ) ) { 00077 $xml .= "<part><name index=\"$k\"/><value>" . htmlspecialchars( $val ) ."</value></part>"; 00078 } else { 00079 $xml .= "<part><name>" . htmlspecialchars( $k ) . "</name>=<value>" . htmlspecialchars( $val ) . "</value></part>"; 00080 } 00081 } 00082 00083 $xml .= "</list>"; 00084 00085 $dom = new DOMDocument(); 00086 $dom->loadXML( $xml ); 00087 $root = $dom->documentElement; 00088 00089 $node = new PPNode_DOM( $root->childNodes ); 00090 return $node; 00091 } 00092 00097 function memCheck() { 00098 if ( $this->memoryLimit === false ) { 00099 return true; 00100 } 00101 $usage = memory_get_usage(); 00102 if ( $usage > $this->memoryLimit * 0.9 ) { 00103 $limit = intval( $this->memoryLimit * 0.9 / 1048576 + 0.5 ); 00104 throw new MWException( "Preprocessor hit 90% memory limit ($limit MB)" ); 00105 } 00106 return $usage <= $this->memoryLimit * 0.8; 00107 } 00108 00132 function preprocessToObj( $text, $flags = 0 ) { 00133 wfProfileIn( __METHOD__ ); 00134 global $wgMemc, $wgPreprocessorCacheThreshold; 00135 00136 $xml = false; 00137 $cacheable = ( $wgPreprocessorCacheThreshold !== false 00138 && strlen( $text ) > $wgPreprocessorCacheThreshold ); 00139 if ( $cacheable ) { 00140 wfProfileIn( __METHOD__.'-cacheable' ); 00141 00142 $cacheKey = wfMemcKey( 'preprocess-xml', md5($text), $flags ); 00143 $cacheValue = $wgMemc->get( $cacheKey ); 00144 if ( $cacheValue ) { 00145 $version = substr( $cacheValue, 0, 8 ); 00146 if ( intval( $version ) == self::CACHE_VERSION ) { 00147 $xml = substr( $cacheValue, 8 ); 00148 // From the cache 00149 wfDebugLog( "Preprocessor", "Loaded preprocessor XML from memcached (key $cacheKey)" ); 00150 } 00151 } 00152 } 00153 if ( $xml === false ) { 00154 if ( $cacheable ) { 00155 wfProfileIn( __METHOD__.'-cache-miss' ); 00156 $xml = $this->preprocessToXml( $text, $flags ); 00157 $cacheValue = sprintf( "%08d", self::CACHE_VERSION ) . $xml; 00158 $wgMemc->set( $cacheKey, $cacheValue, 86400 ); 00159 wfProfileOut( __METHOD__.'-cache-miss' ); 00160 wfDebugLog( "Preprocessor", "Saved preprocessor XML to memcached (key $cacheKey)" ); 00161 } else { 00162 $xml = $this->preprocessToXml( $text, $flags ); 00163 } 00164 00165 } 00166 00167 // Fail if the number of elements exceeds acceptable limits 00168 // Do not attempt to generate the DOM 00169 $this->parser->mGeneratedPPNodeCount += substr_count( $xml, '<' ); 00170 $max = $this->parser->mOptions->getMaxGeneratedPPNodeCount(); 00171 if ( $this->parser->mGeneratedPPNodeCount > $max ) { 00172 throw new MWException( __METHOD__.': generated node count limit exceeded' ); 00173 } 00174 00175 wfProfileIn( __METHOD__.'-loadXML' ); 00176 $dom = new DOMDocument; 00177 wfSuppressWarnings(); 00178 $result = $dom->loadXML( $xml ); 00179 wfRestoreWarnings(); 00180 if ( !$result ) { 00181 // Try running the XML through UtfNormal to get rid of invalid characters 00182 $xml = UtfNormal::cleanUp( $xml ); 00183 // 1 << 19 == XML_PARSE_HUGE, needed so newer versions of libxml2 don't barf when the XML is >256 levels deep 00184 $result = $dom->loadXML( $xml, 1 << 19 ); 00185 if ( !$result ) { 00186 throw new MWException( __METHOD__.' generated invalid XML' ); 00187 } 00188 } 00189 $obj = new PPNode_DOM( $dom->documentElement ); 00190 wfProfileOut( __METHOD__.'-loadXML' ); 00191 if ( $cacheable ) { 00192 wfProfileOut( __METHOD__.'-cacheable' ); 00193 } 00194 wfProfileOut( __METHOD__ ); 00195 return $obj; 00196 } 00197 00203 function preprocessToXml( $text, $flags = 0 ) { 00204 wfProfileIn( __METHOD__ ); 00205 $rules = array( 00206 '{' => array( 00207 'end' => '}', 00208 'names' => array( 00209 2 => 'template', 00210 3 => 'tplarg', 00211 ), 00212 'min' => 2, 00213 'max' => 3, 00214 ), 00215 '[' => array( 00216 'end' => ']', 00217 'names' => array( 2 => null ), 00218 'min' => 2, 00219 'max' => 2, 00220 ) 00221 ); 00222 00223 $forInclusion = $flags & Parser::PTD_FOR_INCLUSION; 00224 00225 $xmlishElements = $this->parser->getStripList(); 00226 $enableOnlyinclude = false; 00227 if ( $forInclusion ) { 00228 $ignoredTags = array( 'includeonly', '/includeonly' ); 00229 $ignoredElements = array( 'noinclude' ); 00230 $xmlishElements[] = 'noinclude'; 00231 if ( strpos( $text, '<onlyinclude>' ) !== false && strpos( $text, '</onlyinclude>' ) !== false ) { 00232 $enableOnlyinclude = true; 00233 } 00234 } else { 00235 $ignoredTags = array( 'noinclude', '/noinclude', 'onlyinclude', '/onlyinclude' ); 00236 $ignoredElements = array( 'includeonly' ); 00237 $xmlishElements[] = 'includeonly'; 00238 } 00239 $xmlishRegex = implode( '|', array_merge( $xmlishElements, $ignoredTags ) ); 00240 00241 // Use "A" modifier (anchored) instead of "^", because ^ doesn't work with an offset 00242 $elementsRegex = "~($xmlishRegex)(?:\s|\/>|>)|(!--)~iA"; 00243 00244 $stack = new PPDStack; 00245 00246 $searchBase = "[{<\n"; #} 00247 $revText = strrev( $text ); // For fast reverse searches 00248 $lengthText = strlen( $text ); 00249 00250 $i = 0; # Input pointer, starts out pointing to a pseudo-newline before the start 00251 $accum =& $stack->getAccum(); # Current accumulator 00252 $accum = '<root>'; 00253 $findEquals = false; # True to find equals signs in arguments 00254 $findPipe = false; # True to take notice of pipe characters 00255 $headingIndex = 1; 00256 $inHeading = false; # True if $i is inside a possible heading 00257 $noMoreGT = false; # True if there are no more greater-than (>) signs right of $i 00258 $findOnlyinclude = $enableOnlyinclude; # True to ignore all input up to the next <onlyinclude> 00259 $fakeLineStart = true; # Do a line-start run without outputting an LF character 00260 00261 while ( true ) { 00262 //$this->memCheck(); 00263 00264 if ( $findOnlyinclude ) { 00265 // Ignore all input up to the next <onlyinclude> 00266 $startPos = strpos( $text, '<onlyinclude>', $i ); 00267 if ( $startPos === false ) { 00268 // Ignored section runs to the end 00269 $accum .= '<ignore>' . htmlspecialchars( substr( $text, $i ) ) . '</ignore>'; 00270 break; 00271 } 00272 $tagEndPos = $startPos + strlen( '<onlyinclude>' ); // past-the-end 00273 $accum .= '<ignore>' . htmlspecialchars( substr( $text, $i, $tagEndPos - $i ) ) . '</ignore>'; 00274 $i = $tagEndPos; 00275 $findOnlyinclude = false; 00276 } 00277 00278 if ( $fakeLineStart ) { 00279 $found = 'line-start'; 00280 $curChar = ''; 00281 } else { 00282 # Find next opening brace, closing brace or pipe 00283 $search = $searchBase; 00284 if ( $stack->top === false ) { 00285 $currentClosing = ''; 00286 } else { 00287 $currentClosing = $stack->top->close; 00288 $search .= $currentClosing; 00289 } 00290 if ( $findPipe ) { 00291 $search .= '|'; 00292 } 00293 if ( $findEquals ) { 00294 // First equals will be for the template 00295 $search .= '='; 00296 } 00297 $rule = null; 00298 # Output literal section, advance input counter 00299 $literalLength = strcspn( $text, $search, $i ); 00300 if ( $literalLength > 0 ) { 00301 $accum .= htmlspecialchars( substr( $text, $i, $literalLength ) ); 00302 $i += $literalLength; 00303 } 00304 if ( $i >= $lengthText ) { 00305 if ( $currentClosing == "\n" ) { 00306 // Do a past-the-end run to finish off the heading 00307 $curChar = ''; 00308 $found = 'line-end'; 00309 } else { 00310 # All done 00311 break; 00312 } 00313 } else { 00314 $curChar = $text[$i]; 00315 if ( $curChar == '|' ) { 00316 $found = 'pipe'; 00317 } elseif ( $curChar == '=' ) { 00318 $found = 'equals'; 00319 } elseif ( $curChar == '<' ) { 00320 $found = 'angle'; 00321 } elseif ( $curChar == "\n" ) { 00322 if ( $inHeading ) { 00323 $found = 'line-end'; 00324 } else { 00325 $found = 'line-start'; 00326 } 00327 } elseif ( $curChar == $currentClosing ) { 00328 $found = 'close'; 00329 } elseif ( isset( $rules[$curChar] ) ) { 00330 $found = 'open'; 00331 $rule = $rules[$curChar]; 00332 } else { 00333 # Some versions of PHP have a strcspn which stops on null characters 00334 # Ignore and continue 00335 ++$i; 00336 continue; 00337 } 00338 } 00339 } 00340 00341 if ( $found == 'angle' ) { 00342 $matches = false; 00343 // Handle </onlyinclude> 00344 if ( $enableOnlyinclude && substr( $text, $i, strlen( '</onlyinclude>' ) ) == '</onlyinclude>' ) { 00345 $findOnlyinclude = true; 00346 continue; 00347 } 00348 00349 // Determine element name 00350 if ( !preg_match( $elementsRegex, $text, $matches, 0, $i + 1 ) ) { 00351 // Element name missing or not listed 00352 $accum .= '<'; 00353 ++$i; 00354 continue; 00355 } 00356 // Handle comments 00357 if ( isset( $matches[2] ) && $matches[2] == '!--' ) { 00358 // To avoid leaving blank lines, when a comment is both preceded 00359 // and followed by a newline (ignoring spaces), trim leading and 00360 // trailing spaces and one of the newlines. 00361 00362 // Find the end 00363 $endPos = strpos( $text, '-->', $i + 4 ); 00364 if ( $endPos === false ) { 00365 // Unclosed comment in input, runs to end 00366 $inner = substr( $text, $i ); 00367 $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>'; 00368 $i = $lengthText; 00369 } else { 00370 // Search backwards for leading whitespace 00371 $wsStart = $i ? ( $i - strspn( $revText, ' ', $lengthText - $i ) ) : 0; 00372 // Search forwards for trailing whitespace 00373 // $wsEnd will be the position of the last space (or the '>' if there's none) 00374 $wsEnd = $endPos + 2 + strspn( $text, ' ', $endPos + 3 ); 00375 // Eat the line if possible 00376 // TODO: This could theoretically be done if $wsStart == 0, i.e. for comments at 00377 // the overall start. That's not how Sanitizer::removeHTMLcomments() did it, but 00378 // it's a possible beneficial b/c break. 00379 if ( $wsStart > 0 && substr( $text, $wsStart - 1, 1 ) == "\n" 00380 && substr( $text, $wsEnd + 1, 1 ) == "\n" ) 00381 { 00382 $startPos = $wsStart; 00383 $endPos = $wsEnd + 1; 00384 // Remove leading whitespace from the end of the accumulator 00385 // Sanity check first though 00386 $wsLength = $i - $wsStart; 00387 if ( $wsLength > 0 && substr( $accum, -$wsLength ) === str_repeat( ' ', $wsLength ) ) { 00388 $accum = substr( $accum, 0, -$wsLength ); 00389 } 00390 // Do a line-start run next time to look for headings after the comment 00391 $fakeLineStart = true; 00392 } else { 00393 // No line to eat, just take the comment itself 00394 $startPos = $i; 00395 $endPos += 2; 00396 } 00397 00398 if ( $stack->top ) { 00399 $part = $stack->top->getCurrentPart(); 00400 if ( ! (isset( $part->commentEnd ) && $part->commentEnd == $wsStart - 1 )) { 00401 $part->visualEnd = $wsStart; 00402 } 00403 // Else comments abutting, no change in visual end 00404 $part->commentEnd = $endPos; 00405 } 00406 $i = $endPos + 1; 00407 $inner = substr( $text, $startPos, $endPos - $startPos + 1 ); 00408 $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>'; 00409 } 00410 continue; 00411 } 00412 $name = $matches[1]; 00413 $lowerName = strtolower( $name ); 00414 $attrStart = $i + strlen( $name ) + 1; 00415 00416 // Find end of tag 00417 $tagEndPos = $noMoreGT ? false : strpos( $text, '>', $attrStart ); 00418 if ( $tagEndPos === false ) { 00419 // Infinite backtrack 00420 // Disable tag search to prevent worst-case O(N^2) performance 00421 $noMoreGT = true; 00422 $accum .= '<'; 00423 ++$i; 00424 continue; 00425 } 00426 00427 // Handle ignored tags 00428 if ( in_array( $lowerName, $ignoredTags ) ) { 00429 $accum .= '<ignore>' . htmlspecialchars( substr( $text, $i, $tagEndPos - $i + 1 ) ) . '</ignore>'; 00430 $i = $tagEndPos + 1; 00431 continue; 00432 } 00433 00434 $tagStartPos = $i; 00435 if ( $text[$tagEndPos-1] == '/' ) { 00436 $attrEnd = $tagEndPos - 1; 00437 $inner = null; 00438 $i = $tagEndPos + 1; 00439 $close = ''; 00440 } else { 00441 $attrEnd = $tagEndPos; 00442 // Find closing tag 00443 if ( preg_match( "/<\/" . preg_quote( $name, '/' ) . "\s*>/i", 00444 $text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 ) ) 00445 { 00446 $inner = substr( $text, $tagEndPos + 1, $matches[0][1] - $tagEndPos - 1 ); 00447 $i = $matches[0][1] + strlen( $matches[0][0] ); 00448 $close = '<close>' . htmlspecialchars( $matches[0][0] ) . '</close>'; 00449 } else { 00450 // No end tag -- let it run out to the end of the text. 00451 $inner = substr( $text, $tagEndPos + 1 ); 00452 $i = $lengthText; 00453 $close = ''; 00454 } 00455 } 00456 // <includeonly> and <noinclude> just become <ignore> tags 00457 if ( in_array( $lowerName, $ignoredElements ) ) { 00458 $accum .= '<ignore>' . htmlspecialchars( substr( $text, $tagStartPos, $i - $tagStartPos ) ) 00459 . '</ignore>'; 00460 continue; 00461 } 00462 00463 $accum .= '<ext>'; 00464 if ( $attrEnd <= $attrStart ) { 00465 $attr = ''; 00466 } else { 00467 $attr = substr( $text, $attrStart, $attrEnd - $attrStart ); 00468 } 00469 $accum .= '<name>' . htmlspecialchars( $name ) . '</name>' . 00470 // Note that the attr element contains the whitespace between name and attribute, 00471 // this is necessary for precise reconstruction during pre-save transform. 00472 '<attr>' . htmlspecialchars( $attr ) . '</attr>'; 00473 if ( $inner !== null ) { 00474 $accum .= '<inner>' . htmlspecialchars( $inner ) . '</inner>'; 00475 } 00476 $accum .= $close . '</ext>'; 00477 } elseif ( $found == 'line-start' ) { 00478 // Is this the start of a heading? 00479 // Line break belongs before the heading element in any case 00480 if ( $fakeLineStart ) { 00481 $fakeLineStart = false; 00482 } else { 00483 $accum .= $curChar; 00484 $i++; 00485 } 00486 00487 $count = strspn( $text, '=', $i, 6 ); 00488 if ( $count == 1 && $findEquals ) { 00489 // DWIM: This looks kind of like a name/value separator 00490 // Let's let the equals handler have it and break the potential heading 00491 // This is heuristic, but AFAICT the methods for completely correct disambiguation are very complex. 00492 } elseif ( $count > 0 ) { 00493 $piece = array( 00494 'open' => "\n", 00495 'close' => "\n", 00496 'parts' => array( new PPDPart( str_repeat( '=', $count ) ) ), 00497 'startPos' => $i, 00498 'count' => $count ); 00499 $stack->push( $piece ); 00500 $accum =& $stack->getAccum(); 00501 $flags = $stack->getFlags(); 00502 extract( $flags ); 00503 $i += $count; 00504 } 00505 } elseif ( $found == 'line-end' ) { 00506 $piece = $stack->top; 00507 // A heading must be open, otherwise \n wouldn't have been in the search list 00508 assert( '$piece->open == "\n"' ); 00509 $part = $piece->getCurrentPart(); 00510 // Search back through the input to see if it has a proper close 00511 // Do this using the reversed string since the other solutions (end anchor, etc.) are inefficient 00512 $wsLength = strspn( $revText, " \t", $lengthText - $i ); 00513 $searchStart = $i - $wsLength; 00514 if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) { 00515 // Comment found at line end 00516 // Search for equals signs before the comment 00517 $searchStart = $part->visualEnd; 00518 $searchStart -= strspn( $revText, " \t", $lengthText - $searchStart ); 00519 } 00520 $count = $piece->count; 00521 $equalsLength = strspn( $revText, '=', $lengthText - $searchStart ); 00522 if ( $equalsLength > 0 ) { 00523 if ( $searchStart - $equalsLength == $piece->startPos ) { 00524 // This is just a single string of equals signs on its own line 00525 // Replicate the doHeadings behaviour /={count}(.+)={count}/ 00526 // First find out how many equals signs there really are (don't stop at 6) 00527 $count = $equalsLength; 00528 if ( $count < 3 ) { 00529 $count = 0; 00530 } else { 00531 $count = min( 6, intval( ( $count - 1 ) / 2 ) ); 00532 } 00533 } else { 00534 $count = min( $equalsLength, $count ); 00535 } 00536 if ( $count > 0 ) { 00537 // Normal match, output <h> 00538 $element = "<h level=\"$count\" i=\"$headingIndex\">$accum</h>"; 00539 $headingIndex++; 00540 } else { 00541 // Single equals sign on its own line, count=0 00542 $element = $accum; 00543 } 00544 } else { 00545 // No match, no <h>, just pass down the inner text 00546 $element = $accum; 00547 } 00548 // Unwind the stack 00549 $stack->pop(); 00550 $accum =& $stack->getAccum(); 00551 $flags = $stack->getFlags(); 00552 extract( $flags ); 00553 00554 // Append the result to the enclosing accumulator 00555 $accum .= $element; 00556 // Note that we do NOT increment the input pointer. 00557 // This is because the closing linebreak could be the opening linebreak of 00558 // another heading. Infinite loops are avoided because the next iteration MUST 00559 // hit the heading open case above, which unconditionally increments the 00560 // input pointer. 00561 } elseif ( $found == 'open' ) { 00562 # count opening brace characters 00563 $count = strspn( $text, $curChar, $i ); 00564 00565 # we need to add to stack only if opening brace count is enough for one of the rules 00566 if ( $count >= $rule['min'] ) { 00567 # Add it to the stack 00568 $piece = array( 00569 'open' => $curChar, 00570 'close' => $rule['end'], 00571 'count' => $count, 00572 'lineStart' => ($i > 0 && $text[$i-1] == "\n"), 00573 ); 00574 00575 $stack->push( $piece ); 00576 $accum =& $stack->getAccum(); 00577 $flags = $stack->getFlags(); 00578 extract( $flags ); 00579 } else { 00580 # Add literal brace(s) 00581 $accum .= htmlspecialchars( str_repeat( $curChar, $count ) ); 00582 } 00583 $i += $count; 00584 } elseif ( $found == 'close' ) { 00585 $piece = $stack->top; 00586 # lets check if there are enough characters for closing brace 00587 $maxCount = $piece->count; 00588 $count = strspn( $text, $curChar, $i, $maxCount ); 00589 00590 # check for maximum matching characters (if there are 5 closing 00591 # characters, we will probably need only 3 - depending on the rules) 00592 $rule = $rules[$piece->open]; 00593 if ( $count > $rule['max'] ) { 00594 # The specified maximum exists in the callback array, unless the caller 00595 # has made an error 00596 $matchingCount = $rule['max']; 00597 } else { 00598 # Count is less than the maximum 00599 # Skip any gaps in the callback array to find the true largest match 00600 # Need to use array_key_exists not isset because the callback can be null 00601 $matchingCount = $count; 00602 while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $rule['names'] ) ) { 00603 --$matchingCount; 00604 } 00605 } 00606 00607 if ( $matchingCount <= 0 ) { 00608 # No matching element found in callback array 00609 # Output a literal closing brace and continue 00610 $accum .= htmlspecialchars( str_repeat( $curChar, $count ) ); 00611 $i += $count; 00612 continue; 00613 } 00614 $name = $rule['names'][$matchingCount]; 00615 if ( $name === null ) { 00616 // No element, just literal text 00617 $element = $piece->breakSyntax( $matchingCount ) . str_repeat( $rule['end'], $matchingCount ); 00618 } else { 00619 # Create XML element 00620 # Note: $parts is already XML, does not need to be encoded further 00621 $parts = $piece->parts; 00622 $title = $parts[0]->out; 00623 unset( $parts[0] ); 00624 00625 # The invocation is at the start of the line if lineStart is set in 00626 # the stack, and all opening brackets are used up. 00627 if ( $maxCount == $matchingCount && !empty( $piece->lineStart ) ) { 00628 $attr = ' lineStart="1"'; 00629 } else { 00630 $attr = ''; 00631 } 00632 00633 $element = "<$name$attr>"; 00634 $element .= "<title>$title</title>"; 00635 $argIndex = 1; 00636 foreach ( $parts as $part ) { 00637 if ( isset( $part->eqpos ) ) { 00638 $argName = substr( $part->out, 0, $part->eqpos ); 00639 $argValue = substr( $part->out, $part->eqpos + 1 ); 00640 $element .= "<part><name>$argName</name>=<value>$argValue</value></part>"; 00641 } else { 00642 $element .= "<part><name index=\"$argIndex\" /><value>{$part->out}</value></part>"; 00643 $argIndex++; 00644 } 00645 } 00646 $element .= "</$name>"; 00647 } 00648 00649 # Advance input pointer 00650 $i += $matchingCount; 00651 00652 # Unwind the stack 00653 $stack->pop(); 00654 $accum =& $stack->getAccum(); 00655 00656 # Re-add the old stack element if it still has unmatched opening characters remaining 00657 if ( $matchingCount < $piece->count ) { 00658 $piece->parts = array( new PPDPart ); 00659 $piece->count -= $matchingCount; 00660 # do we still qualify for any callback with remaining count? 00661 $names = $rules[$piece->open]['names']; 00662 $skippedBraces = 0; 00663 $enclosingAccum =& $accum; 00664 while ( $piece->count ) { 00665 if ( array_key_exists( $piece->count, $names ) ) { 00666 $stack->push( $piece ); 00667 $accum =& $stack->getAccum(); 00668 break; 00669 } 00670 --$piece->count; 00671 $skippedBraces ++; 00672 } 00673 $enclosingAccum .= str_repeat( $piece->open, $skippedBraces ); 00674 } 00675 $flags = $stack->getFlags(); 00676 extract( $flags ); 00677 00678 # Add XML element to the enclosing accumulator 00679 $accum .= $element; 00680 } elseif ( $found == 'pipe' ) { 00681 $findEquals = true; // shortcut for getFlags() 00682 $stack->addPart(); 00683 $accum =& $stack->getAccum(); 00684 ++$i; 00685 } elseif ( $found == 'equals' ) { 00686 $findEquals = false; // shortcut for getFlags() 00687 $stack->getCurrentPart()->eqpos = strlen( $accum ); 00688 $accum .= '='; 00689 ++$i; 00690 } 00691 } 00692 00693 # Output any remaining unclosed brackets 00694 foreach ( $stack->stack as $piece ) { 00695 $stack->rootAccum .= $piece->breakSyntax(); 00696 } 00697 $stack->rootAccum .= '</root>'; 00698 $xml = $stack->rootAccum; 00699 00700 wfProfileOut( __METHOD__ ); 00701 00702 return $xml; 00703 } 00704 } 00705 00710 class PPDStack { 00711 var $stack, $rootAccum; 00712 00716 var $top; 00717 var $out; 00718 var $elementClass = 'PPDStackElement'; 00719 00720 static $false = false; 00721 00722 function __construct() { 00723 $this->stack = array(); 00724 $this->top = false; 00725 $this->rootAccum = ''; 00726 $this->accum =& $this->rootAccum; 00727 } 00728 00732 function count() { 00733 return count( $this->stack ); 00734 } 00735 00736 function &getAccum() { 00737 return $this->accum; 00738 } 00739 00740 function getCurrentPart() { 00741 if ( $this->top === false ) { 00742 return false; 00743 } else { 00744 return $this->top->getCurrentPart(); 00745 } 00746 } 00747 00748 function push( $data ) { 00749 if ( $data instanceof $this->elementClass ) { 00750 $this->stack[] = $data; 00751 } else { 00752 $class = $this->elementClass; 00753 $this->stack[] = new $class( $data ); 00754 } 00755 $this->top = $this->stack[ count( $this->stack ) - 1 ]; 00756 $this->accum =& $this->top->getAccum(); 00757 } 00758 00759 function pop() { 00760 if ( !count( $this->stack ) ) { 00761 throw new MWException( __METHOD__.': no elements remaining' ); 00762 } 00763 $temp = array_pop( $this->stack ); 00764 00765 if ( count( $this->stack ) ) { 00766 $this->top = $this->stack[ count( $this->stack ) - 1 ]; 00767 $this->accum =& $this->top->getAccum(); 00768 } else { 00769 $this->top = self::$false; 00770 $this->accum =& $this->rootAccum; 00771 } 00772 return $temp; 00773 } 00774 00775 function addPart( $s = '' ) { 00776 $this->top->addPart( $s ); 00777 $this->accum =& $this->top->getAccum(); 00778 } 00779 00783 function getFlags() { 00784 if ( !count( $this->stack ) ) { 00785 return array( 00786 'findEquals' => false, 00787 'findPipe' => false, 00788 'inHeading' => false, 00789 ); 00790 } else { 00791 return $this->top->getFlags(); 00792 } 00793 } 00794 } 00795 00799 class PPDStackElement { 00800 var $open, // Opening character (\n for heading) 00801 $close, // Matching closing character 00802 $count, // Number of opening characters found (number of "=" for heading) 00803 $parts, // Array of PPDPart objects describing pipe-separated parts. 00804 $lineStart; // True if the open char appeared at the start of the input line. Not set for headings. 00805 00806 var $partClass = 'PPDPart'; 00807 00808 function __construct( $data = array() ) { 00809 $class = $this->partClass; 00810 $this->parts = array( new $class ); 00811 00812 foreach ( $data as $name => $value ) { 00813 $this->$name = $value; 00814 } 00815 } 00816 00817 function &getAccum() { 00818 return $this->parts[count($this->parts) - 1]->out; 00819 } 00820 00821 function addPart( $s = '' ) { 00822 $class = $this->partClass; 00823 $this->parts[] = new $class( $s ); 00824 } 00825 00826 function getCurrentPart() { 00827 return $this->parts[count($this->parts) - 1]; 00828 } 00829 00833 function getFlags() { 00834 $partCount = count( $this->parts ); 00835 $findPipe = $this->open != "\n" && $this->open != '['; 00836 return array( 00837 'findPipe' => $findPipe, 00838 'findEquals' => $findPipe && $partCount > 1 && !isset( $this->parts[$partCount - 1]->eqpos ), 00839 'inHeading' => $this->open == "\n", 00840 ); 00841 } 00842 00848 function breakSyntax( $openingCount = false ) { 00849 if ( $this->open == "\n" ) { 00850 $s = $this->parts[0]->out; 00851 } else { 00852 if ( $openingCount === false ) { 00853 $openingCount = $this->count; 00854 } 00855 $s = str_repeat( $this->open, $openingCount ); 00856 $first = true; 00857 foreach ( $this->parts as $part ) { 00858 if ( $first ) { 00859 $first = false; 00860 } else { 00861 $s .= '|'; 00862 } 00863 $s .= $part->out; 00864 } 00865 } 00866 return $s; 00867 } 00868 } 00869 00873 class PPDPart { 00874 var $out; // Output accumulator string 00875 00876 // Optional member variables: 00877 // eqpos Position of equals sign in output accumulator 00878 // commentEnd Past-the-end input pointer for the last comment encountered 00879 // visualEnd Past-the-end input pointer for the end of the accumulator minus comments 00880 00881 function __construct( $out = '' ) { 00882 $this->out = $out; 00883 } 00884 } 00885 00890 class PPFrame_DOM implements PPFrame { 00891 00895 var $preprocessor; 00896 00900 var $parser; 00901 00905 var $title; 00906 var $titleCache; 00907 00912 var $loopCheckHash; 00913 00918 var $depth; 00919 00920 00925 function __construct( $preprocessor ) { 00926 $this->preprocessor = $preprocessor; 00927 $this->parser = $preprocessor->parser; 00928 $this->title = $this->parser->mTitle; 00929 $this->titleCache = array( $this->title ? $this->title->getPrefixedDBkey() : false ); 00930 $this->loopCheckHash = array(); 00931 $this->depth = 0; 00932 } 00933 00940 function newChild( $args = false, $title = false, $indexOffset = 0 ) { 00941 $namedArgs = array(); 00942 $numberedArgs = array(); 00943 if ( $title === false ) { 00944 $title = $this->title; 00945 } 00946 if ( $args !== false ) { 00947 $xpath = false; 00948 if ( $args instanceof PPNode ) { 00949 $args = $args->node; 00950 } 00951 foreach ( $args as $arg ) { 00952 if ( $arg instanceof PPNode ) { 00953 $arg = $arg->node; 00954 } 00955 if ( !$xpath ) { 00956 $xpath = new DOMXPath( $arg->ownerDocument ); 00957 } 00958 00959 $nameNodes = $xpath->query( 'name', $arg ); 00960 $value = $xpath->query( 'value', $arg ); 00961 if ( $nameNodes->item( 0 )->hasAttributes() ) { 00962 // Numbered parameter 00963 $index = $nameNodes->item( 0 )->attributes->getNamedItem( 'index' )->textContent; 00964 $index = $index - $indexOffset; 00965 $numberedArgs[$index] = $value->item( 0 ); 00966 unset( $namedArgs[$index] ); 00967 } else { 00968 // Named parameter 00969 $name = trim( $this->expand( $nameNodes->item( 0 ), PPFrame::STRIP_COMMENTS ) ); 00970 $namedArgs[$name] = $value->item( 0 ); 00971 unset( $numberedArgs[$name] ); 00972 } 00973 } 00974 } 00975 return new PPTemplateFrame_DOM( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title ); 00976 } 00977 00984 function expand( $root, $flags = 0 ) { 00985 static $expansionDepth = 0; 00986 if ( is_string( $root ) ) { 00987 return $root; 00988 } 00989 00990 if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->getMaxPPNodeCount() ) { 00991 $this->parser->limitationWarn( 'node-count-exceeded', 00992 $this->parser->mPPNodeCount, 00993 $this->parser->mOptions->getMaxPPNodeCount() 00994 ); 00995 return '<span class="error">Node-count limit exceeded</span>'; 00996 } 00997 00998 if ( $expansionDepth > $this->parser->mOptions->getMaxPPExpandDepth() ) { 00999 $this->parser->limitationWarn( 'expansion-depth-exceeded', 01000 $expansionDepth, 01001 $this->parser->mOptions->getMaxPPExpandDepth() 01002 ); 01003 return '<span class="error">Expansion depth limit exceeded</span>'; 01004 } 01005 wfProfileIn( __METHOD__ ); 01006 ++$expansionDepth; 01007 if ( $expansionDepth > $this->parser->mHighestExpansionDepth ) { 01008 $this->parser->mHighestExpansionDepth = $expansionDepth; 01009 } 01010 01011 if ( $root instanceof PPNode_DOM ) { 01012 $root = $root->node; 01013 } 01014 if ( $root instanceof DOMDocument ) { 01015 $root = $root->documentElement; 01016 } 01017 01018 $outStack = array( '', '' ); 01019 $iteratorStack = array( false, $root ); 01020 $indexStack = array( 0, 0 ); 01021 01022 while ( count( $iteratorStack ) > 1 ) { 01023 $level = count( $outStack ) - 1; 01024 $iteratorNode =& $iteratorStack[ $level ]; 01025 $out =& $outStack[$level]; 01026 $index =& $indexStack[$level]; 01027 01028 if ( $iteratorNode instanceof PPNode_DOM ) $iteratorNode = $iteratorNode->node; 01029 01030 if ( is_array( $iteratorNode ) ) { 01031 if ( $index >= count( $iteratorNode ) ) { 01032 // All done with this iterator 01033 $iteratorStack[$level] = false; 01034 $contextNode = false; 01035 } else { 01036 $contextNode = $iteratorNode[$index]; 01037 $index++; 01038 } 01039 } elseif ( $iteratorNode instanceof DOMNodeList ) { 01040 if ( $index >= $iteratorNode->length ) { 01041 // All done with this iterator 01042 $iteratorStack[$level] = false; 01043 $contextNode = false; 01044 } else { 01045 $contextNode = $iteratorNode->item( $index ); 01046 $index++; 01047 } 01048 } else { 01049 // Copy to $contextNode and then delete from iterator stack, 01050 // because this is not an iterator but we do have to execute it once 01051 $contextNode = $iteratorStack[$level]; 01052 $iteratorStack[$level] = false; 01053 } 01054 01055 if ( $contextNode instanceof PPNode_DOM ) { 01056 $contextNode = $contextNode->node; 01057 } 01058 01059 $newIterator = false; 01060 01061 if ( $contextNode === false ) { 01062 // nothing to do 01063 } elseif ( is_string( $contextNode ) ) { 01064 $out .= $contextNode; 01065 } elseif ( is_array( $contextNode ) || $contextNode instanceof DOMNodeList ) { 01066 $newIterator = $contextNode; 01067 } elseif ( $contextNode instanceof DOMNode ) { 01068 if ( $contextNode->nodeType == XML_TEXT_NODE ) { 01069 $out .= $contextNode->nodeValue; 01070 } elseif ( $contextNode->nodeName == 'template' ) { 01071 # Double-brace expansion 01072 $xpath = new DOMXPath( $contextNode->ownerDocument ); 01073 $titles = $xpath->query( 'title', $contextNode ); 01074 $title = $titles->item( 0 ); 01075 $parts = $xpath->query( 'part', $contextNode ); 01076 if ( $flags & PPFrame::NO_TEMPLATES ) { 01077 $newIterator = $this->virtualBracketedImplode( '{{', '|', '}}', $title, $parts ); 01078 } else { 01079 $lineStart = $contextNode->getAttribute( 'lineStart' ); 01080 $params = array( 01081 'title' => new PPNode_DOM( $title ), 01082 'parts' => new PPNode_DOM( $parts ), 01083 'lineStart' => $lineStart ); 01084 $ret = $this->parser->braceSubstitution( $params, $this ); 01085 if ( isset( $ret['object'] ) ) { 01086 $newIterator = $ret['object']; 01087 } else { 01088 $out .= $ret['text']; 01089 } 01090 } 01091 } elseif ( $contextNode->nodeName == 'tplarg' ) { 01092 # Triple-brace expansion 01093 $xpath = new DOMXPath( $contextNode->ownerDocument ); 01094 $titles = $xpath->query( 'title', $contextNode ); 01095 $title = $titles->item( 0 ); 01096 $parts = $xpath->query( 'part', $contextNode ); 01097 if ( $flags & PPFrame::NO_ARGS ) { 01098 $newIterator = $this->virtualBracketedImplode( '{{{', '|', '}}}', $title, $parts ); 01099 } else { 01100 $params = array( 01101 'title' => new PPNode_DOM( $title ), 01102 'parts' => new PPNode_DOM( $parts ) ); 01103 $ret = $this->parser->argSubstitution( $params, $this ); 01104 if ( isset( $ret['object'] ) ) { 01105 $newIterator = $ret['object']; 01106 } else { 01107 $out .= $ret['text']; 01108 } 01109 } 01110 } elseif ( $contextNode->nodeName == 'comment' ) { 01111 # HTML-style comment 01112 # Remove it in HTML, pre+remove and STRIP_COMMENTS modes 01113 if ( $this->parser->ot['html'] 01114 || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() ) 01115 || ( $flags & PPFrame::STRIP_COMMENTS ) ) 01116 { 01117 $out .= ''; 01118 } 01119 # Add a strip marker in PST mode so that pstPass2() can run some old-fashioned regexes on the result 01120 # Not in RECOVER_COMMENTS mode (extractSections) though 01121 elseif ( $this->parser->ot['wiki'] && ! ( $flags & PPFrame::RECOVER_COMMENTS ) ) { 01122 $out .= $this->parser->insertStripItem( $contextNode->textContent ); 01123 } 01124 # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove 01125 else { 01126 $out .= $contextNode->textContent; 01127 } 01128 } elseif ( $contextNode->nodeName == 'ignore' ) { 01129 # Output suppression used by <includeonly> etc. 01130 # OT_WIKI will only respect <ignore> in substed templates. 01131 # The other output types respect it unless NO_IGNORE is set. 01132 # extractSections() sets NO_IGNORE and so never respects it. 01133 if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] ) || ( $flags & PPFrame::NO_IGNORE ) ) { 01134 $out .= $contextNode->textContent; 01135 } else { 01136 $out .= ''; 01137 } 01138 } elseif ( $contextNode->nodeName == 'ext' ) { 01139 # Extension tag 01140 $xpath = new DOMXPath( $contextNode->ownerDocument ); 01141 $names = $xpath->query( 'name', $contextNode ); 01142 $attrs = $xpath->query( 'attr', $contextNode ); 01143 $inners = $xpath->query( 'inner', $contextNode ); 01144 $closes = $xpath->query( 'close', $contextNode ); 01145 $params = array( 01146 'name' => new PPNode_DOM( $names->item( 0 ) ), 01147 'attr' => $attrs->length > 0 ? new PPNode_DOM( $attrs->item( 0 ) ) : null, 01148 'inner' => $inners->length > 0 ? new PPNode_DOM( $inners->item( 0 ) ) : null, 01149 'close' => $closes->length > 0 ? new PPNode_DOM( $closes->item( 0 ) ) : null, 01150 ); 01151 $out .= $this->parser->extensionSubstitution( $params, $this ); 01152 } elseif ( $contextNode->nodeName == 'h' ) { 01153 # Heading 01154 $s = $this->expand( $contextNode->childNodes, $flags ); 01155 01156 # Insert a heading marker only for <h> children of <root> 01157 # This is to stop extractSections from going over multiple tree levels 01158 if ( $contextNode->parentNode->nodeName == 'root' 01159 && $this->parser->ot['html'] ) 01160 { 01161 # Insert heading index marker 01162 $headingIndex = $contextNode->getAttribute( 'i' ); 01163 $titleText = $this->title->getPrefixedDBkey(); 01164 $this->parser->mHeadings[] = array( $titleText, $headingIndex ); 01165 $serial = count( $this->parser->mHeadings ) - 1; 01166 $marker = "{$this->parser->mUniqPrefix}-h-$serial-" . Parser::MARKER_SUFFIX; 01167 $count = $contextNode->getAttribute( 'level' ); 01168 $s = substr( $s, 0, $count ) . $marker . substr( $s, $count ); 01169 $this->parser->mStripState->addGeneral( $marker, '' ); 01170 } 01171 $out .= $s; 01172 } else { 01173 # Generic recursive expansion 01174 $newIterator = $contextNode->childNodes; 01175 } 01176 } else { 01177 wfProfileOut( __METHOD__ ); 01178 throw new MWException( __METHOD__.': Invalid parameter type' ); 01179 } 01180 01181 if ( $newIterator !== false ) { 01182 if ( $newIterator instanceof PPNode_DOM ) { 01183 $newIterator = $newIterator->node; 01184 } 01185 $outStack[] = ''; 01186 $iteratorStack[] = $newIterator; 01187 $indexStack[] = 0; 01188 } elseif ( $iteratorStack[$level] === false ) { 01189 // Return accumulated value to parent 01190 // With tail recursion 01191 while ( $iteratorStack[$level] === false && $level > 0 ) { 01192 $outStack[$level - 1] .= $out; 01193 array_pop( $outStack ); 01194 array_pop( $iteratorStack ); 01195 array_pop( $indexStack ); 01196 $level--; 01197 } 01198 } 01199 } 01200 --$expansionDepth; 01201 wfProfileOut( __METHOD__ ); 01202 return $outStack[0]; 01203 } 01204 01210 function implodeWithFlags( $sep, $flags /*, ... */ ) { 01211 $args = array_slice( func_get_args(), 2 ); 01212 01213 $first = true; 01214 $s = ''; 01215 foreach ( $args as $root ) { 01216 if ( $root instanceof PPNode_DOM ) $root = $root->node; 01217 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) { 01218 $root = array( $root ); 01219 } 01220 foreach ( $root as $node ) { 01221 if ( $first ) { 01222 $first = false; 01223 } else { 01224 $s .= $sep; 01225 } 01226 $s .= $this->expand( $node, $flags ); 01227 } 01228 } 01229 return $s; 01230 } 01231 01238 function implode( $sep /*, ... */ ) { 01239 $args = array_slice( func_get_args(), 1 ); 01240 01241 $first = true; 01242 $s = ''; 01243 foreach ( $args as $root ) { 01244 if ( $root instanceof PPNode_DOM ) { 01245 $root = $root->node; 01246 } 01247 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) { 01248 $root = array( $root ); 01249 } 01250 foreach ( $root as $node ) { 01251 if ( $first ) { 01252 $first = false; 01253 } else { 01254 $s .= $sep; 01255 } 01256 $s .= $this->expand( $node ); 01257 } 01258 } 01259 return $s; 01260 } 01261 01268 function virtualImplode( $sep /*, ... */ ) { 01269 $args = array_slice( func_get_args(), 1 ); 01270 $out = array(); 01271 $first = true; 01272 01273 foreach ( $args as $root ) { 01274 if ( $root instanceof PPNode_DOM ) { 01275 $root = $root->node; 01276 } 01277 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) { 01278 $root = array( $root ); 01279 } 01280 foreach ( $root as $node ) { 01281 if ( $first ) { 01282 $first = false; 01283 } else { 01284 $out[] = $sep; 01285 } 01286 $out[] = $node; 01287 } 01288 } 01289 return $out; 01290 } 01291 01296 function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) { 01297 $args = array_slice( func_get_args(), 3 ); 01298 $out = array( $start ); 01299 $first = true; 01300 01301 foreach ( $args as $root ) { 01302 if ( $root instanceof PPNode_DOM ) { 01303 $root = $root->node; 01304 } 01305 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) { 01306 $root = array( $root ); 01307 } 01308 foreach ( $root as $node ) { 01309 if ( $first ) { 01310 $first = false; 01311 } else { 01312 $out[] = $sep; 01313 } 01314 $out[] = $node; 01315 } 01316 } 01317 $out[] = $end; 01318 return $out; 01319 } 01320 01321 function __toString() { 01322 return 'frame{}'; 01323 } 01324 01325 function getPDBK( $level = false ) { 01326 if ( $level === false ) { 01327 return $this->title->getPrefixedDBkey(); 01328 } else { 01329 return isset( $this->titleCache[$level] ) ? $this->titleCache[$level] : false; 01330 } 01331 } 01332 01336 function getArguments() { 01337 return array(); 01338 } 01339 01343 function getNumberedArguments() { 01344 return array(); 01345 } 01346 01350 function getNamedArguments() { 01351 return array(); 01352 } 01353 01359 function isEmpty() { 01360 return true; 01361 } 01362 01363 function getArgument( $name ) { 01364 return false; 01365 } 01366 01372 function loopCheck( $title ) { 01373 return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] ); 01374 } 01375 01381 function isTemplate() { 01382 return false; 01383 } 01384 01390 function getTitle() { 01391 return $this->title; 01392 } 01393 } 01394 01399 class PPTemplateFrame_DOM extends PPFrame_DOM { 01400 var $numberedArgs, $namedArgs; 01401 01405 var $parent; 01406 var $numberedExpansionCache, $namedExpansionCache; 01407 01415 function __construct( $preprocessor, $parent = false, $numberedArgs = array(), $namedArgs = array(), $title = false ) { 01416 parent::__construct( $preprocessor ); 01417 01418 $this->parent = $parent; 01419 $this->numberedArgs = $numberedArgs; 01420 $this->namedArgs = $namedArgs; 01421 $this->title = $title; 01422 $pdbk = $title ? $title->getPrefixedDBkey() : false; 01423 $this->titleCache = $parent->titleCache; 01424 $this->titleCache[] = $pdbk; 01425 $this->loopCheckHash = /*clone*/ $parent->loopCheckHash; 01426 if ( $pdbk !== false ) { 01427 $this->loopCheckHash[$pdbk] = true; 01428 } 01429 $this->depth = $parent->depth + 1; 01430 $this->numberedExpansionCache = $this->namedExpansionCache = array(); 01431 } 01432 01433 function __toString() { 01434 $s = 'tplframe{'; 01435 $first = true; 01436 $args = $this->numberedArgs + $this->namedArgs; 01437 foreach ( $args as $name => $value ) { 01438 if ( $first ) { 01439 $first = false; 01440 } else { 01441 $s .= ', '; 01442 } 01443 $s .= "\"$name\":\"" . 01444 str_replace( '"', '\\"', $value->ownerDocument->saveXML( $value ) ) . '"'; 01445 } 01446 $s .= '}'; 01447 return $s; 01448 } 01449 01455 function isEmpty() { 01456 return !count( $this->numberedArgs ) && !count( $this->namedArgs ); 01457 } 01458 01459 function getArguments() { 01460 $arguments = array(); 01461 foreach ( array_merge( 01462 array_keys($this->numberedArgs), 01463 array_keys($this->namedArgs)) as $key ) { 01464 $arguments[$key] = $this->getArgument($key); 01465 } 01466 return $arguments; 01467 } 01468 01469 function getNumberedArguments() { 01470 $arguments = array(); 01471 foreach ( array_keys($this->numberedArgs) as $key ) { 01472 $arguments[$key] = $this->getArgument($key); 01473 } 01474 return $arguments; 01475 } 01476 01477 function getNamedArguments() { 01478 $arguments = array(); 01479 foreach ( array_keys($this->namedArgs) as $key ) { 01480 $arguments[$key] = $this->getArgument($key); 01481 } 01482 return $arguments; 01483 } 01484 01485 function getNumberedArgument( $index ) { 01486 if ( !isset( $this->numberedArgs[$index] ) ) { 01487 return false; 01488 } 01489 if ( !isset( $this->numberedExpansionCache[$index] ) ) { 01490 # No trimming for unnamed arguments 01491 $this->numberedExpansionCache[$index] = $this->parent->expand( $this->numberedArgs[$index], PPFrame::STRIP_COMMENTS ); 01492 } 01493 return $this->numberedExpansionCache[$index]; 01494 } 01495 01496 function getNamedArgument( $name ) { 01497 if ( !isset( $this->namedArgs[$name] ) ) { 01498 return false; 01499 } 01500 if ( !isset( $this->namedExpansionCache[$name] ) ) { 01501 # Trim named arguments post-expand, for backwards compatibility 01502 $this->namedExpansionCache[$name] = trim( 01503 $this->parent->expand( $this->namedArgs[$name], PPFrame::STRIP_COMMENTS ) ); 01504 } 01505 return $this->namedExpansionCache[$name]; 01506 } 01507 01508 function getArgument( $name ) { 01509 $text = $this->getNumberedArgument( $name ); 01510 if ( $text === false ) { 01511 $text = $this->getNamedArgument( $name ); 01512 } 01513 return $text; 01514 } 01515 01521 function isTemplate() { 01522 return true; 01523 } 01524 } 01525 01530 class PPCustomFrame_DOM extends PPFrame_DOM { 01531 var $args; 01532 01533 function __construct( $preprocessor, $args ) { 01534 parent::__construct( $preprocessor ); 01535 $this->args = $args; 01536 } 01537 01538 function __toString() { 01539 $s = 'cstmframe{'; 01540 $first = true; 01541 foreach ( $this->args as $name => $value ) { 01542 if ( $first ) { 01543 $first = false; 01544 } else { 01545 $s .= ', '; 01546 } 01547 $s .= "\"$name\":\"" . 01548 str_replace( '"', '\\"', $value->__toString() ) . '"'; 01549 } 01550 $s .= '}'; 01551 return $s; 01552 } 01553 01557 function isEmpty() { 01558 return !count( $this->args ); 01559 } 01560 01561 function getArgument( $index ) { 01562 if ( !isset( $this->args[$index] ) ) { 01563 return false; 01564 } 01565 return $this->args[$index]; 01566 } 01567 01568 function getArguments() { 01569 return $this->args; 01570 } 01571 } 01572 01576 class PPNode_DOM implements PPNode { 01577 01581 var $node; 01582 var $xpath; 01583 01584 function __construct( $node, $xpath = false ) { 01585 $this->node = $node; 01586 } 01587 01591 function getXPath() { 01592 if ( $this->xpath === null ) { 01593 $this->xpath = new DOMXPath( $this->node->ownerDocument ); 01594 } 01595 return $this->xpath; 01596 } 01597 01598 function __toString() { 01599 if ( $this->node instanceof DOMNodeList ) { 01600 $s = ''; 01601 foreach ( $this->node as $node ) { 01602 $s .= $node->ownerDocument->saveXML( $node ); 01603 } 01604 } else { 01605 $s = $this->node->ownerDocument->saveXML( $this->node ); 01606 } 01607 return $s; 01608 } 01609 01613 function getChildren() { 01614 return $this->node->childNodes ? new self( $this->node->childNodes ) : false; 01615 } 01616 01620 function getFirstChild() { 01621 return $this->node->firstChild ? new self( $this->node->firstChild ) : false; 01622 } 01623 01627 function getNextSibling() { 01628 return $this->node->nextSibling ? new self( $this->node->nextSibling ) : false; 01629 } 01630 01636 function getChildrenOfType( $type ) { 01637 return new self( $this->getXPath()->query( $type, $this->node ) ); 01638 } 01639 01643 function getLength() { 01644 if ( $this->node instanceof DOMNodeList ) { 01645 return $this->node->length; 01646 } else { 01647 return false; 01648 } 01649 } 01650 01655 function item( $i ) { 01656 $item = $this->node->item( $i ); 01657 return $item ? new self( $item ) : false; 01658 } 01659 01663 function getName() { 01664 if ( $this->node instanceof DOMNodeList ) { 01665 return '#nodelist'; 01666 } else { 01667 return $this->node->nodeName; 01668 } 01669 } 01670 01680 function splitArg() { 01681 $xpath = $this->getXPath(); 01682 $names = $xpath->query( 'name', $this->node ); 01683 $values = $xpath->query( 'value', $this->node ); 01684 if ( !$names->length || !$values->length ) { 01685 throw new MWException( 'Invalid brace node passed to ' . __METHOD__ ); 01686 } 01687 $name = $names->item( 0 ); 01688 $index = $name->getAttribute( 'index' ); 01689 return array( 01690 'name' => new self( $name ), 01691 'index' => $index, 01692 'value' => new self( $values->item( 0 ) ) ); 01693 } 01694 01702 function splitExt() { 01703 $xpath = $this->getXPath(); 01704 $names = $xpath->query( 'name', $this->node ); 01705 $attrs = $xpath->query( 'attr', $this->node ); 01706 $inners = $xpath->query( 'inner', $this->node ); 01707 $closes = $xpath->query( 'close', $this->node ); 01708 if ( !$names->length || !$attrs->length ) { 01709 throw new MWException( 'Invalid ext node passed to ' . __METHOD__ ); 01710 } 01711 $parts = array( 01712 'name' => new self( $names->item( 0 ) ), 01713 'attr' => new self( $attrs->item( 0 ) ) ); 01714 if ( $inners->length ) { 01715 $parts['inner'] = new self( $inners->item( 0 ) ); 01716 } 01717 if ( $closes->length ) { 01718 $parts['close'] = new self( $closes->item( 0 ) ); 01719 } 01720 return $parts; 01721 } 01722 01728 function splitHeading() { 01729 if ( $this->getName() !== 'h' ) { 01730 throw new MWException( 'Invalid h node passed to ' . __METHOD__ ); 01731 } 01732 return array( 01733 'i' => $this->node->getAttribute( 'i' ), 01734 'level' => $this->node->getAttribute( 'level' ), 01735 'contents' => $this->getChildren() 01736 ); 01737 } 01738 }