[ Index ]

PHP Cross Reference of phpBB-3.3.2-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-2020 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 are not supported
 162          if (preg_match('(\\$this->at\\((?!\\$node\\);))', $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                      return preg_replace('(' . $getAttribute . ')', '$attributes[$1]', $m[1]);
 414                  },
 415  
 416              // Character replacement can be performed directly on the escaped value provided that it
 417              // is then escaped as ENT_COMPAT and that replacements do not interfere with the escaping
 418              // of the characters &<>" or their representation &amp;&lt;&gt;&quot;
 419              '(htmlspecialchars\\(strtr\\(' . $getAttribute . ",('[^\"&\\\\';<>aglmopqtu]+'),('[^\"&\\\\'<>]+')\\)," . ENT_COMPAT . '\\))'
 420                  => 'strtr($attributes[$1],$2,$3)',
 421  
 422              // A comparison between two attributes. No need to unescape
 423              '(' . $getAttribute . '(!?=+)' . $getAttribute . ')'
 424                  => '$attributes[$1]$2$attributes[$3]',
 425  
 426              // A comparison between an attribute and a literal string. Rather than unescape the
 427              // attribute value, we escape the literal. This applies to comparisons using XPath's
 428              // contains() as well (translated to PHP's strpos())
 429              '(' . $getAttribute . '===(' . $string . '))s'
 430                  => function ($m)
 431                  {
 432                      return '$attributes[' . $m[1] . ']===' . htmlspecialchars($m[2], ENT_COMPAT);
 433                  },
 434  
 435              '((' . $string . ')===' . $getAttribute . ')s'
 436                  => function ($m)
 437                  {
 438                      return htmlspecialchars($m[1], ENT_COMPAT) . '===$attributes[' . $m[2] . ']';
 439                  },
 440  
 441              '(strpos\\(' . $getAttribute . ',(' . $string . ')\\)([!=]==(?:0|false)))s'
 442                  => function ($m)
 443                  {
 444                      return 'strpos($attributes[' . $m[1] . "]," . htmlspecialchars($m[2], ENT_COMPAT) . ')' . $m[3];
 445                  },
 446  
 447              '(strpos\\((' . $string . '),' . $getAttribute . '\\)([!=]==(?:0|false)))s'
 448                  => function ($m)
 449                  {
 450                      return 'strpos(' . htmlspecialchars($m[1], ENT_COMPAT) . ',$attributes[' . $m[2] . '])' . $m[3];
 451                  },
 452  
 453              // An attribute value used in an arithmetic comparison or operation does not need to be
 454              // unescaped. The same applies to empty(), isset() and conditionals
 455              '(' . $getAttribute . '(?=(?:==|[-+*])\\d+))'  => '$attributes[$1]',
 456              '(\\b(\\d+(?:==|[-+*]))' . $getAttribute . ')' => '$1$attributes[$2]',
 457              '(empty\\(' . $getAttribute . '\\))'           => 'empty($attributes[$1])',
 458              "(\\\$node->hasAttribute\\(('[^']+')\\))"      => 'isset($attributes[$1])',
 459              'if($node->attributes->length)'                => 'if($this->hasNonNullValues($attributes))',
 460  
 461              // In all other situations, unescape the attribute value before use
 462              '(' . $getAttribute . ')' => 'htmlspecialchars_decode($attributes[$1])'
 463          ];
 464  
 465          foreach ($replacements as $match => $replace)
 466          {
 467              if ($replace instanceof Closure)
 468              {
 469                  $php = preg_replace_callback($match, $replace, $php);
 470              }
 471              elseif ($match[0] === '(')
 472              {
 473                  $php = preg_replace($match, $replace, $php);
 474              }
 475              else
 476              {
 477                  $php = str_replace($match, $replace, $php);
 478              }
 479          }
 480      }
 481  
 482      /**
 483      * Build the source for the two sides of a templates based on the structure extracted from its
 484      * original source
 485      *
 486      * @param  array    $branches
 487      * @return string[]
 488      */
 489  	protected static function buildPHP(array $branches)
 490      {
 491          $return = ['', ''];
 492          foreach ($branches as $branch)
 493          {
 494              $return[0] .= $branch['statement'] . '{' . $branch['head'];
 495              $return[1] .= $branch['statement'] . '{';
 496  
 497              if ($branch['branches'])
 498              {
 499                  list($head, $tail) = self::buildPHP($branch['branches']);
 500  
 501                  $return[0] .= $head;
 502                  $return[1] .= $tail;
 503              }
 504  
 505              $return[0] .= '}';
 506              $return[1] .= $branch['tail'] . '}';
 507          }
 508  
 509          return $return;
 510      }
 511  
 512      /**
 513      * Get the unique values for the "passthrough" key of given branches
 514      *
 515      * @param  array     $branches
 516      * @return integer[]
 517      */
 518  	protected static function getBranchesPassthrough(array $branches)
 519      {
 520          $values = [];
 521          foreach ($branches as $branch)
 522          {
 523              $values[] = $branch['passthrough'];
 524          }
 525  
 526          // If the last branch isn't an "else", we act as if there was an additional branch with no
 527          // passthrough
 528          if ($branch['statement'] !== 'else')
 529          {
 530              $values[] = 0;
 531          }
 532  
 533          return array_unique($values);
 534      }
 535  
 536      /**
 537      * Get a string suitable as a preg_replace() replacement for given PHP code
 538      *
 539      * @param  string     $php Original code
 540      * @return array|bool      Array of [regexp, replacement] if possible, or FALSE otherwise
 541      */
 542  	protected static function getDynamicRendering($php)
 543      {
 544          $rendering = '';
 545  
 546          $literal   = "(?<literal>'((?>[^'\\\\]+|\\\\['\\\\])*)')";
 547          $attribute = "(?<attribute>htmlspecialchars\\(\\\$node->getAttribute\\('([^']+)'\\),2\\))";
 548          $value     = "(?<value>$literal|$attribute)";
 549          $output    = "(?<output>\\\$this->out\\.=$value(?:\\.(?&value))*;)";
 550  
 551          $copyOfAttribute = "(?<copyOfAttribute>if\\(\\\$node->hasAttribute\\('([^']+)'\\)\\)\\{\\\$this->out\\.=' \\g-1=\"'\\.htmlspecialchars\\(\\\$node->getAttribute\\('\\g-1'\\),2\\)\\.'\"';\\})";
 552  
 553          $regexp = '(^(' . $output . '|' . $copyOfAttribute . ')*$)';
 554          if (!preg_match($regexp, $php, $m))
 555          {
 556              return false;
 557          }
 558  
 559          // Attributes that are copied in the replacement
 560          $copiedAttributes = [];
 561  
 562          // Attributes whose value is used in the replacement
 563          $usedAttributes = [];
 564  
 565          $regexp = '(' . $output . '|' . $copyOfAttribute . ')A';
 566          $offset = 0;
 567          while (preg_match($regexp, $php, $m, 0, $offset))
 568          {
 569              // Test whether it's normal output or a copy of attribute
 570              if ($m['output'])
 571              {
 572                  // 12 === strlen('$this->out.=')
 573                  $offset += 12;
 574  
 575                  while (preg_match('(' . $value . ')A', $php, $m, 0, $offset))
 576                  {
 577                      // Test whether it's a literal or an attribute value
 578                      if ($m['literal'])
 579                      {
 580                          // Unescape the literal
 581                          $str = stripslashes(substr($m[0], 1, -1));
 582  
 583                          // Escape special characters
 584                          $rendering .= preg_replace('([\\\\$](?=\\d))', '\\\\$0', $str);
 585                      }
 586                      else
 587                      {
 588                          $attrName = end($m);
 589  
 590                          // Generate a unique ID for this attribute name, we'll use it as a
 591                          // placeholder until we have the full list of captures and we can replace it
 592                          // with the capture number
 593                          if (!isset($usedAttributes[$attrName]))
 594                          {
 595                              $usedAttributes[$attrName] = uniqid($attrName, true);
 596                          }
 597  
 598                          $rendering .= $usedAttributes[$attrName];
 599                      }
 600  
 601                      // Skip the match plus the next . or ;
 602                      $offset += 1 + strlen($m[0]);
 603                  }
 604              }
 605              else
 606              {
 607                  $attrName = end($m);
 608  
 609                  if (!isset($copiedAttributes[$attrName]))
 610                  {
 611                      $copiedAttributes[$attrName] = uniqid($attrName, true);
 612                  }
 613  
 614                  $rendering .= $copiedAttributes[$attrName];
 615                  $offset += strlen($m[0]);
 616              }
 617          }
 618  
 619          // Gather the names of the attributes used in the replacement either by copy or by value
 620          $attrNames = array_keys($copiedAttributes + $usedAttributes);
 621  
 622          // Sort them alphabetically
 623          sort($attrNames);
 624  
 625          // Keep a copy of the attribute names to be used in the fillter subpattern
 626          $remainingAttributes = array_combine($attrNames, $attrNames);
 627  
 628          // Prepare the final regexp
 629          $regexp = '(^[^ ]+';
 630          $index  = 0;
 631          foreach ($attrNames as $attrName)
 632          {
 633              // Add a subpattern that matches (and skips) any attribute definition that is not one of
 634              // the remaining attributes we're trying to match
 635              $regexp .= '(?> (?!' . RegexpBuilder::fromList($remainingAttributes) . '=)[^=]+="[^"]*")*';
 636              unset($remainingAttributes[$attrName]);
 637  
 638              $regexp .= '(';
 639  
 640              if (isset($copiedAttributes[$attrName]))
 641              {
 642                  self::replacePlaceholder($rendering, $copiedAttributes[$attrName], ++$index);
 643              }
 644              else
 645              {
 646                  $regexp .= '?>';
 647              }
 648  
 649              $regexp .= ' ' . $attrName . '="';
 650  
 651              if (isset($usedAttributes[$attrName]))
 652              {
 653                  $regexp .= '(';
 654  
 655                  self::replacePlaceholder($rendering, $usedAttributes[$attrName], ++$index);
 656              }
 657  
 658              $regexp .= '[^"]*';
 659  
 660              if (isset($usedAttributes[$attrName]))
 661              {
 662                  $regexp .= ')';
 663              }
 664  
 665              $regexp .= '")?';
 666          }
 667  
 668          $regexp .= '.*)s';
 669  
 670          return [$regexp, $rendering];
 671      }
 672  
 673      /**
 674      * Get a string suitable as a str_replace() replacement for given PHP code
 675      *
 676      * @param  string      $php Original code
 677      * @return bool|string      Static replacement if possible, or FALSE otherwise
 678      */
 679  	protected static function getStaticRendering($php)
 680      {
 681          if ($php === '')
 682          {
 683              return '';
 684          }
 685  
 686          $regexp = "(^\\\$this->out\.='((?>[^'\\\\]|\\\\['\\\\])*+)';\$)";
 687          if (preg_match($regexp, $php, $m))
 688          {
 689              return stripslashes($m[1]);
 690          }
 691  
 692          return false;
 693      }
 694  
 695      /**
 696      * Get string rendering strategies for given chunks
 697      *
 698      * @param  string $php
 699      * @return array
 700      */
 701  	protected static function getStringRenderings($php)
 702      {
 703          $chunks = explode('$this->at($node);', $php);
 704          if (count($chunks) > 2)
 705          {
 706              // Can't use string replacements if there are more than one xsl:apply-templates
 707              return [];
 708          }
 709  
 710          $renderings = [];
 711          foreach ($chunks as $k => $chunk)
 712          {
 713              // Try a static replacement first
 714              $rendering = self::getStaticRendering($chunk);
 715              if ($rendering !== false)
 716              {
 717                  $renderings[$k] = ['static', $rendering];
 718              }
 719              elseif ($k === 0)
 720              {
 721                  // If this is the first chunk, we can try a dynamic replacement. This wouldn't work
 722                  // for the second chunk because we wouldn't have access to the attribute values
 723                  $rendering = self::getDynamicRendering($chunk);
 724                  if ($rendering !== false)
 725                  {
 726                      $renderings[$k] = ['dynamic', $rendering];
 727                  }
 728              }
 729          }
 730  
 731          return $renderings;
 732      }
 733  
 734      /**
 735      * Replace all instances of a uniqid with a PCRE replacement in a string
 736      *
 737      * @param  string  &$str    PCRE replacement
 738      * @param  string   $uniqid Unique ID
 739      * @param  integer  $index  Capture index
 740      * @return void
 741      */
 742  	protected static function replacePlaceholder(&$str, $uniqid, $index)
 743      {
 744          $str = preg_replace_callback(
 745              '(' . preg_quote($uniqid) . '(.))',
 746              function ($m) use ($index)
 747              {
 748                  // Replace with $1 where unambiguous and ${1} otherwise
 749                  if (is_numeric($m[1]))
 750                  {
 751                      return '${' . $index . '}' . $m[1];
 752                  }
 753                  else
 754                  {
 755                      return '$' . $index . $m[1];
 756                  }
 757              },
 758              $str
 759          );
 760      }
 761  }


Generated: Wed Nov 11 20:28:18 2020 Cross-referenced by PHPXref 0.7.1