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