Serializer::serializeApplyTemplates()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 10
ccs 6
cts 6
cp 1
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 2
1
<?php
2
3
/**
4
* @package   s9e\TextFormatter
5
* @copyright Copyright (c) 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 DOMElement;
11
use DOMXPath;
12
use RuntimeException;
13
use s9e\TextFormatter\Configurator\Helpers\AVTHelper;
14
use s9e\TextFormatter\Configurator\Helpers\TemplateParser;
15
16
class Serializer
17
{
18
	/**
19
	* @var XPathConvertor XPath-to-PHP convertor
20
	*/
21
	public $convertor;
22
23
	/**
24
	* @var array Value of the "void" attribute of all elements, using the element's "id" as key
25
	*/
26
	protected $isVoid;
27
28
	/**
29
	* @var DOMXPath
30
	*/
31
	protected $xpath;
32
33
	/**
34
	* Constructor
35
	*/
36 827
	public function __construct()
37
	{
38 827
		$this->convertor = new XPathConvertor;
39
	}
40
41
	/**
42
	* Convert an XPath expression (used in a condition) into PHP code
43
	*
44
	* This method is similar to convertXPath() but it selectively replaces some simple conditions
45
	* with the corresponding DOM method for performance reasons
46
	*
47
	* @param  string $expr XPath expression
48
	* @return string       PHP code
49
	*/
50 335
	public function convertCondition($expr)
51
	{
52 335
		return $this->convertor->convertCondition($expr);
53
	}
54
55
	/**
56
	* Convert an XPath expression (used as value) into PHP code
57
	*
58
	* @param  string $expr XPath expression
59
	* @return string       PHP code
60
	*/
61 637
	public function convertXPath($expr)
62
	{
63 637
		$php = $this->convertor->convertXPath($expr);
64 637
		if (is_numeric($php))
65
		{
66 7
			$php = "'" . $php . "'";
67
		}
68
69 637
		return $php;
70
	}
71
72
	/**
73
	* Serialize the internal representation of a template into PHP
74
	*
75
	* @param  DOMElement $ir Internal representation
76
	* @return string
77
	*/
78 822
	public function serialize(DOMElement $ir)
79
	{
80 822
		$this->xpath  = new DOMXPath($ir->ownerDocument);
81 822
		$this->isVoid = [];
82 822
		foreach ($this->xpath->query('//element') as $element)
83
		{
84 816
			$this->isVoid[$element->getAttribute('id')] = $element->getAttribute('void');
85
		}
86
87 822
		return $this->serializeChildren($ir);
88
	}
89
90
	/**
91
	* Convert an attribute value template into PHP
92
	*
93
	* NOTE: escaping must be performed by the caller
94
	*
95
	* @link https://www.w3.org/TR/1999/REC-xslt-19991116#dt-attribute-value-template
96
	*
97
	* @param  string $attrValue Attribute value template
98
	* @return string
99
	*/
100 13
	protected function convertAttributeValueTemplate($attrValue)
101
	{
102 13
		$phpExpressions = [];
103 13
		foreach (AVTHelper::parse($attrValue) as $token)
104
		{
105 13
			if ($token[0] === 'literal')
106
			{
107
				$phpExpressions[] = var_export($token[1], true);
108
			}
109
			else
110
			{
111 13
				$phpExpressions[] = $this->convertXPath($token[1]);
112
			}
113
		}
114
115 13
		return implode('.', $phpExpressions);
116
	}
117
118
	/**
119
	* Convert a dynamic xsl:attribute/xsl:element name into PHP
120
	*
121
	* @param  string $attrValue Attribute value template
122
	* @return string
123
	*/
124 816
	protected function convertDynamicNodeName(string $attrValue): string
125
	{
126 816
		if (strpos($attrValue, '{') === false)
127
		{
128 815
			return var_export(htmlspecialchars($attrValue, ENT_QUOTES), true);
129
		}
130
131 13
		return 'htmlspecialchars(' . $this->convertAttributeValueTemplate($attrValue) . ',' . ENT_QUOTES . ')';
132
	}
133
134
	/**
135
	* Escape given literal
136
	*
137
	* @param  string $text    Literal
138
	* @param  string $context Either "raw", "text" or "attribute"
139
	* @return string          Escaped literal
140
	*/
141 470
	protected function escapeLiteral($text, $context)
142
	{
143 470
		if ($context === 'raw')
144
		{
145 3
			return $text;
146
		}
147
148 467
		$escapeMode = ($context === 'attribute') ? ENT_COMPAT : ENT_NOQUOTES;
149
150 467
		return htmlspecialchars($text, $escapeMode);
151
	}
152
153
	/**
154
	* Escape the output of given PHP expression
155
	*
156
	* @param  string $php     PHP expression
157
	* @param  string $context Either "raw", "text" or "attribute"
158
	* @return string          PHP expression, including escaping mechanism
159
	*/
160 622
	protected function escapePHPOutput($php, $context)
