[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

/includes/parser/ -> Preprocessor_DOM.php (source)

   1  <?php
   2  /**
   3   * Preprocessor using PHP's dom extension
   4   *
   5   * This program is free software; you can redistribute it and/or modify
   6   * it under the terms of the GNU General Public License as published by
   7   * the Free Software Foundation; either version 2 of the License, or
   8   * (at your option) any later version.
   9   *
  10   * This program is distributed in the hope that it will be useful,
  11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13   * GNU General Public License for more details.
  14   *
  15   * You should have received a copy of the GNU General Public License along
  16   * with this program; if not, write to the Free Software Foundation, Inc.,
  17   * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18   * http://www.gnu.org/copyleft/gpl.html
  19   *
  20   * @file
  21   * @ingroup Parser
  22   */
  23  
  24  /**
  25   * @ingroup Parser
  26   * @codingStandardsIgnoreStart
  27   */
  28  class Preprocessor_DOM implements Preprocessor {
  29      // @codingStandardsIgnoreEnd
  30  
  31      /**
  32       * @var Parser
  33       */
  34      public $parser;
  35  
  36      public $memoryLimit;
  37  
  38      const CACHE_VERSION = 1;
  39  
  40  	public function __construct( $parser ) {
  41          $this->parser = $parser;
  42          $mem = ini_get( 'memory_limit' );
  43          $this->memoryLimit = false;
  44          if ( strval( $mem ) !== '' && $mem != -1 ) {
  45              if ( preg_match( '/^\d+$/', $mem ) ) {
  46                  $this->memoryLimit = $mem;
  47              } elseif ( preg_match( '/^(\d+)M$/i', $mem, $m ) ) {
  48                  $this->memoryLimit = $m[1] * 1048576;
  49              }
  50          }
  51      }
  52  
  53      /**
  54       * @return PPFrame_DOM
  55       */
  56  	public function newFrame() {
  57          return new PPFrame_DOM( $this );
  58      }
  59  
  60      /**
  61       * @param array $args
  62       * @return PPCustomFrame_DOM
  63       */
  64  	public function newCustomFrame( $args ) {
  65          return new PPCustomFrame_DOM( $this, $args );
  66      }
  67  
  68      /**
  69       * @param array $values
  70       * @return PPNode_DOM
  71       */
  72  	public function newPartNodeArray( $values ) {
  73          //NOTE: DOM manipulation is slower than building & parsing XML! (or so Tim sais)
  74          $xml = "<list>";
  75  
  76          foreach ( $values as $k => $val ) {
  77              if ( is_int( $k ) ) {
  78                  $xml .= "<part><name index=\"$k\"/><value>"
  79                      . htmlspecialchars( $val ) . "</value></part>";
  80              } else {
  81                  $xml .= "<part><name>" . htmlspecialchars( $k )
  82                      . "</name>=<value>" . htmlspecialchars( $val ) . "</value></part>";
  83              }
  84          }
  85  
  86          $xml .= "</list>";
  87  
  88          wfProfileIn( __METHOD__ . '-loadXML' );
  89          $dom = new DOMDocument();
  90          wfSuppressWarnings();
  91          $result = $dom->loadXML( $xml );
  92          wfRestoreWarnings();
  93          if ( !$result ) {
  94              // Try running the XML through UtfNormal to get rid of invalid characters
  95              $xml = UtfNormal::cleanUp( $xml );
  96              // 1 << 19 == XML_PARSE_HUGE, needed so newer versions of libxml2
  97              // don't barf when the XML is >256 levels deep
  98              $result = $dom->loadXML( $xml, 1 << 19 );
  99          }
 100          wfProfileOut( __METHOD__ . '-loadXML' );
 101  
 102          if ( !$result ) {
 103              throw new MWException( 'Parameters passed to ' . __METHOD__ . ' result in invalid XML' );
 104          }
 105  
 106          $root = $dom->documentElement;
 107          $node = new PPNode_DOM( $root->childNodes );
 108          return $node;
 109      }
 110  
 111      /**
 112       * @throws MWException
 113       * @return bool
 114       */
 115  	public function memCheck() {
 116          if ( $this->memoryLimit === false ) {
 117              return true;
 118          }
 119          $usage = memory_get_usage();
 120          if ( $usage > $this->memoryLimit * 0.9 ) {
 121              $limit = intval( $this->memoryLimit * 0.9 / 1048576 + 0.5 );
 122              throw new MWException( "Preprocessor hit 90% memory limit ($limit MB)" );
 123          }
 124          return $usage <= $this->memoryLimit * 0.8;
 125      }
 126  
 127      /**
 128       * Preprocess some wikitext and return the document tree.
 129       * This is the ghost of Parser::replace_variables().
 130       *
 131       * @param string $text The text to parse
 132       * @param int $flags Bitwise combination of:
 133       *     Parser::PTD_FOR_INCLUSION  Handle "<noinclude>" and "<includeonly>"
 134       *                                as if the text is being included. Default
 135       *                                is to assume a direct page view.
 136       *
 137       * The generated DOM tree must depend only on the input text and the flags.
 138       * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of bug 4899.
 139       *
 140       * Any flag added to the $flags parameter here, or any other parameter liable to cause a
 141       * change in the DOM tree for a given text, must be passed through the section identifier
 142       * in the section edit link and thus back to extractSections().
 143       *
 144       * The output of this function is currently only cached in process memory, but a persistent
 145       * cache may be implemented at a later date which takes further advantage of these strict
 146       * dependency requirements.
 147       *
 148       * @throws MWException
 149       * @return PPNode_DOM
 150       */
 151  	public function preprocessToObj( $text, $flags = 0 ) {
 152          wfProfileIn( __METHOD__ );
 153          global $wgMemc, $wgPreprocessorCacheThreshold;
 154  
 155          $xml = false;
 156          $cacheable = ( $wgPreprocessorCacheThreshold !== false
 157              && strlen( $text ) > $wgPreprocessorCacheThreshold );
 158          if ( $cacheable ) {
 159              wfProfileIn( __METHOD__ . '-cacheable' );
 160  
 161              $cacheKey = wfMemcKey( 'preprocess-xml', md5( $text ), $flags );
 162              $cacheValue = $wgMemc->get( $cacheKey );
 163              if ( $cacheValue ) {
 164                  $version = substr( $cacheValue, 0, 8 );
 165                  if ( intval( $version ) == self::CACHE_VERSION ) {
 166                      $xml = substr( $cacheValue, 8 );
 167                      // From the cache
 168                      wfDebugLog( "Preprocessor", "Loaded preprocessor XML from memcached (key $cacheKey)" );
 169                  }
 170              }
 171              if ( $xml === false ) {
 172                  wfProfileIn( __METHOD__ . '-cache-miss' );
 173                  $xml = $this->preprocessToXml( $text, $flags );
 174                  $cacheValue = sprintf( "%08d", self::CACHE_VERSION ) . $xml;
 175                  $wgMemc->set( $cacheKey, $cacheValue, 86400 );
 176                  wfProfileOut( __METHOD__ . '-cache-miss' );
 177                  wfDebugLog( "Preprocessor", "Saved preprocessor XML to memcached (key $cacheKey)" );
 178              }
 179          } else {
 180              $xml = $this->preprocessToXml( $text, $flags );
 181          }
 182  
 183          // Fail if the number of elements exceeds acceptable limits
 184          // Do not attempt to generate the DOM
 185          $this->parser->mGeneratedPPNodeCount += substr_count( $xml, '<' );
 186          $max = $this->parser->mOptions->getMaxGeneratedPPNodeCount();
 187          if ( $this->parser->mGeneratedPPNodeCount > $max ) {
 188              if ( $cacheable ) {
 189                  wfProfileOut( __METHOD__ . '-cacheable' );
 190              }
 191              wfProfileOut( __METHOD__ );
 192              throw new MWException( __METHOD__ . ': generated node count limit exceeded' );
 193          }
 194  
 195          wfProfileIn( __METHOD__ . '-loadXML' );
 196          $dom = new DOMDocument;
 197          wfSuppressWarnings();
 198          $result = $dom->loadXML( $xml );
 199          wfRestoreWarnings();
 200          if ( !$result ) {
 201              // Try running the XML through UtfNormal to get rid of invalid characters
 202              $xml = UtfNormal::cleanUp( $xml );
 203              // 1 << 19 == XML_PARSE_HUGE, needed so newer versions of libxml2
 204              // don't barf when the XML is >256 levels deep.
 205              $result = $dom->loadXML( $xml, 1 << 19 );
 206          }
 207          if ( $result ) {
 208              $obj = new PPNode_DOM( $dom->documentElement );
 209          }
 210          wfProfileOut( __METHOD__ . '-loadXML' );
 211  
 212          if ( $cacheable ) {
 213              wfProfileOut( __METHOD__ . '-cacheable' );
 214          }
 215  
 216          wfProfileOut( __METHOD__ );
 217  
 218          if ( !$result ) {
 219              throw new MWException( __METHOD__ . ' generated invalid XML' );
 220          }
 221          return $obj;
 222      }
 223  
 224      /**
 225       * @param string $text
 226       * @param int $flags
 227       * @return string
 228       */
 229  	public function preprocessToXml( $text, $flags = 0 ) {
 230          wfProfileIn( __METHOD__ );
 231          $rules = array(
 232              '{' => array(
 233                  'end' => '}',
 234                  'names' => array(
 235                      2 => 'template',
 236                      3 => 'tplarg',
 237                  ),
 238                  'min' => 2,
 239                  'max' => 3,
 240              ),
 241              '[' => array(
 242                  'end' => ']',
 243                  'names' => array( 2 => null ),
 244                  'min' => 2,
 245                  'max' => 2,
 246              )
 247          );
 248  
 249          $forInclusion = $flags & Parser::PTD_FOR_INCLUSION;
 250  
 251          $xmlishElements = $this->parser->getStripList();
 252          $enableOnlyinclude = false;
 253          if ( $forInclusion ) {
 254              $ignoredTags = array( 'includeonly', '/includeonly' );
 255              $ignoredElements = array( 'noinclude' );
 256              $xmlishElements[] = 'noinclude';
 257              if ( strpos( $text, '<onlyinclude>' ) !== false
 258                  && strpos( $text, '</onlyinclude>' ) !== false
 259              ) {
 260                  $enableOnlyinclude = true;
 261              }
 262          } else {
 263              $ignoredTags = array( 'noinclude', '/noinclude', 'onlyinclude', '/onlyinclude' );
 264              $ignoredElements = array( 'includeonly' );
 265              $xmlishElements[] = 'includeonly';
 266          }
 267          $xmlishRegex = implode( '|', array_merge( $xmlishElements, $ignoredTags ) );
 268  
 269          // Use "A" modifier (anchored) instead of "^", because ^ doesn't work with an offset
 270          $elementsRegex = "~($xmlishRegex)(?:\s|\/>|>)|(!--)~iA";
 271  
 272          $stack = new PPDStack;
 273  
 274          $searchBase = "[{<\n"; #}
 275          // For fast reverse searches
 276          $revText = strrev( $text );
 277          $lengthText = strlen( $text );
 278  
 279          // Input pointer, starts out pointing to a pseudo-newline before the start
 280          $i = 0;
 281          // Current accumulator
 282          $accum =& $stack->getAccum();
 283          $accum = '<root>';
 284          // True to find equals signs in arguments
 285          $findEquals = false;
 286          // True to take notice of pipe characters
 287          $findPipe = false;
 288          $headingIndex = 1;
 289          // True if $i is inside a possible heading
 290          $inHeading = false;
 291          // True if there are no more greater-than (>) signs right of $i
 292          $noMoreGT = false;
 293          // True to ignore all input up to the next <onlyinclude>
 294          $findOnlyinclude = $enableOnlyinclude;
 295          // Do a line-start run without outputting an LF character
 296          $fakeLineStart = true;
 297  
 298          while ( true ) {
 299              //$this->memCheck();
 300  
 301              if ( $findOnlyinclude ) {
 302                  // Ignore all input up to the next <onlyinclude>
 303                  $startPos = strpos( $text, '<onlyinclude>', $i );
 304                  if ( $startPos === false ) {
 305                      // Ignored section runs to the end
 306                      $accum .= '<ignore>' . htmlspecialchars( substr( $text, $i ) ) . '</ignore>';
 307                      break;
 308                  }
 309                  $tagEndPos = $startPos + strlen( '<onlyinclude>' ); // past-the-end
 310                  $accum .= '<ignore>' . htmlspecialchars( substr( $text, $i, $tagEndPos - $i ) ) . '</ignore>';
 311                  $i = $tagEndPos;
 312                  $findOnlyinclude = false;
 313              }
 314  
 315              if ( $fakeLineStart ) {
 316                  $found = 'line-start';
 317                  $curChar = '';
 318              } else {
 319                  # Find next opening brace, closing brace or pipe
 320                  $search = $searchBase;
 321                  if ( $stack->top === false ) {
 322                      $currentClosing = '';
 323                  } else {
 324                      $currentClosing = $stack->top->close;
 325                      $search .= $currentClosing;
 326                  }
 327                  if ( $findPipe ) {
 328                      $search .= '|';
 329                  }
 330                  if ( $findEquals ) {
 331                      // First equals will be for the template
 332                      $search .= '=';
 333                  }
 334                  $rule = null;
 335                  # Output literal section, advance input counter
 336                  $literalLength = strcspn( $text, $search, $i );
 337                  if ( $literalLength > 0 ) {
 338                      $accum .= htmlspecialchars( substr( $text, $i, $literalLength ) );
 339                      $i += $literalLength;
 340                  }
 341                  if ( $i >= $lengthText ) {
 342                      if ( $currentClosing == "\n" ) {
 343                          // Do a past-the-end run to finish off the heading
 344                          $curChar = '';
 345                          $found = 'line-end';
 346                      } else {
 347                          # All done
 348                          break;
 349                      }
 350                  } else {
 351                      $curChar = $text[$i];
 352                      if ( $curChar == '|' ) {
 353                          $found = 'pipe';
 354                      } elseif ( $curChar == '=' ) {
 355                          $found = 'equals';
 356                      } elseif ( $curChar == '<' ) {
 357                          $found = 'angle';
 358                      } elseif ( $curChar == "\n" ) {
 359                          if ( $inHeading ) {
 360                              $found = 'line-end';
 361                          } else {
 362                              $found = 'line-start';
 363                          }
 364                      } elseif ( $curChar == $currentClosing ) {
 365                          $found = 'close';
 366                      } elseif ( isset( $rules[$curChar] ) ) {
 367                          $found = 'open';
 368                          $rule = $rules[$curChar];
 369                      } else {
 370                          # Some versions of PHP have a strcspn which stops on null characters
 371                          # Ignore and continue
 372                          ++$i;
 373                          continue;
 374                      }
 375                  }
 376              }
 377  
 378              if ( $found == 'angle' ) {
 379                  $matches = false;
 380                  // Handle </onlyinclude>
 381                  if ( $enableOnlyinclude
 382                      && substr( $text, $i, strlen( '</onlyinclude>' ) ) == '</onlyinclude>'
 383                  ) {
 384                      $findOnlyinclude = true;
 385                      continue;
 386                  }
 387  
 388                  // Determine element name
 389                  if ( !preg_match( $elementsRegex, $text, $matches, 0, $i + 1 ) ) {
 390                      // Element name missing or not listed
 391                      $accum .= '&lt;';
 392                      ++$i;
 393                      continue;
 394                  }
 395                  // Handle comments
 396                  if ( isset( $matches[2] ) && $matches[2] == '!--' ) {
 397  
 398                      // To avoid leaving blank lines, when a sequence of
 399                      // space-separated comments is both preceded and followed by
 400                      // a newline (ignoring spaces), then
 401                      // trim leading and trailing spaces and the trailing newline.
 402  
 403                      // Find the end
 404                      $endPos = strpos( $text, '-->', $i + 4 );
 405                      if ( $endPos === false ) {
 406                          // Unclosed comment in input, runs to end
 407                          $inner = substr( $text, $i );
 408                          $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>';
 409                          $i = $lengthText;
 410                      } else {
 411                          // Search backwards for leading whitespace
 412                          $wsStart = $i ? ( $i - strspn( $revText, " \t", $lengthText - $i ) ) : 0;
 413  
 414                          // Search forwards for trailing whitespace
 415                          // $wsEnd will be the position of the last space (or the '>' if there's none)
 416                          $wsEnd = $endPos + 2 + strspn( $text, " \t", $endPos + 3 );
 417  
 418                          // Keep looking forward as long as we're finding more
 419                          // comments.
 420                          $comments = array( array( $wsStart, $wsEnd ) );
 421                          while ( substr( $text, $wsEnd + 1, 4 ) == '<!--' ) {
 422                              $c = strpos( $text, '-->', $wsEnd + 4 );
 423                              if ( $c === false ) {
 424                                  break;
 425                              }
 426                              $c = $c + 2 + strspn( $text, " \t", $c + 3 );
 427                              $comments[] = array( $wsEnd + 1, $c );
 428                              $wsEnd = $c;
 429                          }
 430  
 431                          // Eat the line if possible
 432                          // TODO: This could theoretically be done if $wsStart == 0, i.e. for comments at
 433                          // the overall start. That's not how Sanitizer::removeHTMLcomments() did it, but
 434                          // it's a possible beneficial b/c break.
 435                          if ( $wsStart > 0 && substr( $text, $wsStart - 1, 1 ) == "\n"
 436                              && substr( $text, $wsEnd + 1, 1 ) == "\n"
 437                          ) {
 438                              // Remove leading whitespace from the end of the accumulator
 439                              // Sanity check first though
 440                              $wsLength = $i - $wsStart;
 441                              if ( $wsLength > 0
 442                                  && strspn( $accum, " \t", -$wsLength ) === $wsLength
 443                              ) {
 444                                  $accum = substr( $accum, 0, -$wsLength );
 445                              }
 446  
 447                              // Dump all but the last comment to the accumulator
 448                              foreach ( $comments as $j => $com ) {
 449                                  $startPos = $com[0];
 450                                  $endPos = $com[1] + 1;
 451                                  if ( $j == ( count( $comments ) - 1 ) ) {
 452                                      break;
 453                                  }
 454                                  $inner = substr( $text, $startPos, $endPos - $startPos );
 455                                  $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>';
 456                              }
 457  
 458                              // Do a line-start run next time to look for headings after the comment
 459                              $fakeLineStart = true;
 460                          } else {
 461                              // No line to eat, just take the comment itself
 462                              $startPos = $i;
 463                              $endPos += 2;
 464                          }
 465  
 466                          if ( $stack->top ) {
 467                              $part = $stack->top->getCurrentPart();
 468                              if ( !( isset( $part->commentEnd ) && $part->commentEnd == $wsStart - 1 ) ) {
 469                                  $part->visualEnd = $wsStart;
 470                              }
 471                              // Else comments abutting, no change in visual end
 472                              $part->commentEnd = $endPos;
 473                          }
 474                          $i = $endPos + 1;
 475                          $inner = substr( $text, $startPos, $endPos - $startPos + 1 );
 476                          $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>';
 477                      }
 478                      continue;
 479                  }
 480                  $name = $matches[1];
 481                  $lowerName = strtolower( $name );
 482                  $attrStart = $i + strlen( $name ) + 1;
 483  
 484                  // Find end of tag
 485                  $tagEndPos = $noMoreGT ? false : strpos( $text, '>', $attrStart );
 486                  if ( $tagEndPos === false ) {
 487                      // Infinite backtrack
 488                      // Disable tag search to prevent worst-case O(N^2) performance
 489                      $noMoreGT = true;
 490                      $accum .= '&lt;';
 491                      ++$i;
 492                      continue;
 493                  }
 494  
 495                  // Handle ignored tags
 496                  if ( in_array( $lowerName, $ignoredTags ) ) {
 497                      $accum .= '<ignore>'
 498                          . htmlspecialchars( substr( $text, $i, $tagEndPos - $i + 1 ) )
 499                          . '</ignore>';
 500                      $i = $tagEndPos + 1;
 501                      continue;
 502                  }
 503  
 504                  $tagStartPos = $i;
 505                  if ( $text[$tagEndPos - 1] == '/' ) {
 506                      $attrEnd = $tagEndPos - 1;
 507                      $inner = null;
 508                      $i = $tagEndPos + 1;
 509                      $close = '';
 510                  } else {
 511                      $attrEnd = $tagEndPos;
 512                      // Find closing tag
 513                      if ( preg_match( "/<\/" . preg_quote( $name, '/' ) . "\s*>/i",
 514                              $text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 )
 515                      ) {
 516                          $inner = substr( $text, $tagEndPos + 1, $matches[0][1] - $tagEndPos - 1 );
 517                          $i = $matches[0][1] + strlen( $matches[0][0] );
 518                          $close = '<close>' . htmlspecialchars( $matches[0][0] ) . '</close>';
 519                      } else {
 520                          // No end tag -- let it run out to the end of the text.
 521                          $inner = substr( $text, $tagEndPos + 1 );
 522                          $i = $lengthText;
 523                          $close = '';
 524                      }
 525                  }
 526                  // <includeonly> and <noinclude> just become <ignore> tags
 527                  if ( in_array( $lowerName, $ignoredElements ) ) {
 528                      $accum .= '<ignore>' . htmlspecialchars( substr( $text, $tagStartPos, $i - $tagStartPos ) )
 529                          . '</ignore>';
 530                      continue;
 531                  }
 532  
 533                  $accum .= '<ext>';
 534                  if ( $attrEnd <= $attrStart ) {
 535                      $attr = '';
 536                  } else {
 537                      $attr = substr( $text, $attrStart, $attrEnd - $attrStart );
 538                  }
 539                  $accum .= '<name>' . htmlspecialchars( $name ) . '</name>' .
 540                      // Note that the attr element contains the whitespace between name and attribute,
 541                      // this is necessary for precise reconstruction during pre-save transform.
 542                      '<attr>' . htmlspecialchars( $attr ) . '</attr>';
 543                  if ( $inner !== null ) {
 544                      $accum .= '<inner>' . htmlspecialchars( $inner ) . '</inner>';
 545                  }
 546                  $accum .= $close . '</ext>';
 547              } elseif ( $found == 'line-start' ) {
 548                  // Is this the start of a heading?
 549                  // Line break belongs before the heading element in any case
 550                  if ( $fakeLineStart ) {
 551                      $fakeLineStart = false;
 552                  } else {
 553                      $accum .= $curChar;
 554                      $i++;
 555                  }
 556  
 557                  $count = strspn( $text, '=', $i, 6 );
 558                  if ( $count == 1 && $findEquals ) {
 559                      // DWIM: This looks kind of like a name/value separator.
 560                      // Let's let the equals handler have it and break the
 561                      // potential heading. This is heuristic, but AFAICT the
 562                      // methods for completely correct disambiguation are very
 563                      // complex.
 564                  } elseif ( $count > 0 ) {
 565                      $piece = array(
 566                          'open' => "\n",
 567                          'close' => "\n",
 568                          'parts' => array( new PPDPart( str_repeat( '=', $count ) ) ),
 569                          'startPos' => $i,
 570                          'count' => $count );
 571                      $stack->push( $piece );
 572                      $accum =& $stack->getAccum();
 573                      $flags = $stack->getFlags();
 574                      extract( $flags );
 575                      $i += $count;
 576                  }
 577              } elseif ( $found == 'line-end' ) {
 578                  $piece = $stack->top;
 579                  // A heading must be open, otherwise \n wouldn't have been in the search list
 580                  assert( '$piece->open == "\n"' );
 581                  $part = $piece->getCurrentPart();
 582                  // Search back through the input to see if it has a proper close.
 583                  // Do this using the reversed string since the other solutions
 584                  // (end anchor, etc.) are inefficient.
 585                  $wsLength = strspn( $revText, " \t", $lengthText - $i );
 586                  $searchStart = $i - $wsLength;
 587                  if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) {
 588                      // Comment found at line end
 589                      // Search for equals signs before the comment
 590                      $searchStart = $part->visualEnd;
 591                      $searchStart -= strspn( $revText, " \t", $lengthText - $searchStart );
 592                  }
 593                  $count = $piece->count;
 594                  $equalsLength = strspn( $revText, '=', $lengthText - $searchStart );
 595                  if ( $equalsLength > 0 ) {
 596                      if ( $searchStart - $equalsLength == $piece->startPos ) {
 597                          // This is just a single string of equals signs on its own line
 598                          // Replicate the doHeadings behavior /={count}(.+)={count}/
 599                          // First find out how many equals signs there really are (don't stop at 6)
 600                          $count = $equalsLength;
 601                          if ( $count < 3 ) {
 602                              $count = 0;
 603                          } else {
 604                              $count = min( 6, intval( ( $count - 1 ) / 2 ) );
 605                          }
 606                      } else {
 607                          $count = min( $equalsLength, $count );
 608                      }
 609                      if ( $count > 0 ) {
 610                          // Normal match, output <h>
 611                          $element = "<h level=\"$count\" i=\"$headingIndex\">$accum</h>";
 612                          $headingIndex++;
 613                      } else {
 614                          // Single equals sign on its own line, count=0
 615                          $element = $accum;
 616                      }
 617                  } else {
 618                      // No match, no <h>, just pass down the inner text
 619                      $element = $accum;
 620                  }
 621                  // Unwind the stack
 622                  $stack->pop();
 623                  $accum =& $stack->getAccum();
 624                  $flags = $stack->getFlags();
 625                  extract( $flags );
 626  
 627                  // Append the result to the enclosing accumulator
 628                  $accum .= $element;
 629                  // Note that we do NOT increment the input pointer.
 630                  // This is because the closing linebreak could be the opening linebreak of
 631                  // another heading. Infinite loops are avoided because the next iteration MUST
 632                  // hit the heading open case above, which unconditionally increments the
 633                  // input pointer.
 634              } elseif ( $found == 'open' ) {
 635                  # count opening brace characters
 636                  $count = strspn( $text, $curChar, $i );
 637  
 638                  # we need to add to stack only if opening brace count is enough for one of the rules
 639                  if ( $count >= $rule['min'] ) {
 640                      # Add it to the stack
 641                      $piece = array(
 642                          'open' => $curChar,
 643                          'close' => $rule['end'],
 644                          'count' => $count,
 645                          'lineStart' => ( $i > 0 && $text[$i - 1] == "\n" ),
 646                      );
 647  
 648                      $stack->push( $piece );
 649                      $accum =& $stack->getAccum();
 650                      $flags = $stack->getFlags();
 651                      extract( $flags );
 652                  } else {
 653                      # Add literal brace(s)
 654                      $accum .= htmlspecialchars( str_repeat( $curChar, $count ) );
 655                  }
 656                  $i += $count;
 657              } elseif ( $found == 'close' ) {
 658                  $piece = $stack->top;
 659                  # lets check if there are enough characters for closing brace
 660                  $maxCount = $piece->count;
 661                  $count = strspn( $text, $curChar, $i, $maxCount );
 662  
 663                  # check for maximum matching characters (if there are 5 closing
 664                  # characters, we will probably need only 3 - depending on the rules)
 665                  $rule = $rules[$piece->open];
 666                  if ( $count > $rule['max'] ) {
 667                      # The specified maximum exists in the callback array, unless the caller
 668                      # has made an error
 669                      $matchingCount = $rule['max'];
 670                  } else {
 671                      # Count is less than the maximum
 672                      # Skip any gaps in the callback array to find the true largest match
 673                      # Need to use array_key_exists not isset because the callback can be null
 674                      $matchingCount = $count;
 675                      while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $rule['names'] ) ) {
 676                          --$matchingCount;
 677                      }
 678                  }
 679  
 680                  if ( $matchingCount <= 0 ) {
 681                      # No matching element found in callback array
 682                      # Output a literal closing brace and continue
 683                      $accum .= htmlspecialchars( str_repeat( $curChar, $count ) );
 684                      $i += $count;
 685                      continue;
 686                  }
 687                  $name = $rule['names'][$matchingCount];
 688                  if ( $name === null ) {
 689                      // No element, just literal text
 690                      $element = $piece->breakSyntax( $matchingCount ) . str_repeat( $rule['end'], $matchingCount );
 691                  } else {
 692                      # Create XML element
 693                      # Note: $parts is already XML, does not need to be encoded further
 694                      $parts = $piece->parts;
 695                      $title = $parts[0]->out;
 696                      unset( $parts[0] );
 697  
 698                      # The invocation is at the start of the line if lineStart is set in
 699                      # the stack, and all opening brackets are used up.
 700                      if ( $maxCount == $matchingCount && !empty( $piece->lineStart ) ) {
 701                          $attr = ' lineStart="1"';
 702                      } else {
 703                          $attr = '';
 704                      }
 705  
 706                      $element = "<$name$attr>";
 707                      $element .= "<title>$title</title>";
 708                      $argIndex = 1;
 709                      foreach ( $parts as $part ) {
 710                          if ( isset( $part->eqpos ) ) {
 711                              $argName = substr( $part->out, 0, $part->eqpos );
 712                              $argValue = substr( $part->out, $part->eqpos + 1 );
 713                              $element .= "<part><name>$argName</name>=<value>$argValue</value></part>";
 714                          } else {
 715                              $element .= "<part><name index=\"$argIndex\" /><value>{$part->out}</value></part>";
 716                              $argIndex++;
 717                          }
 718                      }
 719                      $element .= "</$name>";
 720                  }
 721  
 722                  # Advance input pointer
 723                  $i += $matchingCount;
 724  
 725                  # Unwind the stack
 726                  $stack->pop();
 727                  $accum =& $stack->getAccum();
 728  
 729                  # Re-add the old stack element if it still has unmatched opening characters remaining
 730                  if ( $matchingCount < $piece->count ) {
 731                      $piece->parts = array( new PPDPart );
 732                      $piece->count -= $matchingCount;
 733                      # do we still qualify for any callback with remaining count?
 734                      $min = $rules[$piece->open]['min'];
 735                      if ( $piece->count >= $min ) {
 736                          $stack->push( $piece );
 737                          $accum =& $stack->getAccum();
 738                      } else {
 739                          $accum .= str_repeat( $piece->open, $piece->count );
 740                      }
 741                  }
 742                  $flags = $stack->getFlags();
 743                  extract( $flags );
 744  
 745                  # Add XML element to the enclosing accumulator
 746                  $accum .= $element;
 747              } elseif ( $found == 'pipe' ) {
 748                  $findEquals = true; // shortcut for getFlags()
 749                  $stack->addPart();
 750                  $accum =& $stack->getAccum();
 751                  ++$i;
 752              } elseif ( $found == 'equals' ) {
 753                  $findEquals = false; // shortcut for getFlags()
 754                  $stack->getCurrentPart()->eqpos = strlen( $accum );
 755                  $accum .= '=';
 756                  ++$i;
 757              }
 758          }
 759  
 760          # Output any remaining unclosed brackets
 761          foreach ( $stack->stack as $piece ) {
 762              $stack->rootAccum .= $piece->breakSyntax();
 763          }
 764          $stack->rootAccum .= '</root>';
 765          $xml = $stack->rootAccum;
 766  
 767          wfProfileOut( __METHOD__ );
 768  
 769          return $xml;
 770      }
 771  }
 772  
 773  /**
 774   * Stack class to help Preprocessor::preprocessToObj()
 775   * @ingroup Parser
 776   */
 777  class PPDStack {
 778      public $stack, $rootAccum;
 779  
 780      /**
 781       * @var PPDStack
 782       */
 783      public $top;
 784      public $out;
 785      public $elementClass = 'PPDStackElement';
 786  
 787      public static $false = false;
 788  
 789  	public function __construct() {
 790          $this->stack = array();
 791          $this->top = false;
 792          $this->rootAccum = '';
 793          $this->accum =& $this->rootAccum;
 794      }
 795  
 796      /**
 797       * @return int
 798       */
 799  	public function count() {
 800          return count( $this->stack );
 801      }
 802  
 803      public function &getAccum() {
 804          return $this->accum;
 805      }
 806  
 807  	public function getCurrentPart() {
 808          if ( $this->top === false ) {
 809              return false;
 810          } else {
 811              return $this->top->getCurrentPart();
 812          }
 813      }
 814  
 815  	public function push( $data ) {
 816          if ( $data instanceof $this->elementClass ) {
 817              $this->stack[] = $data;
 818          } else {
 819              $class = $this->elementClass;
 820              $this->stack[] = new $class( $data );
 821          }
 822          $this->top = $this->stack[count( $this->stack ) - 1];
 823          $this->accum =& $this->top->getAccum();
 824      }
 825  
 826  	public function pop() {
 827          if ( !count( $this->stack ) ) {
 828              throw new MWException( __METHOD__ . ': no elements remaining' );
 829          }
 830          $temp = array_pop( $this->stack );
 831  
 832          if ( count( $this->stack ) ) {
 833              $this->top = $this->stack[count( $this->stack ) - 1];
 834              $this->accum =& $this->top->getAccum();
 835          } else {
 836              $this->top = self::$false;
 837              $this->accum =& $this->rootAccum;
 838          }
 839          return $temp;
 840      }
 841  
 842  	public function addPart( $s = '' ) {
 843          $this->top->addPart( $s );
 844          $this->accum =& $this->top->getAccum();
 845      }
 846  
 847      /**
 848       * @return array
 849       */
 850  	public function getFlags() {
 851          if ( !count( $this->stack ) ) {
 852              return array(
 853                  'findEquals' => false,
 854                  'findPipe' => false,
 855                  'inHeading' => false,
 856              );
 857          } else {
 858              return $this->top->getFlags();
 859          }
 860      }
 861  }
 862  
 863  /**
 864   * @ingroup Parser
 865   */
 866  class PPDStackElement {
 867      public $open,              // Opening character (\n for heading)
 868          $close,             // Matching closing character
 869          $count,             // Number of opening characters found (number of "=" for heading)
 870          $parts,             // Array of PPDPart objects describing pipe-separated parts.
 871          $lineStart;         // True if the open char appeared at the start of the input line. Not set for headings.
 872  
 873      public $partClass = 'PPDPart';
 874  
 875  	public function __construct( $data = array() ) {
 876          $class = $this->partClass;
 877          $this->parts = array( new $class );
 878  
 879          foreach ( $data as $name => $value ) {
 880              $this->$name = $value;
 881          }
 882      }
 883  
 884      public function &getAccum() {
 885          return $this->parts[count( $this->parts ) - 1]->out;
 886      }
 887  
 888  	public function addPart( $s = '' ) {
 889          $class = $this->partClass;
 890          $this->parts[] = new $class( $s );
 891      }
 892  
 893  	public function getCurrentPart() {
 894          return $this->parts[count( $this->parts ) - 1];
 895      }
 896  
 897      /**
 898       * @return array
 899       */
 900  	public function getFlags() {
 901          $partCount = count( $this->parts );
 902          $findPipe = $this->open != "\n" && $this->open != '[';
 903          return array(
 904              'findPipe' => $findPipe,
 905              'findEquals' => $findPipe && $partCount > 1 && !isset( $this->parts[$partCount - 1]->eqpos ),
 906              'inHeading' => $this->open == "\n",
 907          );
 908      }
 909  
 910      /**
 911       * Get the output string that would result if the close is not found.
 912       *
 913       * @param bool|int $openingCount
 914       * @return string
 915       */
 916  	public function breakSyntax( $openingCount = false ) {
 917          if ( $this->open == "\n" ) {
 918              $s = $this->parts[0]->out;
 919          } else {
 920              if ( $openingCount === false ) {
 921                  $openingCount = $this->count;
 922              }
 923              $s = str_repeat( $this->open, $openingCount );
 924              $first = true;
 925              foreach ( $this->parts as $part ) {
 926                  if ( $first ) {
 927                      $first = false;
 928                  } else {
 929                      $s .= '|';
 930                  }
 931                  $s .= $part->out;
 932              }
 933          }
 934          return $s;
 935      }
 936  }
 937  
 938  /**
 939   * @ingroup Parser
 940   */
 941  class PPDPart {
 942      public $out; // Output accumulator string
 943  
 944      // Optional member variables:
 945      //   eqpos        Position of equals sign in output accumulator
 946      //   commentEnd   Past-the-end input pointer for the last comment encountered
 947      //   visualEnd    Past-the-end input pointer for the end of the accumulator minus comments
 948  
 949  	public function __construct( $out = '' ) {
 950          $this->out = $out;
 951      }
 952  }
 953  
 954  /**
 955   * An expansion frame, used as a context to expand the result of preprocessToObj()
 956   * @ingroup Parser
 957   * @codingStandardsIgnoreStart
 958   */
 959  class PPFrame_DOM implements PPFrame {
 960      // @codingStandardsIgnoreEnd
 961  
 962      /**
 963       * @var Preprocessor
 964       */
 965      public $preprocessor;
 966  
 967      /**
 968       * @var Parser
 969       */
 970      public $parser;
 971  
 972      /**
 973       * @var Title
 974       */
 975      public $title;
 976      public $titleCache;
 977  
 978      /**
 979       * Hashtable listing templates which are disallowed for expansion in this frame,
 980       * having been encountered previously in parent frames.
 981       */
 982      public $loopCheckHash;
 983  
 984      /**
 985       * Recursion depth of this frame, top = 0
 986       * Note that this is NOT the same as expansion depth in expand()
 987       */
 988      public $depth;
 989  
 990      private $volatile = false;
 991      private $ttl = null;
 992  
 993      /**
 994       * @var array
 995       */
 996      protected $childExpansionCache;
 997  
 998      /**
 999       * Construct a new preprocessor frame.
1000       * @param Preprocessor $preprocessor The parent preprocessor
1001       */
1002  	public function __construct( $preprocessor ) {
1003          $this->preprocessor = $preprocessor;
1004          $this->parser = $preprocessor->parser;
1005          $this->title = $this->parser->mTitle;
1006          $this->titleCache = array( $this->title ? $this->title->getPrefixedDBkey() : false );
1007          $this->loopCheckHash = array();
1008          $this->depth = 0;
1009          $this->childExpansionCache = array();
1010      }
1011  
1012      /**
1013       * Create a new child frame
1014       * $args is optionally a multi-root PPNode or array containing the template arguments
1015       *
1016       * @param bool|array $args
1017       * @param Title|bool $title
1018       * @param int $indexOffset
1019       * @return PPTemplateFrame_DOM
1020       */
1021  	public function newChild( $args = false, $title = false, $indexOffset = 0 ) {
1022          $namedArgs = array();
1023          $numberedArgs = array();
1024          if ( $title === false ) {
1025              $title = $this->title;
1026          }
1027          if ( $args !== false ) {
1028              $xpath = false;
1029              if ( $args instanceof PPNode ) {
1030                  $args = $args->node;
1031              }
1032              foreach ( $args as $arg ) {
1033                  if ( $arg instanceof PPNode ) {
1034                      $arg = $arg->node;
1035                  }
1036                  if ( !$xpath || $xpath->document !== $arg->ownerDocument ) {
1037                      $xpath = new DOMXPath( $arg->ownerDocument );
1038                  }
1039  
1040                  $nameNodes = $xpath->query( 'name', $arg );
1041                  $value = $xpath->query( 'value', $arg );
1042                  if ( $nameNodes->item( 0 )->hasAttributes() ) {
1043                      // Numbered parameter
1044                      $index = $nameNodes->item( 0 )->attributes->getNamedItem( 'index' )->textContent;
1045                      $index = $index - $indexOffset;
1046                      $numberedArgs[$index] = $value->item( 0 );
1047                      unset( $namedArgs[$index] );
1048                  } else {
1049                      // Named parameter
1050                      $name = trim( $this->expand( $nameNodes->item( 0 ), PPFrame::STRIP_COMMENTS ) );
1051                      $namedArgs[$name] = $value->item( 0 );
1052                      unset( $numberedArgs[$name] );
1053                  }
1054              }
1055          }
1056          return new PPTemplateFrame_DOM( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title );
1057      }
1058  
1059      /**
1060       * @throws MWException
1061       * @param string|int $key
1062       * @param string|PPNode_DOM|DOMDocument $root
1063       * @param int $flags
1064       * @return string
1065       */
1066  	public function cachedExpand( $key, $root, $flags = 0 ) {
1067          // we don't have a parent, so we don't have a cache
1068          return $this->expand( $root, $flags );
1069      }
1070  
1071      /**
1072       * @throws MWException
1073       * @param string|PPNode_DOM|DOMDocument $root
1074       * @param int $flags
1075       * @return string
1076       */
1077  	public function expand( $root, $flags = 0 ) {
1078          static $expansionDepth = 0;
1079          if ( is_string( $root ) ) {
1080              return $root;
1081          }
1082  
1083          if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->getMaxPPNodeCount() ) {
1084              $this->parser->limitationWarn( 'node-count-exceeded',
1085                  $this->parser->mPPNodeCount,
1086                  $this->parser->mOptions->getMaxPPNodeCount()
1087              );
1088              return '<span class="error">Node-count limit exceeded</span>';
1089          }
1090  
1091          if ( $expansionDepth > $this->parser->mOptions->getMaxPPExpandDepth() ) {
1092              $this->parser->limitationWarn( 'expansion-depth-exceeded',
1093                  $expansionDepth,
1094                  $this->parser->mOptions->getMaxPPExpandDepth()
1095              );
1096              return '<span class="error">Expansion depth limit exceeded</span>';
1097          }
1098          wfProfileIn( __METHOD__ );
1099          ++$expansionDepth;
1100          if ( $expansionDepth > $this->parser->mHighestExpansionDepth ) {
1101              $this->parser->mHighestExpansionDepth = $expansionDepth;
1102          }
1103  
1104          if ( $root instanceof PPNode_DOM ) {
1105              $root = $root->node;
1106          }
1107          if ( $root instanceof DOMDocument ) {
1108              $root = $root->documentElement;
1109          }
1110  
1111          $outStack = array( '', '' );
1112          $iteratorStack = array( false, $root );
1113          $indexStack = array( 0, 0 );
1114  
1115          while ( count( $iteratorStack ) > 1 ) {
1116              $level = count( $outStack ) - 1;
1117              $iteratorNode =& $iteratorStack[$level];
1118              $out =& $outStack[$level];
1119              $index =& $indexStack[$level];
1120  
1121              if ( $iteratorNode instanceof PPNode_DOM ) {
1122                  $iteratorNode = $iteratorNode->node;
1123              }
1124  
1125              if ( is_array( $iteratorNode ) ) {
1126                  if ( $index >= count( $iteratorNode ) ) {
1127                      // All done with this iterator
1128                      $iteratorStack[$level] = false;
1129                      $contextNode = false;
1130                  } else {
1131                      $contextNode = $iteratorNode[$index];
1132                      $index++;
1133                  }
1134              } elseif ( $iteratorNode instanceof DOMNodeList ) {
1135                  if ( $index >= $iteratorNode->length ) {
1136                      // All done with this iterator
1137                      $iteratorStack[$level] = false;
1138                      $contextNode = false;
1139                  } else {
1140                      $contextNode = $iteratorNode->item( $index );
1141                      $index++;
1142                  }
1143              } else {
1144                  // Copy to $contextNode and then delete from iterator stack,
1145                  // because this is not an iterator but we do have to execute it once
1146                  $contextNode = $iteratorStack[$level];
1147                  $iteratorStack[$level] = false;
1148              }
1149  
1150              if ( $contextNode instanceof PPNode_DOM ) {
1151                  $contextNode = $contextNode->node;
1152              }
1153  
1154              $newIterator = false;
1155  
1156              if ( $contextNode === false ) {
1157                  // nothing to do
1158              } elseif ( is_string( $contextNode ) ) {
1159                  $out .= $contextNode;
1160              } elseif ( is_array( $contextNode ) || $contextNode instanceof DOMNodeList ) {
1161                  $newIterator = $contextNode;
1162              } elseif ( $contextNode instanceof DOMNode ) {
1163                  if ( $contextNode->nodeType == XML_TEXT_NODE ) {
1164                      $out .= $contextNode->nodeValue;
1165                  } elseif ( $contextNode->nodeName == 'template' ) {
1166                      # Double-brace expansion
1167                      $xpath = new DOMXPath( $contextNode->ownerDocument );
1168                      $titles = $xpath->query( 'title', $contextNode );
1169                      $title = $titles->item( 0 );
1170                      $parts = $xpath->query( 'part', $contextNode );
1171                      if ( $flags & PPFrame::NO_TEMPLATES ) {
1172                          $newIterator = $this->virtualBracketedImplode( '{{', '|', '}}', $title, $parts );
1173                      } else {
1174                          $lineStart = $contextNode->getAttribute( 'lineStart' );
1175                          $params = array(
1176                              'title' => new PPNode_DOM( $title ),
1177                              'parts' => new PPNode_DOM( $parts ),
1178                              'lineStart' => $lineStart );
1179                          $ret = $this->parser->braceSubstitution( $params, $this );
1180                          if ( isset( $ret['object'] ) ) {
1181                              $newIterator = $ret['object'];
1182                          } else {
1183                              $out .= $ret['text'];
1184                          }
1185                      }
1186                  } elseif ( $contextNode->nodeName == 'tplarg' ) {
1187                      # Triple-brace expansion
1188                      $xpath = new DOMXPath( $contextNode->ownerDocument );
1189                      $titles = $xpath->query( 'title', $contextNode );
1190                      $title = $titles->item( 0 );
1191                      $parts = $xpath->query( 'part', $contextNode );
1192                      if ( $flags & PPFrame::NO_ARGS ) {
1193                          $newIterator = $this->virtualBracketedImplode( '{{{', '|', '}}}', $title, $parts );
1194                      } else {
1195                          $params = array(
1196                              'title' => new PPNode_DOM( $title ),
1197                              'parts' => new PPNode_DOM( $parts ) );
1198                          $ret = $this->parser->argSubstitution( $params, $this );
1199                          if ( isset( $ret['object'] ) ) {
1200                              $newIterator = $ret['object'];
1201                          } else {
1202                              $out .= $ret['text'];
1203                          }
1204                      }
1205                  } elseif ( $contextNode->nodeName == 'comment' ) {
1206                      # HTML-style comment
1207                      # Remove it in HTML, pre+remove and STRIP_COMMENTS modes
1208                      if ( $this->parser->ot['html']
1209                          || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() )
1210                          || ( $flags & PPFrame::STRIP_COMMENTS )
1211                      ) {
1212                          $out .= '';
1213                      } elseif ( $this->parser->ot['wiki'] && !( $flags & PPFrame::RECOVER_COMMENTS ) ) {
1214                          # Add a strip marker in PST mode so that pstPass2() can
1215                          # run some old-fashioned regexes on the result.
1216                          # Not in RECOVER_COMMENTS mode (extractSections) though.
1217                          $out .= $this->parser->insertStripItem( $contextNode->textContent );
1218                      } else {
1219                          # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
1220                          $out .= $contextNode->textContent;
1221                      }
1222                  } elseif ( $contextNode->nodeName == 'ignore' ) {
1223                      # Output suppression used by <includeonly> etc.
1224                      # OT_WIKI will only respect <ignore> in substed templates.
1225                      # The other output types respect it unless NO_IGNORE is set.
1226                      # extractSections() sets NO_IGNORE and so never respects it.
1227                      if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] )
1228                          || ( $flags & PPFrame::NO_IGNORE )
1229                      ) {
1230                          $out .= $contextNode->textContent;
1231                      } else {
1232                          $out .= '';
1233                      }
1234                  } elseif ( $contextNode->nodeName == 'ext' ) {
1235                      # Extension tag
1236                      $xpath = new DOMXPath( $contextNode->ownerDocument );
1237                      $names = $xpath->query( 'name', $contextNode );
1238                      $attrs = $xpath->query( 'attr', $contextNode );
1239                      $inners = $xpath->query( 'inner', $contextNode );
1240                      $closes = $xpath->query( 'close', $contextNode );
1241                      if ( $flags & PPFrame::NO_TAGS ) {
1242                          $s = '<' . $this->expand( $names->item( 0 ), $flags );
1243                          if ( $attrs->length > 0 ) {
1244                              $s .= $this->expand( $attrs->item( 0 ), $flags );
1245                          }
1246                          if ( $inners->length > 0 ) {
1247                              $s .= '>' . $this->expand( $inners->item( 0 ), $flags );
1248                              if ( $closes->length > 0 ) {
1249                                  $s .= $this->expand( $closes->item( 0 ), $flags );
1250                              }
1251                          } else {
1252                              $s .= '/>';
1253                          }
1254                          $out .= $s;
1255                      } else {
1256                          $params = array(
1257                              'name' => new PPNode_DOM( $names->item( 0 ) ),
1258                              'attr' => $attrs->length > 0 ? new PPNode_DOM( $attrs->item( 0 ) ) : null,
1259                              'inner' => $inners->length > 0 ? new PPNode_DOM( $inners->item( 0 ) ) : null,
1260                              'close' => $closes->length > 0 ? new PPNode_DOM( $closes->item( 0 ) ) : null,
1261                          );
1262                          $out .= $this->parser->extensionSubstitution( $params, $this );
1263                      }
1264                  } elseif ( $contextNode->nodeName == 'h' ) {
1265                      # Heading
1266                      $s = $this->expand( $contextNode->childNodes, $flags );
1267  
1268                      # Insert a heading marker only for <h> children of <root>
1269                      # This is to stop extractSections from going over multiple tree levels
1270                      if ( $contextNode->parentNode->nodeName == 'root' && $this->parser->ot['html'] ) {
1271                          # Insert heading index marker
1272                          $headingIndex = $contextNode->getAttribute( 'i' );
1273                          $titleText = $this->title->getPrefixedDBkey();
1274                          $this->parser->mHeadings[] = array( $titleText, $headingIndex );
1275                          $serial = count( $this->parser->mHeadings ) - 1;
1276                          $marker = "{$this->parser->mUniqPrefix}-h-$serial-" . Parser::MARKER_SUFFIX;
1277                          $count = $contextNode->getAttribute( 'level' );
1278                          $s = substr( $s, 0, $count ) . $marker . substr( $s, $count );
1279                          $this->parser->mStripState->addGeneral( $marker, '' );
1280                      }
1281                      $out .= $s;
1282                  } else {
1283                      # Generic recursive expansion
1284                      $newIterator = $contextNode->childNodes;
1285                  }
1286              } else {
1287                  wfProfileOut( __METHOD__ );
1288                  throw new MWException( __METHOD__ . ': Invalid parameter type' );
1289              }
1290  
1291              if ( $newIterator !== false ) {
1292                  if ( $newIterator instanceof PPNode_DOM ) {
1293                      $newIterator = $newIterator->node;
1294                  }
1295                  $outStack[] = '';
1296                  $iteratorStack[] = $newIterator;
1297                  $indexStack[] = 0;
1298              } elseif ( $iteratorStack[$level] === false ) {
1299                  // Return accumulated value to parent
1300                  // With tail recursion
1301                  while ( $iteratorStack[$level] === false && $level > 0 ) {
1302                      $outStack[$level - 1] .= $out;
1303                      array_pop( $outStack );
1304                      array_pop( $iteratorStack );
1305                      array_pop( $indexStack );
1306                      $level--;
1307                  }
1308              }
1309          }
1310          --$expansionDepth;
1311          wfProfileOut( __METHOD__ );
1312          return $outStack[0];
1313      }
1314  
1315      /**
1316       * @param string $sep
1317       * @param int $flags
1318       * @param string|PPNode_DOM|DOMDocument $args,...
1319       * @return string
1320       */
1321  	public function implodeWithFlags( $sep, $flags /*, ... */ ) {
1322          $args = array_slice( func_get_args(), 2 );
1323  
1324          $first = true;
1325          $s = '';
1326          foreach ( $args as $root ) {
1327              if ( $root instanceof PPNode_DOM ) {
1328                  $root = $root->node;
1329              }
1330              if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
1331                  $root = array( $root );
1332              }
1333              foreach ( $root as $node ) {
1334                  if ( $first ) {
1335                      $first = false;
1336                  } else {
1337                      $s .= $sep;
1338                  }
1339                  $s .= $this->expand( $node, $flags );
1340              }
1341          }
1342          return $s;
1343      }
1344  
1345      /**
1346       * Implode with no flags specified
1347       * This previously called implodeWithFlags but has now been inlined to reduce stack depth
1348       *
1349       * @param string $sep
1350       * @param string|PPNode_DOM|DOMDocument $args,...
1351       * @return string
1352       */
1353  	public function implode( $sep /*, ... */ ) {
1354          $args = array_slice( func_get_args(), 1 );
1355  
1356          $first = true;
1357          $s = '';
1358          foreach ( $args as $root ) {
1359              if ( $root instanceof PPNode_DOM ) {
1360                  $root = $root->node;
1361              }
1362              if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
1363                  $root = array( $root );
1364              }
1365              foreach ( $root as $node ) {
1366                  if ( $first ) {
1367                      $first = false;
1368                  } else {
1369                      $s .= $sep;
1370                  }
1371                  $s .= $this->expand( $node );
1372              }
1373          }
1374          return $s;
1375      }
1376  
1377      /**
1378       * Makes an object that, when expand()ed, will be the same as one obtained
1379       * with implode()
1380       *
1381       * @param string $sep
1382       * @param string|PPNode_DOM|DOMDocument $args,...
1383       * @return array
1384       */
1385  	public function virtualImplode( $sep /*, ... */ ) {
1386          $args = array_slice( func_get_args(), 1 );
1387          $out = array();
1388          $first = true;
1389  
1390          foreach ( $args as $root ) {
1391              if ( $root instanceof PPNode_DOM ) {
1392                  $root = $root->node;
1393              }
1394              if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
1395                  $root = array( $root );
1396              }
1397              foreach ( $root as $node ) {
1398                  if ( $first ) {
1399                      $first = false;
1400                  } else {
1401                      $out[] = $sep;
1402                  }
1403                  $out[] = $node;
1404              }
1405          }
1406          return $out;
1407      }
1408  
1409      /**
1410       * Virtual implode with brackets
1411       * @param string $start
1412       * @param string $sep
1413       * @param string $end
1414       * @param string|PPNode_DOM|DOMDocument $args,...
1415       * @return array
1416       */
1417  	public function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) {
1418          $args = array_slice( func_get_args(), 3 );
1419          $out = array( $start );
1420          $first = true;
1421  
1422          foreach ( $args as $root ) {
1423              if ( $root instanceof PPNode_DOM ) {
1424                  $root = $root->node;
1425              }
1426              if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
1427                  $root = array( $root );
1428              }
1429              foreach ( $root as $node ) {
1430                  if ( $first ) {
1431                      $first = false;
1432                  } else {
1433                      $out[] = $sep;
1434                  }
1435                  $out[] = $node;
1436              }
1437          }
1438          $out[] = $end;
1439          return $out;
1440      }
1441  
1442  	public function __toString() {
1443          return 'frame{}';
1444      }
1445  
1446  	public function getPDBK( $level = false ) {
1447          if ( $level === false ) {
1448              return $this->title->getPrefixedDBkey();
1449          } else {
1450              return isset( $this->titleCache[$level] ) ? $this->titleCache[$level] : false;
1451          }
1452      }
1453  
1454      /**
1455       * @return array
1456       */
1457  	public function getArguments() {
1458          return array();
1459      }
1460  
1461      /**
1462       * @return array
1463       */
1464  	public function getNumberedArguments() {
1465          return array();
1466      }
1467  
1468      /**
1469       * @return array
1470       */
1471  	public function getNamedArguments() {
1472          return array();
1473      }
1474  
1475      /**
1476       * Returns true if there are no arguments in this frame
1477       *
1478       * @return bool
1479       */
1480  	public function isEmpty() {
1481          return true;
1482      }
1483  
1484  	public function getArgument( $name ) {
1485          return false;
1486      }
1487  
1488      /**
1489       * Returns true if the infinite loop check is OK, false if a loop is detected
1490       *
1491       * @param Title $title
1492       * @return bool
1493       */
1494  	public function loopCheck( $title ) {
1495          return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] );
1496      }
1497  
1498      /**
1499       * Return true if the frame is a template frame
1500       *
1501       * @return bool
1502       */
1503  	public function isTemplate() {
1504          return false;
1505      }
1506  
1507      /**
1508       * Get a title of frame
1509       *
1510       * @return Title
1511       */
1512  	public function getTitle() {
1513          return $this->title;
1514      }
1515  
1516      /**
1517       * Set the volatile flag
1518       *
1519       * @param bool $flag
1520       */
1521  	public function setVolatile( $flag = true ) {
1522          $this->volatile = $flag;
1523      }
1524  
1525      /**
1526       * Get the volatile flag
1527       *
1528       * @return bool
1529       */
1530  	public function isVolatile() {
1531          return $this->volatile;
1532      }
1533  
1534      /**
1535       * Set the TTL
1536       *
1537       * @param int $ttl
1538       */
1539  	public function setTTL( $ttl ) {
1540          if ( $ttl !== null && ( $this->ttl === null || $ttl < $this->ttl ) ) {
1541              $this->ttl = $ttl;
1542          }
1543      }
1544  
1545      /**
1546       * Get the TTL
1547       *
1548       * @return int|null
1549       */
1550  	public function getTTL() {
1551          return $this->ttl;
1552      }
1553  }
1554  
1555  /**
1556   * Expansion frame with template arguments
1557   * @ingroup Parser
1558   * @codingStandardsIgnoreStart
1559   */
1560  class PPTemplateFrame_DOM extends PPFrame_DOM {
1561      // @codingStandardsIgnoreEnd
1562  
1563      public $numberedArgs, $namedArgs;
1564  
1565      /**
1566       * @var PPFrame_DOM
1567       */
1568      public $parent;
1569      public $numberedExpansionCache, $namedExpansionCache;
1570  
1571      /**
1572       * @param Preprocessor $preprocessor
1573       * @param bool|PPFrame_DOM $parent
1574       * @param array $numberedArgs
1575       * @param array $namedArgs
1576       * @param bool|Title $title
1577       */
1578  	public function __construct( $preprocessor, $parent = false, $numberedArgs = array(),
1579          $namedArgs = array(), $title = false
1580      ) {
1581          parent::__construct( $preprocessor );
1582  
1583          $this->parent = $parent;
1584          $this->numberedArgs = $numberedArgs;
1585          $this->namedArgs = $namedArgs;
1586          $this->title = $title;
1587          $pdbk = $title ? $title->getPrefixedDBkey() : false;
1588          $this->titleCache = $parent->titleCache;
1589          $this->titleCache[] = $pdbk;
1590          $this->loopCheckHash = /*clone*/ $parent->loopCheckHash;
1591          if ( $pdbk !== false ) {
1592              $this->loopCheckHash[$pdbk] = true;
1593          }
1594          $this->depth = $parent->depth + 1;
1595          $this->numberedExpansionCache = $this->namedExpansionCache = array();
1596      }
1597  
1598  	public function __toString() {
1599          $s = 'tplframe{';
1600          $first = true;
1601          $args = $this->numberedArgs + $this->namedArgs;
1602          foreach ( $args as $name => $value ) {
1603              if ( $first ) {
1604                  $first = false;
1605              } else {
1606                  $s .= ', ';
1607              }
1608              $s .= "\"$name\":\"" .
1609                  str_replace( '"', '\\"', $value->ownerDocument->saveXML( $value ) ) . '"';
1610          }
1611          $s .= '}';
1612          return $s;
1613      }
1614  
1615      /**
1616       * @throws MWException
1617       * @param string|int $key
1618       * @param string|PPNode_DOM|DOMDocument $root
1619       * @param int $flags
1620       * @return string
1621       */
1622  	public function cachedExpand( $key, $root, $flags = 0 ) {
1623          if ( isset( $this->parent->childExpansionCache[$key] ) ) {
1624              return $this->parent->childExpansionCache[$key];
1625          }
1626          $retval = $this->expand( $root, $flags );
1627          if ( !$this->isVolatile() ) {
1628              $this->parent->childExpansionCache[$key] = $retval;
1629          }
1630          return $retval;
1631      }
1632  
1633      /**
1634       * Returns true if there are no arguments in this frame
1635       *
1636       * @return bool
1637       */
1638  	public function isEmpty() {
1639          return !count( $this->numberedArgs ) && !count( $this->namedArgs );
1640      }
1641  
1642  	public function getArguments() {
1643          $arguments = array();
1644          foreach ( array_merge(
1645                  array_keys( $this->numberedArgs ),
1646                  array_keys( $this->namedArgs ) ) as $key ) {
1647              $arguments[$key] = $this->getArgument( $key );
1648          }
1649          return $arguments;
1650      }
1651  
1652  	public function getNumberedArguments() {
1653          $arguments = array();
1654          foreach ( array_keys( $this->numberedArgs ) as $key ) {
1655              $arguments[$key] = $this->getArgument( $key );
1656          }
1657          return $arguments;
1658      }
1659  
1660  	public function getNamedArguments() {
1661          $arguments = array();
1662          foreach ( array_keys( $this->namedArgs ) as $key ) {
1663              $arguments[$key] = $this->getArgument( $key );
1664          }
1665          return $arguments;
1666      }
1667  
1668  	public function getNumberedArgument( $index ) {
1669          if ( !isset( $this->numberedArgs[$index] ) ) {
1670              return false;
1671          }
1672          if ( !isset( $this->numberedExpansionCache[$index] ) ) {
1673              # No trimming for unnamed arguments
1674              $this->numberedExpansionCache[$index] = $this->parent->expand(
1675                  $this->numberedArgs[$index],
1676                  PPFrame::STRIP_COMMENTS
1677              );
1678          }
1679          return $this->numberedExpansionCache[$index];
1680      }
1681  
1682  	public function getNamedArgument( $name ) {
1683          if ( !isset( $this->namedArgs[$name] ) ) {
1684              return false;
1685          }
1686          if ( !isset( $this->namedExpansionCache[$name] ) ) {
1687              # Trim named arguments post-expand, for backwards compatibility
1688              $this->namedExpansionCache[$name] = trim(
1689                  $this->parent->expand( $this->namedArgs[$name], PPFrame::STRIP_COMMENTS ) );
1690          }
1691          return $this->namedExpansionCache[$name];
1692      }
1693  
1694  	public function getArgument( $name ) {
1695          $text = $this->getNumberedArgument( $name );
1696          if ( $text === false ) {
1697              $text = $this->getNamedArgument( $name );
1698          }
1699          return $text;
1700      }
1701  
1702      /**
1703       * Return true if the frame is a template frame
1704       *
1705       * @return bool
1706       */
1707  	public function isTemplate() {
1708          return true;
1709      }
1710  
1711  	public function setVolatile( $flag = true ) {
1712          parent::setVolatile( $flag );
1713          $this->parent->setVolatile( $flag );
1714      }
1715  
1716  	public function setTTL( $ttl ) {
1717          parent::setTTL( $ttl );
1718          $this->parent->setTTL( $ttl );
1719      }
1720  }
1721  
1722  /**
1723   * Expansion frame with custom arguments
1724   * @ingroup Parser
1725   * @codingStandardsIgnoreStart
1726   */
1727  class PPCustomFrame_DOM extends PPFrame_DOM {
1728      // @codingStandardsIgnoreEnd
1729  
1730      public $args;
1731  
1732  	public function __construct( $preprocessor, $args ) {
1733          parent::__construct( $preprocessor );
1734          $this->args = $args;
1735      }
1736  
1737  	public function __toString() {
1738          $s = 'cstmframe{';
1739          $first = true;
1740          foreach ( $this->args as $name => $value ) {
1741              if ( $first ) {
1742                  $first = false;
1743              } else {
1744                  $s .= ', ';
1745              }
1746              $s .= "\"$name\":\"" .
1747                  str_replace( '"', '\\"', $value->__toString() ) . '"';
1748          }
1749          $s .= '}';
1750          return $s;
1751      }
1752  
1753      /**
1754       * @return bool
1755       */
1756  	public function isEmpty() {
1757          return !count( $this->args );
1758      }
1759  
1760  	public function getArgument( $index ) {
1761          if ( !isset( $this->args[$index] ) ) {
1762              return false;
1763          }
1764          return $this->args[$index];
1765      }
1766  
1767  	public function getArguments() {
1768          return $this->args;
1769      }
1770  }
1771  
1772  /**
1773   * @ingroup Parser
1774   * @codingStandardsIgnoreStart
1775   */
1776  class PPNode_DOM implements PPNode {
1777      // @codingStandardsIgnoreEnd
1778  
1779      /**
1780       * @var DOMElement
1781       */
1782      public $node;
1783      public $xpath;
1784  
1785  	public function __construct( $node, $xpath = false ) {
1786          $this->node = $node;
1787      }
1788  
1789      /**
1790       * @return DOMXPath
1791       */
1792  	public function getXPath() {
1793          if ( $this->xpath === null ) {
1794              $this->xpath = new DOMXPath( $this->node->ownerDocument );
1795          }
1796          return $this->xpath;
1797      }
1798  
1799  	public function __toString() {
1800          if ( $this->node instanceof DOMNodeList ) {
1801              $s = '';
1802              foreach ( $this->node as $node ) {
1803                  $s .= $node->ownerDocument->saveXML( $node );
1804              }
1805          } else {
1806              $s = $this->node->ownerDocument->saveXML( $this->node );
1807          }
1808          return $s;
1809      }
1810  
1811      /**
1812       * @return bool|PPNode_DOM
1813       */
1814  	public function getChildren() {
1815          return $this->node->childNodes ? new self( $this->node->childNodes ) : false;
1816      }
1817  
1818      /**
1819       * @return bool|PPNode_DOM
1820       */
1821  	public function getFirstChild() {
1822          return $this->node->firstChild ? new self( $this->node->firstChild ) : false;
1823      }
1824  
1825      /**
1826       * @return bool|PPNode_DOM
1827       */
1828  	public function getNextSibling() {
1829          return $this->node->nextSibling ? new self( $this->node->nextSibling ) : false;
1830      }
1831  
1832      /**
1833       * @param string $type
1834       *
1835       * @return bool|PPNode_DOM
1836       */
1837  	public function getChildrenOfType( $type ) {
1838          return new self( $this->getXPath()->query( $type, $this->node ) );
1839      }
1840  
1841      /**
1842       * @return int
1843       */
1844  	public function getLength() {
1845          if ( $this->node instanceof DOMNodeList ) {
1846              return $this->node->length;
1847          } else {
1848              return false;
1849          }
1850      }
1851  
1852      /**
1853       * @param int $i
1854       * @return bool|PPNode_DOM
1855       */
1856  	public function item( $i ) {
1857          $item = $this->node->item( $i );
1858          return $item ? new self( $item ) : false;
1859      }
1860  
1861      /**
1862       * @return string
1863       */
1864  	public function getName() {
1865          if ( $this->node instanceof DOMNodeList ) {
1866              return '#nodelist';
1867          } else {
1868              return $this->node->nodeName;
1869          }
1870      }
1871  
1872      /**
1873       * Split a "<part>" node into an associative array containing:
1874       *  - name          PPNode name
1875       *  - index         String index
1876       *  - value         PPNode value
1877       *
1878       * @throws MWException
1879       * @return array
1880       */
1881  	public function splitArg() {
1882          $xpath = $this->getXPath();
1883          $names = $xpath->query( 'name', $this->node );
1884          $values = $xpath->query( 'value', $this->node );
1885          if ( !$names->length || !$values->length ) {
1886              throw new MWException( 'Invalid brace node passed to ' . __METHOD__ );
1887          }
1888          $name = $names->item( 0 );
1889          $index = $name->getAttribute( 'index' );
1890          return array(
1891              'name' => new self( $name ),
1892              'index' => $index,
1893              'value' => new self( $values->item( 0 ) ) );
1894      }
1895  
1896      /**
1897       * Split an "<ext>" node into an associative array containing name, attr, inner and close
1898       * All values in the resulting array are PPNodes. Inner and close are optional.
1899       *
1900       * @throws MWException
1901       * @return array
1902       */
1903  	public function splitExt() {
1904          $xpath = $this->getXPath();
1905          $names = $xpath->query( 'name', $this->node );
1906          $attrs = $xpath->query( 'attr', $this->node );
1907          $inners = $xpath->query( 'inner', $this->node );
1908          $closes = $xpath->query( 'close', $this->node );
1909          if ( !$names->length || !$attrs->length ) {
1910              throw new MWException( 'Invalid ext node passed to ' . __METHOD__ );
1911          }
1912          $parts = array(
1913              'name' => new self( $names->item( 0 ) ),
1914              'attr' => new self( $attrs->item( 0 ) ) );
1915          if ( $inners->length ) {
1916              $parts['inner'] = new self( $inners->item( 0 ) );
1917          }
1918          if ( $closes->length ) {
1919              $parts['close'] = new self( $closes->item( 0 ) );
1920          }
1921          return $parts;
1922      }
1923  
1924      /**
1925       * Split a "<h>" node
1926       * @throws MWException
1927       * @return array
1928       */
1929  	public function splitHeading() {
1930          if ( $this->getName() !== 'h' ) {
1931              throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
1932          }
1933          return array(
1934              'i' => $this->node->getAttribute( 'i' ),
1935              'level' => $this->node->getAttribute( 'level' ),
1936              'contents' => $this->getChildren()
1937          );
1938      }
1939  }


Generated: Fri Nov 28 14:03:12 2014 Cross-referenced by PHPXref 0.7.1