Verzeichnisstruktur phpBB-3.3.15


Veröffentlicht
28.08.2024

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: 02.04.2025, 15:04 - 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  }