[ Index ]

PHP Cross Reference of phpBB-3.3.14-deutsch

title

Body

[close]

/vendor/s9e/text-formatter/src/Configurator/RendererGenerators/PHP/ -> Quick.php (source)

   1  <?php
   2  
   3  /**
   4  * @package   s9e\TextFormatter
   5  * @copyright Copyright (c) 2010-2022 The s9e authors
   6  * @license   http://www.opensource.org/licenses/mit-license.php The MIT License
   7  */
   8  namespace s9e\TextFormatter\Configurator\RendererGenerators\PHP;
   9  
  10  use Closure;
  11  use RuntimeException;
  12  use s9e\TextFormatter\Configurator\Helpers\RegexpBuilder;
  13  
  14  class Quick
  15  {
  16      /**
  17      * Generate the Quick renderer's source
  18      *
  19      * @param  array  $compiledTemplates Array of tagName => compiled template
  20      * @return string
  21      */
  22  	public static function getSource(array $compiledTemplates)
  23      {
  24          $map         = ['dynamic' => [], 'php' => [], 'static' => []];
  25          $tagNames    = [];
  26          $unsupported = [];
  27  
  28          // Ignore system tags
  29          unset($compiledTemplates['br']);
  30          unset($compiledTemplates['e']);
  31          unset($compiledTemplates['i']);
  32          unset($compiledTemplates['p']);
  33          unset($compiledTemplates['s']);
  34  
  35          foreach ($compiledTemplates as $tagName => $php)
  36          {
  37              $renderings = self::getRenderingStrategy($php);
  38              if (empty($renderings))
  39              {
  40                  $unsupported[] = $tagName;
  41                  continue;
  42              }
  43  
  44              foreach ($renderings as $i => list($strategy, $replacement))
  45              {
  46                  $match = (($i) ? '/' : '') . $tagName;
  47                  $map[$strategy][$match] = $replacement;
  48              }
  49  
  50              // Record the names of tags whose template does not contain a passthrough
  51              if (!isset($renderings[1]))
  52              {
  53                  $tagNames[] = $tagName;
  54              }
  55          }
  56  
  57          $php = [];
  58          $php[] = '    /** {@inheritdoc} */';
  59          $php[] = '    public $enableQuickRenderer=true;';
  60          $php[] = '    /** {@inheritdoc} */';
  61          $php[] = '    protected $static=' . self::export($map['static']) . ';';
  62          $php[] = '    /** {@inheritdoc} */';
  63          $php[] = '    protected $dynamic=' . self::export($map['dynamic']) . ';';
  64  
  65          $quickSource = '';
  66          if (!empty($map['php']))
  67          {
  68              $quickSource = SwitchStatement::generate('$id', $map['php']);
  69          }
  70  
  71          // Build a regexp that matches all the tags
  72          $regexp  = '(<(?:(?!/)(';
  73          $regexp .= ($tagNames) ? RegexpBuilder::fromList($tagNames) : '(?!)';
  74          $regexp .= ')(?: [^>]*)?>.*?</\\1|(/?(?!br/|p>)[^ />]+)[^>]*?(/)?)>)s';
  75          $php[] = '    /** {@inheritdoc} */';
  76          $php[] = '    protected $quickRegexp=' . var_export($regexp, true) . ';';
  77  
  78          // Build a regexp that matches tags that cannot be rendered with the Quick renderer
  79          if (!empty($unsupported))
  80          {
  81              $regexp = '((?<=<)(?:[!?]|' . RegexpBuilder::fromList($unsupported) . '[ />]))';
  82              $php[]  = '    /** {@inheritdoc} */';
  83              $php[]  = '    protected $quickRenderingTest=' . var_export($regexp, true) . ';';
  84          }
  85  
  86          $php[] = '    /** {@inheritdoc} */';
  87          $php[] = '    protected function renderQuickTemplate($id, $xml)';
  88          $php[] = '    {';
  89          $php[] = '        $attributes=$this->matchAttributes($xml);';
  90          $php[] = "        \$html='';" . $quickSource;
  91          $php[] = '';
  92          $php[] = '        return $html;';
  93          $php[] = '    }';
  94  
  95          return implode("\n", $php);
  96      }
  97  
  98      /**
  99      * 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  }


Generated: Mon Nov 25 19:05:08 2024 Cross-referenced by PHPXref 0.7.1