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. |
|
(Beispiel Datei-Icons)
|
Auf das Icon klicken um den Quellcode anzuzeigen |
Quick.php
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('"','\"',\$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 &<>"
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 }