MediaWiki  REL1_19
Preprocessor_DOM.php
Go to the documentation of this file.
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 .= '&lt;';
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 .= '&lt;';
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 }