161
	{
162 622
		if ($context === 'raw')
163
		{
164 3
			return $php;
165
		}
166
167 619
		$escapeMode = ($context === 'attribute') ? ENT_COMPAT : ENT_NOQUOTES;
168
169 619
		return 'htmlspecialchars(' . $php . ',' . $escapeMode . ')';
170
	}
171
172
	/**
173
	* Test whether given switch has more than one non-default case
174
	*
175
	* @param  DOMElement $switch <switch/> node
176
	* @return bool
177
	*/
178 33
	protected function hasMultipleCases(DOMElement $switch)
179
	{
180 33
		return $this->xpath->evaluate('count(case[@test]) > 1', $switch);
181
	}
182
183
	/**
184
	* Test whether given attribute declaration is a minimizable boolean attribute
185
	*
186
	* @param  DOMElement $attribute <attribute/> node
187
	* @param  string     $php       Attribute content, in PHP
188
	* @return boolean
189
	*/
190 491
	protected function isBooleanAttribute(DOMElement $attribute, string $php): bool
191
	{
192 491
		if ($attribute->getAttribute('boolean') !== 'yes')
193
		{
194 491
			return false;
195
		}
196
197 8
		return ($php === '' || $php === "\$this->out.='" . $attribute->getAttribute('name') . "';");
198
	}
199
200
	/**
201
	* Serialize an <applyTemplates/> node
202
	*
203
	* @param  DOMElement $applyTemplates <applyTemplates/> node
204
	* @return string
205
	*/
206 812
	protected function serializeApplyTemplates(DOMElement $applyTemplates)
207
	{
208 812
		$php = '$this->at($node';
209 812
		if ($applyTemplates->hasAttribute('select'))
210
		{
211 2
			$php .= ',' . var_export($applyTemplates->getAttribute('select'), true);
212
		}
213 812
		$php .= ');';
214
215 812
		return $php;
216
	}
217
218
	/**
219
	* Serialize an <attribute/> node
220
	*
221
	* @param  DOMElement $attribute <attribute/> node
222
	* @return string
223
	*/
224 491
	protected function serializeAttribute(DOMElement $attribute)
225
	{
226 491
		$attrName = $attribute->getAttribute('name');
227
228
		// PHP representation of this attribute's name
229 491
		$phpAttrName = $this->convertDynamicNodeName($attrName);
230
231 491
		$php     = "\$this->out.=' '." . $phpAttrName;
232 491
		$content = $this->serializeChildren($attribute);
233 491
		if (!$this->isBooleanAttribute($attribute, $content))
234
		{
235 491
			$php .= ".'=\"';" . $content . "\$this->out.='\"'";
236
		}
237 491
		$php .= ';';
238
239 491
		return $php;
240
	}
241
242
	/**
243
	* Serialize all the children of given node into PHP
244
	*
245
	* @param  DOMElement $ir Internal representation
246
	* @return string
247
	*/
248 822
	protected function serializeChildren(DOMElement $ir)
249
	{
250 822
		$php = '';
251 822
		foreach ($ir->childNodes as $node)
252
		{
253 822
			if ($node instanceof DOMElement)
254
			{
255 822
				$methodName = 'serialize' . ucfirst($node->localName);
256 822
				$php .= $this->$methodName($node);
257
			}
258
		}
259
260 821
		return $php;
261
	}
262
263
	/**
264
	* Serialize a <closeTag/> node
265
	*
266
	* @param  DOMElement $closeTag <closeTag/> node
267
	* @return string
268
	*/
269 816
	protected function serializeCloseTag(DOMElement $closeTag)
270
	{
271 816
		$php = "\$this->out.='>';";
272 816
		$id  = $closeTag->getAttribute('id');
273
274 816
		if ($closeTag->hasAttribute('set'))
275
		{
276 1
			$php .= '$t' . $id . '=1;';
277
		}
278
279 816
		if ($closeTag->hasAttribute('check'))
280
		{
281 1
			$php = 'if(!isset($t' . $id . ')){' . $php . '}';
282
		}
283
284 816
		if ($this->isVoid[$id] === 'maybe')
285
		{
286
			// Check at runtime whether this element is not void
287 12
			$php .= 'if(!$v' . $id . '){';
288
		}
289
290 816
		return $php;
291
	}
292
293
	/**
294
	* Serialize a <comment/> node
295
	*
296
	* @param  DOMElement $comment <comment/> node
297
	* @return string
298
	*/
299 17
	protected function serializeComment(DOMElement $comment)
300
	{
301
		return "\$this->out.='<!--';"
302 17
		     . $this->serializeChildren($comment)
303 17
		     . "\$this->out.='-->';";
304
	}
305
306
	/**
307
	* Serialize a <copyOfAttributes/> node
308
	*
309
	* @param  DOMElement $copyOfAttributes <copyOfAttributes/> node
310
	* @return string
311
	*/
312 2
	protected function serializeCopyOfAttributes(DOMElement $copyOfAttributes)
313
	{
314
		return 'foreach($node->attributes as $attribute)'
315
		     . '{'
316
		     . "\$this->out.=' ';"
317
		     . "\$this->out.=\$attribute->name;"
318
		     . "\$this->out.='=\"';"
319 2
		     . "\$this->out.=htmlspecialchars(\$attribute->value," . ENT_COMPAT . ");"
320 2
		     . "\$this->out.='\"';"
321 2
		     . '}';
322
	}
