Completed
Push — master ( d91fed...fd66aa )
by Josh
17:36
created

Quick::getStaticRendering()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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