Verzeichnisstruktur phpBB-3.3.16


Veröffentlicht
27.04.2026

So funktioniert es


Auf das letzte Element klicken. Dies geht jeweils ein Schritt zurück

Auf das Icon klicken, dies öffnet das Verzeichnis. Nochmal klicken schließt das Verzeichnis.
Auf den Verzeichnisnamen klicken, dies zeigt nur das Verzeichnis mit Inhalt an

      (Beispiel Datei-Icons)

Auf das Icon klicken um den Quellcode anzuzeigen

Quick.php

Zuletzt modifiziert: 01.05.2026, 11:28 - Dateigröße: 21.66 KiB


 001  <?php
 002   
 003  /**
 004  * @package   s9e\TextFormatter
 005  * @copyright Copyright (c) 2010-2022 The s9e authors
 006  * @license   http://www.opensource.org/licenses/mit-license.php The MIT License
 007  */
 008  namespace s9e\TextFormatter\Configurator\RendererGenerators\PHP;
 009   
 010  use Closure;
 011  use RuntimeException;
 012  use s9e\TextFormatter\Configurator\Helpers\RegexpBuilder;
 013   
 014  class Quick
 015  {
 016      /**
 017      * Generate the Quick renderer's source
 018      *
 019      * @param  array  $compiledTemplates Array of tagName => compiled template
 020      * @return string
 021      */
 022      public static function getSource(array $compiledTemplates)
 023      {
 024          $map         = ['dynamic' => [], 'php' => [], 'static' => []];
 025          $tagNames    = [];
 026          $unsupported = [];
 027   
 028          // Ignore system tags
 029          unset($compiledTemplates['br']);
 030          unset($compiledTemplates['e']);
 031          unset($compiledTemplates['i']);
 032          unset($compiledTemplates['p']);
 033          unset($compiledTemplates['s']);
 034   
 035          foreach ($compiledTemplates as $tagName => $php)
 036          {
 037              $renderings = self::getRenderingStrategy($php);
 038              if (empty($renderings))
 039              {
 040                  $unsupported[] = $tagName;
 041                  continue;
 042              }
 043   
 044              foreach ($renderings as $i => list($strategy, $replacement))
 045              {
 046                  $match = (($i) ? '/' : '') . $tagName;
 047                  $map[$strategy][$match] = $replacement;
 048              }
 049   
 050              // Record the names of tags whose template does not contain a passthrough
 051              if (!isset($renderings[1]))
 052              {
 053                  $tagNames[] = $tagName;
 054              }
 055          }
 056   
 057          $php = [];
 058          $php[] = '    /** {@inheritdoc} */';
 059          $php[] = '    public $enableQuickRenderer=true;';
 060          $php[] = '    /** {@inheritdoc} */';
 061          $php[] = '    protected $static=' . self::export($map['static']) . ';';
 062          $php[] = '    /** {@inheritdoc} */';
 063          $php[] = '    protected $dynamic=' . self::export($map['dynamic']) . ';';
 064   
 065          $quickSource = '';
 066          if (!empty($map['php']))
 067          {
 068              $quickSource = SwitchStatement::generate('$id', $map['php']);
 069          }
 070   
 071          // Build a regexp that matches all the tags
 072          $regexp  = '(<(?:(?!/)(';
 073          $regexp .= ($tagNames) ? RegexpBuilder::fromList($tagNames) : '(?!)';
 074          $regexp .= ')(?: [^>]*)?>.*?</\\1|(/?(?!br/|p>)[^ />]+)[^>]*?(/)?)>)s';
 075          $php[] = '    /** {@inheritdoc} */';
 076          $php[] = '    protected $quickRegexp=' . var_export($regexp, true) . ';';
 077   
 078          // Build a regexp that matches tags that cannot be rendered with the Quick renderer
 079          if (!empty($unsupported))
 080          {
 081              $regexp = '((?<=<)(?:[!?]|' . RegexpBuilder::fromList($unsupported) . '[ />]))';
 082              $php[]  = '    /** {@inheritdoc} */';
 083              $php[]  = '    protected $quickRenderingTest=' . var_export($regexp, true) . ';';
 084          }
 085   
 086          $php[] = '    /** {@inheritdoc} */';
 087          $php[] = '    protected function renderQuickTemplate($id, $xml)';
 088          $php[] = '    {';
 089          $php[] = '        $attributes=$this->matchAttributes($xml);';
 090          $php[] = "        \$html='';" . $quickSource;
 091          $php[] = '';
 092          $php[] = '        return $html;';
 093          $php[] = '    }';
 094   
 095          return implode("\n", $php);
 096      }
 097   
 098      /**
 099      * Export an array as PHP
 100      *
 101      * @param  array  $arr
 102      * @return string
 103      */
 104      protected static function export(array $arr)
 105      {
 106          $exportKeys = (array_keys($arr) !== range(0, count($arr) - 1));
 107          ksort($arr);
 108   
 109          $entries = [];
 110          foreach ($arr as $k => $v)
 111          {
 112              $entries[] = (($exportKeys) ? var_export($k, true) . '=>' : '')
 113                         . ((is_array($v)) ? self::export($v) : var_export($v, true));
 114          }
 115   
 116          return '[' . implode(',', $entries) . ']';
 117      }
 118   
 119      /**
 120      * Compute the rendering strategy for a compiled template
 121      *
 122      * @param  string  $php Template compiled for the PHP renderer
 123      * @return array[]      An array containing 0 to 2 pairs of [<rendering type>, <replacement>]
 124      */
 125      public static function getRenderingStrategy($php)
 126      {
 127          $phpRenderings = self::getQuickRendering($php);
 128          if (empty($phpRenderings))
 129          {
 130              return [];
 131          }
 132          $renderings = self::getStringRenderings($php);
 133   
 134          // Keep string rendering where possible, use PHP rendering wherever else
 135          foreach ($phpRenderings as $i => $phpRendering)
 136          {
 137              if (!isset($renderings[$i]) || strpos($phpRendering, '$this->attributes[]') !== false)
 138              {
 139                  $renderings[$i] = ['php', $phpRendering];
 140              }
 141          }
 142   
 143          return $renderings;
 144      }
 145   
 146      /**
 147      * Generate the code for rendering a compiled template with the Quick renderer
 148      *
 149      * Parse and record every code path that contains a passthrough. Parse every if-else structure.
 150      * When the whole structure is parsed, there are 2 possible situations:
 151      *  - no code path contains a passthrough, in which case we discard the data
 152      *  - all the code paths including the mandatory "else" branch contain a passthrough, in which
 153      *    case we keep the data
 154      *
 155      * @param  string     $php Template compiled for the PHP renderer
 156      * @return string[]        An array containing one or two strings of PHP, or an empty array
 157      *                         if the PHP cannot be converted
 158      */
 159      protected static function getQuickRendering($php)
 160      {
 161          // xsl:apply-templates elements with a select expression and switch statements are not supported
 162          if (preg_match('(\\$this->at\\((?!\\$node\\);)|switch\()', $php))
 163          {
 164              return [];
 165          }
 166   
 167          // Tokenize the PHP and add an empty token as terminator
 168          $tokens   = token_get_all('<?php ' . $php);
 169          $tokens[] = [0, ''];
 170   
 171          // Remove the first token, which is a T_OPEN_TAG
 172          array_shift($tokens);
 173          $cnt = count($tokens);
 174   
 175          // Prepare the main branch
 176          $branch = [
 177              // We purposefully use a value that can never match
 178              'braces'      => -1,
 179              'branches'    => [],
 180              'head'        => '',
 181              'passthrough' => 0,
 182              'statement'   => '',
 183              'tail'        => ''
 184          ];
 185   
 186          $braces = 0;
 187          $i = 0;
 188          do
 189          {
 190              // Test whether we've reached a passthrough
 191              if ($tokens[$i    ][0] === T_VARIABLE
 192               && $tokens[$i    ][1] === '$this'
 193               && $tokens[$i + 1][0] === T_OBJECT_OPERATOR
 194               && $tokens[$i + 2][0] === T_STRING
 195               && $tokens[$i + 2][1] === 'at'
 196               && $tokens[$i + 3]    === '('
 197               && $tokens[$i + 4][0] === T_VARIABLE
 198               && $tokens[$i + 4][1] === '$node'
 199               && $tokens[$i + 5]    === ')'
 200               && $tokens[$i + 6]    === ';')
 201              {
 202                  if (++$branch['passthrough'] > 1)
 203                  {
 204                      // Multiple passthroughs are not supported
 205                      return [];
 206                  }
 207   
 208                  // Skip to the semi-colon
 209                  $i += 6;
 210   
 211                  continue;
 212              }
 213   
 214              $key = ($branch['passthrough']) ? 'tail' : 'head';
 215              $branch[$key] .= (is_array($tokens[$i])) ? $tokens[$i][1] : $tokens[$i];
 216   
 217              if ($tokens[$i] === '{')
 218              {
 219                  ++$braces;
 220                  continue;
 221              }
 222   
 223              if ($tokens[$i] === '}')
 224              {
 225                  --$braces;
 226   
 227                  if ($branch['braces'] === $braces)
 228                  {
 229                      // Remove the last brace from the branch's content
 230                      $branch[$key] = substr($branch[$key], 0, -1);
 231   
 232                      // Jump back to the parent branch
 233                      $branch =& $branch['parent'];
 234   
 235                      // Copy the current index to look ahead
 236                      $j = $i;
 237   
 238                      // Skip whitespace
 239                      while ($tokens[++$j][0] === T_WHITESPACE);
 240   
 241                      // Test whether this is the last brace of an if-else structure by looking for
 242                      // an additional elseif/else case
 243                      if ($tokens[$j][0] !== T_ELSEIF && $tokens[$j][0] !== T_ELSE)
 244                      {
 245                          $passthroughs = self::getBranchesPassthrough($branch['branches']);
 246                          if ($passthroughs === [0])
 247                          {
 248                              // No branch was passthrough, move their PHP source back to this branch
 249                              // then discard the data
 250                              foreach ($branch['branches'] as $child)
 251                              {
 252                                  $branch['head'] .= $child['statement'] . '{' . $child['head'] . '}';
 253                              }
 254   
 255                              $branch['branches'] = [];
 256                              continue;
 257                          }
 258   
 259                          if ($passthroughs === [1])
 260                          {
 261                              // All branches were passthrough, so their parent is passthrough
 262                              ++$branch['passthrough'];
 263   
 264                              continue;
 265                          }
 266   
 267                          // Mixed branches (with/out passthrough) are not supported
 268                          return [];
 269                      }
 270                  }
 271   
 272                  continue;
 273              }
 274   
 275              // We don't have to record child branches if we know that current branch is passthrough.
 276              // If a child branch contains a passthrough, it will be treated as a multiple
 277              // passthrough and we will abort
 278              if ($branch['passthrough'])
 279              {
 280                  continue;
 281              }
 282   
 283              if ($tokens[$i][0] === T_IF
 284               || $tokens[$i][0] === T_ELSEIF
 285               || $tokens[$i][0] === T_ELSE)
 286              {
 287                  // Remove the statement from the branch's content
 288                  $branch[$key] = substr($branch[$key], 0, -strlen($tokens[$i][1]));
 289   
 290                  // Create a new branch
 291                  $branch['branches'][] = [
 292                      'braces'      => $braces,
 293                      'branches'    => [],
 294                      'head'        => '',
 295                      'parent'      => &$branch,
 296                      'passthrough' => 0,
 297                      'statement'   => '',
 298                      'tail'        => ''
 299                  ];
 300   
 301                  // Jump to the new branch
 302                  $branch =& $branch['branches'][count($branch['branches']) - 1];
 303   
 304                  // Record the PHP statement
 305                  do
 306                  {
 307                      $branch['statement'] .= (is_array($tokens[$i])) ? $tokens[$i][1] : $tokens[$i];
 308                  }
 309                  while ($tokens[++$i] !== '{');
 310   
 311                  // Account for the brace in the statement
 312                  ++$braces;
 313              }
 314          }
 315          while (++$i < $cnt);
 316   
 317          list($head, $tail) = self::buildPHP($branch['branches']);
 318          $head  = $branch['head'] . $head;
 319          $tail .= $branch['tail'];
 320   
 321          // Convert the PHP renderer source to the format used in the Quick renderer
 322          self::convertPHP($head, $tail, (bool) $branch['passthrough']);
 323   
 324          // Test whether any method call was left unconverted. If so, we cannot render this template
 325          if (preg_match('((?<!-|\\$this)->)', $head . $tail))
 326          {
 327              return [];
 328          }
 329   
 330          return ($branch['passthrough']) ? [$head, $tail] : [$head];
 331      }
 332   
 333      /**
 334      * Convert the two sides of a compiled template to quick rendering
 335      *
 336      * @param  string &$head
 337      * @param  string &$tail
 338      * @param  bool    $passthrough
 339      * @return void
 340      */
 341      protected static function convertPHP(&$head, &$tail, $passthrough)
 342      {
 343          // Test whether the attributes must be saved when rendering the head because they're needed
 344          // when rendering the tail
 345          $saveAttributes = (bool) preg_match('(\\$node->(?:get|has)Attribute)', $tail);
 346   
 347          // Collect the names of all the attributes so that we can initialize them with a null value
 348          // to avoid undefined variable notices. We exclude attributes that seem to be in an if block
 349          // that tests its existence beforehand. This last part is not an accurate process as it
 350          // would be much more expensive to do it accurately but where it fails the only consequence
 351          // is we needlessly add the attribute to the list. There is no difference in functionality
 352          preg_match_all(
 353              "(\\\$node->getAttribute\\('([^']+)'\\))",
 354              preg_replace_callback(
 355                  '(if\\(\\$node->hasAttribute\\(([^\\)]+)[^}]+)',
 356                  function ($m)
 357                  {
 358                      return str_replace('$node->getAttribute(' . $m[1] . ')', '', $m[0]);
 359                  },
 360                  $head . $tail
 361              ),
 362              $matches
 363          );
 364          $attrNames = array_unique($matches[1]);
 365   
 366          // Replace the source in $head and $tail
 367          self::replacePHP($head);
 368          self::replacePHP($tail);
 369   
 370          if (!$passthrough && strpos($head, '$node->textContent') !== false)
 371          {
 372              $head = '$textContent=$this->getQuickTextContent($xml);' . str_replace('$node->textContent', '$textContent', $head);
 373          }
 374   
 375          if (!empty($attrNames))
 376          {
 377              ksort($attrNames);
 378              $head = "\$attributes+=['" . implode("'=>null,'", $attrNames) . "'=>null];" . $head;
 379          }
 380   
 381          if ($saveAttributes)
 382          {
 383              $head .= '$this->attributes[]=$attributes;';
 384              $tail  = '$attributes=array_pop($this->attributes);' . $tail;
 385          }
 386      }
 387   
 388      /**
 389      * Replace the PHP code used in a compiled template to be used by the Quick renderer
 390      *
 391      * @param  string &$php
 392      * @return void
 393      */
 394      protected static function replacePHP(&$php)
 395      {
 396          // Expression that matches a $node->getAttribute() call and captures its string argument
 397          $getAttribute = "\\\$node->getAttribute\\(('[^']+')\\)";
 398   
 399          // Expression that matches a single-quoted string literal
 400          $string       = "'(?:[^\\\\']|\\\\.)*+'";
 401   
 402          $replacements = [
 403              '$this->out' => '$html',
 404   
 405              // An attribute value escaped as ENT_NOQUOTES. We only need to unescape quotes
 406              '(htmlspecialchars\\(' . $getAttribute . ',' . ENT_NOQUOTES . '\\))'
 407                  => "str_replace('&quot;','\"',\$attributes[\$1]??'')",
 408   
 409              // One or several attribute values escaped as ENT_COMPAT can be used as-is
 410              '((\\.?)htmlspecialchars\\((' . $getAttribute . '(?:\\.' . $getAttribute . ')*),' . ENT_COMPAT . '\\)(\\.?))'
 411                  => function ($m) use ($getAttribute)
 412                  {
 413                      $replacement = (strpos($m[0], '.') === false) ? '($attributes[$1]??\'\')' : '$attributes[$1]';
 414   
 415                      return $m[1] . preg_replace('(' . $getAttribute . ')', $replacement, $m[2]) . $m[5];
 416                  },
 417   
 418              // Character replacement can be performed directly on the escaped value provided that it
 419              // is then escaped as ENT_COMPAT and that replacements do not interfere with the escaping
 420              // of the characters &<>" or their representation &amp;&lt;&gt;&quot;
 421              '(htmlspecialchars\\(strtr\\(' . $getAttribute . ",('[^\"&\\\\';<>aglmopqtu]+'),('[^\"&\\\\'<>]+')\\)," . ENT_COMPAT . '\\))'
 422                  => 'strtr($attributes[$1]??\'\',$2,$3)',
 423   
 424              // A comparison between two attributes. No need to unescape
 425              '(' . $getAttribute . '(!?=+)' . $getAttribute . ')'
 426                  => '$attributes[$1]$2$attributes[$3]',
 427   
 428              // A comparison between an attribute and a literal string. Rather than unescape the
 429              // attribute value, we escape the literal. This applies to comparisons using XPath's
 430              // contains() as well (translated to PHP's strpos())
 431              '(' . $getAttribute . '===(' . $string . '))s'
 432                  => function ($m)
 433                  {
 434                      return '$attributes[' . $m[1] . ']===' . htmlspecialchars($m[2], ENT_COMPAT);
 435                  },
 436   
 437              '((' . $string . ')===' . $getAttribute . ')s'
 438                  => function ($m)
 439                  {
 440                      return htmlspecialchars($m[1], ENT_COMPAT) . '===$attributes[' . $m[2] . ']';
 441                  },
 442   
 443              '(strpos\\(' . $getAttribute . ',(' . $string . ')\\)([!=]==(?:0|false)))s'
 444                  => function ($m)
 445                  {
 446                      return 'strpos($attributes[' . $m[1] . "]??''," . htmlspecialchars($m[2], ENT_COMPAT) . ')' . $m[3];
 447                  },
 448   
 449              '(strpos\\((' . $string . '),' . $getAttribute . '\\)([!=]==(?:0|false)))s'
 450                  => function ($m)
 451                  {
 452                      return 'strpos(' . htmlspecialchars($m[1], ENT_COMPAT) . ',$attributes[' . $m[2] . "]??'')" . $m[3];
 453                  },
 454   
 455              '(str_(contains|(?:end|start)s_with)\\(' . $getAttribute . ',(' . $string . ')\\))s'
 456                  => function ($m)
 457                  {
 458                      return 'str_' . $m[1] . '($attributes[' . $m[2] . "]??''," . htmlspecialchars($m[3], ENT_COMPAT) . ')';
 459                  },
 460   
 461              '(str_(contains|(?:end|start)s_with)\\((' . $string . '),' . $getAttribute . '\\))s'
 462                  => function ($m)
 463                  {
 464                      return 'str_' . $m[1] . '(' . htmlspecialchars($m[2], ENT_COMPAT) . ',$attributes[' . $m[3] . "]??'')";
 465                  },
 466   
 467              // An attribute value used in an arithmetic comparison or operation does not need to be
 468              // unescaped. The same applies to empty(), isset() and conditionals
 469              '(' . $getAttribute . '(?=(?:==|[-+*])\\d+))'  => '$attributes[$1]',
 470              '(\\b(\\d+(?:==|[-+*]))' . $getAttribute . ')' => '$1$attributes[$2]',
 471              '(empty\\(' . $getAttribute . '\\))'           => 'empty($attributes[$1])',
 472              "(\\\$node->hasAttribute\\(('[^']+')\\))"      => 'isset($attributes[$1])',
 473              'if($node->attributes->length)'                => 'if($this->hasNonNullValues($attributes))',
 474   
 475              // In all other situations, unescape the attribute value before use
 476              '(' . $getAttribute . ')' => 'htmlspecialchars_decode($attributes[$1]??\'\')'
 477          ];
 478   
 479          foreach ($replacements as $match => $replace)
 480          {
 481              if ($replace instanceof Closure)
 482              {
 483                  $php = preg_replace_callback($match, $replace, $php);
 484              }
 485              elseif ($match[0] === '(')
 486              {
 487                  $php = preg_replace($match, $replace, $php);
 488              }
 489              else
 490              {
 491                  $php = str_replace($match, $replace, $php);
 492              }
 493          }
 494      }
 495   
 496      /**
 497      * Build the source for the two sides of a templates based on the structure extracted from its
 498      * original source
 499      *
 500      * @param  array    $branches
 501      * @return string[]
 502      */
 503      protected static function buildPHP(array $branches)
 504      {
 505          $return = ['', ''];
 506          foreach ($branches as $branch)
 507          {
 508              $return[0] .= $branch['statement'] . '{' . $branch['head'];
 509              $return[1] .= $branch['statement'] . '{';
 510   
 511              if ($branch['branches'])
 512              {
 513                  list($head, $tail) = self::buildPHP($branch['branches']);
 514   
 515                  $return[0] .= $head;
 516                  $return[1] .= $tail;
 517              }
 518   
 519              $return[0] .= '}';
 520              $return[1] .= $branch['tail'] . '}';
 521          }
 522   
 523          return $return;
 524      }
 525   
 526      /**
 527      * Get the unique values for the "passthrough" key of given branches
 528      *
 529      * @param  array     $branches
 530      * @return integer[]
 531      */
 532      protected static function getBranchesPassthrough(array $branches)
 533      {
 534          $values = [];
 535          foreach ($branches as $branch)
 536          {
 537              $values[] = $branch['passthrough'];
 538          }
 539   
 540          // If the last branch isn't an "else", we act as if there was an additional branch with no
 541          // passthrough
 542          if ($branch['statement'] !== 'else')
 543          {
 544              $values[] = 0;
 545          }
 546   
 547          return array_unique($values);
 548      }
 549   
 550      /**
 551      * Get a string suitable as a preg_replace() replacement for given PHP code
 552      *
 553      * @param  string     $php Original code
 554      * @return array|bool      Array of [regexp, replacement] if possible, or FALSE otherwise
 555      */
 556      protected static function getDynamicRendering($php)
 557      {
 558          $rendering = '';
 559   
 560          $literal   = "(?<literal>'((?>[^'\\\\]+|\\\\['\\\\])*)')";
 561          $attribute = "(?<attribute>htmlspecialchars\\(\\\$node->getAttribute\\('([^']+)'\\),2\\))";
 562          $value     = "(?<value>$literal|$attribute)";
 563          $output    = "(?<output>\\\$this->out\\.=$value(?:\\.(?&value))*;)";
 564   
 565          $copyOfAttribute = "(?<copyOfAttribute>if\\(\\\$node->hasAttribute\\('([^']+)'\\)\\)\\{\\\$this->out\\.=' \\g-1=\"'\\.htmlspecialchars\\(\\\$node->getAttribute\\('\\g-1'\\),2\\)\\.'\"';\\})";
 566   
 567          $regexp = '(^(' . $output . '|' . $copyOfAttribute . ')*$)';
 568          if (!preg_match($regexp, $php, $m))
 569          {
 570              return false;
 571          }
 572   
 573          // Attributes that are copied in the replacement
 574          $copiedAttributes = [];
 575   
 576          // Attributes whose value is used in the replacement
 577          $usedAttributes = [];
 578   
 579          $regexp = '(' . $output . '|' . $copyOfAttribute . ')A';
 580          $offset = 0;
 581          while (preg_match($regexp, $php, $m, 0, $offset))
 582          {
 583              // Test whether it's normal output or a copy of attribute
 584              if ($m['output'])
 585              {
 586                  // 12 === strlen('$this->out.=')
 587                  $offset += 12;
 588   
 589                  while (preg_match('(' . $value . ')A', $php, $m, 0, $offset))
 590                  {
 591                      // Test whether it's a literal or an attribute value
 592                      if ($m['literal'])
 593                      {
 594                          // Unescape the literal
 595                          $str = stripslashes(substr($m[0], 1, -1));
 596   
 597                          // Escape special characters
 598                          $rendering .= preg_replace('([\\\\$](?=\\d))', '\\\\$0', $str);
 599                      }
 600                      else
 601                      {
 602                          $attrName = end($m);
 603   
 604                          // Generate a unique ID for this attribute name, we'll use it as a
 605                          // placeholder until we have the full list of captures and we can replace it
 606                          // with the capture number
 607                          if (!isset($usedAttributes[$attrName]))
 608                          {
 609                              $usedAttributes[$attrName] = uniqid($attrName, true);
 610                          }
 611   
 612                          $rendering .= $usedAttributes[$attrName];
 613                      }
 614   
 615                      // Skip the match plus the next . or ;
 616                      $offset += 1 + strlen($m[0]);
 617                  }
 618              }
 619              else
 620              {
 621                  $attrName = end($m);
 622   
 623                  if (!isset($copiedAttributes[$attrName]))
 624                  {
 625                      $copiedAttributes[$attrName] = uniqid($attrName, true);
 626                  }
 627   
 628                  $rendering .= $copiedAttributes[$attrName];
 629                  $offset += strlen($m[0]);
 630              }
 631          }
 632   
 633          // Gather the names of the attributes used in the replacement either by copy or by value
 634          $attrNames = array_keys($copiedAttributes + $usedAttributes);
 635   
 636          // Sort them alphabetically
 637          sort($attrNames);
 638   
 639          // Keep a copy of the attribute names to be used in the fillter subpattern
 640          $remainingAttributes = array_combine($attrNames, $attrNames);
 641   
 642          // Prepare the final regexp
 643          $regexp = '(^[^ ]+';
 644          $index  = 0;
 645          foreach ($attrNames as $attrName)
 646          {
 647              // Add a subpattern that matches (and skips) any attribute definition that is not one of
 648              // the remaining attributes we're trying to match
 649              $regexp .= '(?> (?!' . RegexpBuilder::fromList($remainingAttributes) . '=)[^=]+="[^"]*")*';
 650              unset($remainingAttributes[$attrName]);
 651   
 652              $regexp .= '(';
 653   
 654              if (isset($copiedAttributes[$attrName]))
 655              {
 656                  self::replacePlaceholder($rendering, $copiedAttributes[$attrName], ++$index);
 657              }
 658              else
 659              {
 660                  $regexp .= '?>';
 661              }
 662   
 663              $regexp .= ' ' . $attrName . '="';
 664   
 665              if (isset($usedAttributes[$attrName]))
 666              {
 667                  $regexp .= '(';
 668   
 669                  self::replacePlaceholder($rendering, $usedAttributes[$attrName], ++$index);
 670              }
 671   
 672              $regexp .= '[^"]*';
 673   
 674              if (isset($usedAttributes[$attrName]))
 675              {
 676                  $regexp .= ')';
 677              }
 678   
 679              $regexp .= '")?';
 680          }
 681   
 682          $regexp .= '.*)s';
 683   
 684          return [$regexp, $rendering];
 685      }
 686   
 687      /**
 688      * Get a string suitable as a str_replace() replacement for given PHP code
 689      *
 690      * @param  string      $php Original code
 691      * @return bool|string      Static replacement if possible, or FALSE otherwise
 692      */
 693      protected static function getStaticRendering($php)
 694      {
 695          if ($php === '')
 696          {
 697              return '';
 698          }
 699   
 700          $regexp = "(^\\\$this->out\.='((?>[^'\\\\]|\\\\['\\\\])*+)';\$)";
 701          if (preg_match($regexp, $php, $m))
 702          {
 703              return stripslashes($m[1]);
 704          }
 705   
 706          return false;
 707      }
 708   
 709      /**
 710      * Get string rendering strategies for given chunks
 711      *
 712      * @param  string $php
 713      * @return array
 714      */
 715      protected static function getStringRenderings($php)
 716      {
 717          $chunks = explode('$this->at($node);', $php);
 718          if (count($chunks) > 2)
 719          {
 720              // Can't use string replacements if there are more than one xsl:apply-templates
 721              return [];
 722          }
 723   
 724          $renderings = [];
 725          foreach ($chunks as $k => $chunk)
 726          {
 727              // Try a static replacement first
 728              $rendering = self::getStaticRendering($chunk);
 729              if ($rendering !== false)
 730              {
 731                  $renderings[$k] = ['static', $rendering];
 732              }
 733              elseif ($k === 0)
 734              {
 735                  // If this is the first chunk, we can try a dynamic replacement. This wouldn't work
 736                  // for the second chunk because we wouldn't have access to the attribute values
 737                  $rendering = self::getDynamicRendering($chunk);
 738                  if ($rendering !== false)
 739                  {
 740                      $renderings[$k] = ['dynamic', $rendering];
 741                  }
 742              }
 743          }
 744   
 745          return $renderings;
 746      }
 747   
 748      /**
 749      * Replace all instances of a uniqid with a PCRE replacement in a string
 750      *
 751      * @param  string  &$str    PCRE replacement
 752      * @param  string   $uniqid Unique ID
 753      * @param  integer  $index  Capture index
 754      * @return void
 755      */
 756      protected static function replacePlaceholder(&$str, $uniqid, $index)
 757      {
 758          $str = preg_replace_callback(
 759              '(' . preg_quote($uniqid) . '(.))',
 760              function ($m) use ($index)
 761              {
 762                  // Replace with $1 where unambiguous and ${1} otherwise
 763                  if (is_numeric($m[1]))
 764                  {
 765                      return '${' . $index . '}' . $m[1];
 766                  }
 767                  else
 768                  {
 769                      return '$' . $index . $m[1];
 770                  }
 771              },
 772              $str
 773          );
 774      }
 775  }