323
324
	/**
325
	* Serialize an <element/> node
326
	*
327
	* @param  DOMElement $element <element/> node
328
	* @return string
329
	*/
330 816
	protected function serializeElement(DOMElement $element)
331
	{
332 816
		$php     = '';
333 816
		$elName  = $element->getAttribute('name');
334 816
		$id      = $element->getAttribute('id');
335 816
		$isVoid  = $element->getAttribute('void');
336
337
		// Test whether this element name is dynamic
338 816
		$isDynamic = (bool) (strpos($elName, '{') !== false);
339
340
		// PHP representation of this element's name
341 816
		$phpElName = $this->convertDynamicNodeName($elName);
342
343
		// If the element name is dynamic, we cache its name for convenience and performance
344 816
		if ($isDynamic)
345
		{
346 13
			$varName = '$e' . $id;
347
348
			// Add the var declaration to the source
349 13
			$php .= $varName . '=' . $phpElName . ';';
350
351
			// Replace the element name with the var
352 13
			$phpElName = $varName;
353
		}
354
355
		// Test whether this element is void if we need this information
356 816
		if ($isVoid === 'maybe')
357
		{
358 12
			$php .= '$v' . $id . '=preg_match(' . var_export(TemplateParser::$voidRegexp, true) . ',' . $phpElName . ');';
359
		}
360
361
		// Open the start tag
362 816
		$php .= "\$this->out.='<'." . $phpElName . ';';
363
364
		// Serialize this element's content
365 816
		$php .= $this->serializeChildren($element);
366
367
		// Close that element unless we know it's void
368 816
		if ($isVoid !== 'yes')
369
		{
370 815
			$php .= "\$this->out.='</'." . $phpElName . ".'>';";
371
		}
372
373
		// If this element was maybe void, serializeCloseTag() has put its content within an if
374
		// block. We need to close that block
375 816
		if ($isVoid === 'maybe')
376
		{
377 12
			$php .= '}';
378
		}
379
380 816
		return $php;
381
	}
382
383
	/**
384
	* Serialize a <switch/> node that has a branch-key attribute
385
	*
386
	* @param  DOMElement $switch <switch/> node
387
	* @return string
388
	*/
389 33
	protected function serializeHash(DOMElement $switch)
390
	{
391 33
		$statements = [];
392 33
		foreach ($this->xpath->query('case[@branch-values]', $switch) as $case)
393
		{
394 32
			foreach (unserialize($case->getAttribute('branch-values')) as $value)
395
			{
396 32
				$statements[$value] = $this->serializeChildren($case);
397
			}
398
		}
399 33
		if (!isset($case))
400
		{
401 1
			throw new RuntimeException;
402
		}
403
404 32
		$defaultCase = $this->xpath->query('case[not(@branch-values)]', $switch)->item(0);
405 32
		$defaultCode = ($defaultCase instanceof DOMElement) ? $this->serializeChildren($defaultCase) : '';
406 32
		$expr        = $this->convertXPath($switch->getAttribute('branch-key'));
407
408 32
		return SwitchStatement::generate($expr, $statements, $defaultCode);
409
	}
410
411
	/**
412
	* Serialize an <output/> node
413
	*
414
	* @param  DOMElement $output <output/> node
415
	* @return string
416
	*/
417 707
	protected function serializeOutput(DOMElement $output)
418
	{
419 707
		$context = $output->getAttribute('escape');
420
421 707
		$php = '$this->out.=';
422 707
		if ($output->getAttribute('type') === 'xpath')
423
		{
424 622
			$php .= $this->escapePHPOutput($this->convertXPath($output->textContent), $context);
425
		}
426
		else
427
		{
428 470
			$php .= var_export($this->escapeLiteral($output->textContent, $context), true);
429
		}
430 707
		$php .= ';';
431
432 707
		return $php;
433
	}
434
435
	/**
436
	* Serialize a <switch/> node
437
	*
438
	* @param  DOMElement $switch <switch/> node
439
	* @return string
440
	*/
441 343
	protected function serializeSwitch(DOMElement $switch)
442
	{
443
		// Use a specialized branch table if the minimum number of branches is reached
444 343
		if ($switch->hasAttribute('branch-key') && $this->hasMultipleCases($switch))
445
		{
446 32
			return $this->serializeHash($switch);
447
		}
448
449 335
		$php   = '';
450 335
		$if    = 'if';
451 335
		foreach ($this->xpath->query('case', $switch) as $case)
452
		{
453 335
			if ($case->hasAttribute('test'))
454
			{
455 335
				$php .= $if . '(' . $this->convertCondition($case->getAttribute('test')) . ')';
456
			}
457
			else
458
			{
459 188
				$php .= 'else';
460
			}
461
462 335
			$php .= '{' . $this->serializeChildren($case) . '}';
463 335
			$if   = 'elseif';
464
		}
465
466 335
		return $php;
467
	}
468
}