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