[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

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

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


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