Passed
Push — master ( f6a3e3...06d5e4 )
by Josh
34:56
created

Serializer::convertDynamicNodeName()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 8
ccs 3
cts 3
cp 1
rs 10
cc 2
nc 2
nop 1
crap 2
1
<?php
2
3
/**
4
* @package   s9e\TextFormatter
5
* @copyright Copyright (c) 2010-2021 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 820
	public function __construct()
37
	{
38 820
		$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 331
	public function convertCondition($expr)
51
	{
52 331
		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 632
	public function convertXPath($expr)
62
	{
63 632
		$php = $this->convertor->convertXPath($expr);
64 632
		if (is_numeric($php))
65
		{
66 7
			$php = "'" . $php . "'";
67
		}
68
69 632
		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 815
	public function serialize(DOMElement $ir)
79
	{
80 815
		$this->xpath  = new DOMXPath($ir->ownerDocument);
1 ignored issue
show
Bug introduced by
It seems like $ir->ownerDocument can also be of type null; however, parameter $document of DOMXPath::__construct() does only seem to accept DOMDocument, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

80
		$this->xpath  = new DOMXPath(/** @scrutinizer ignore-type */ $ir->ownerDocument);
Loading history...
81 815
		$this->isVoid = [];
82 815
		foreach ($this->xpath->query('//element') as $element)
83
		{
84 809
			$this->isVoid[$element->getAttribute('id')] = $element->getAttribute('void');
85
		}
86
87 815
		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 809
	protected function convertAttributeValueTemplate($attrValue)
101
	{
102 809
		$phpExpressions = [];
103 809
		foreach (AVTHelper::parse($attrValue) as $token)
104
		{
105 809
			if ($token[0] === 'literal')
106
			{
107 809
				$phpExpressions[] = var_export($token[1], true);
108
			}
109
			else
110
			{
111 12
				$phpExpressions[] = $this->convertXPath($token[1]);
112
			}
113
		}
114
115 809
		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
	protected function convertDynamicNodeName(string $attrValue): string
125 466
	{
126
		if (strpos($attrValue, '{') === false)
127 466
		{
128
			return var_export(htmlspecialchars($attrValue, ENT_QUOTES), true);
129 3
		}
130
131
		return 'htmlspecialchars(' . $this->convertAttributeValueTemplate($attrValue) . ',' . ENT_QUOTES . ')';
132 463
	}
133
134 463
	/**
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
	protected function escapeLiteral($text, $context)
142
	{
143
		if ($context === 'raw')
144 618
		{
145
			return $text;
146 618
		}
147
148 3
		$escapeMode = ($context === 'attribute') ? ENT_COMPAT : ENT_NOQUOTES;
149
150
		return htmlspecialchars($text, $escapeMode);
151 615
	}
152
153 615
	/**
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
	protected function escapePHPOutput($php, $context)
161
	{
162 33
		if ($context === 'raw')
163
		{
164 33
			return $php;
165
		}
166
167
		$escapeMode = ($context === 'attribute') ? ENT_COMPAT : ENT_NOQUOTES;
168
169
		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 486
	* @return bool
177
	*/
178 486
	protected function hasMultipleCases(DOMElement $switch)
179 486
	{
180
		return $this->xpath->evaluate('count(case[@test]) > 1', $switch);
181 486
	}
182
183
	/**
184 8
	* Test whether given attribute declaration is a minimizable boolean attribute
185
	*
186
	* The test is case-sensitive and only covers attribute that are minimized by libxslt
187
	*
188
	* @param  string $attrName Attribute name
189
	* @param  string $php      Attribute content, in PHP
190
	* @return boolean
191
	*/
192
	protected function isBooleanAttribute(string $attrName, string $php): bool
193 808
	{
194
		$attrNames = ['checked', 'compact', 'declare', 'defer', 'disabled', 'ismap', 'multiple', 'nohref', 'noresize', 'noshade', 'nowrap', 'readonly', 'selected'];
195 808
		if (!in_array($attrName, $attrNames, true))
196 808
		{
197
			return false;
198 2
		}
199
200 808
		return ($php === '' || $php === "\$this->out.='" . $attrName . "';");
201
	}
202 808
203
	/**
204
	* Serialize an <applyTemplates/> node
205
	*
206
	* @param  DOMElement $applyTemplates <applyTemplates/> node
207
	* @return string
208
	*/
209
	protected function serializeApplyTemplates(DOMElement $applyTemplates)
210
	{
211 486
		$php = '$this->at($node';
212
		if ($applyTemplates->hasAttribute('select'))
213 486
		{
214
			$php .= ',' . var_export($applyTemplates->getAttribute('select'), true);
215
		}
216 486
		$php .= ');';
217
218
		return $php;
219 486
	}
220
221 486
	/**
222 486
	* Serialize an <attribute/> node
223 486
	*
224
	* @param  DOMElement $attribute <attribute/> node
225 486
	* @return string
226
	*/
227 486
	protected function serializeAttribute(DOMElement $attribute)
228
	{
229 486
		$attrName = $attribute->getAttribute('name');
230
231
		// PHP representation of this attribute's name
232
		$phpAttrName = $this->convertDynamicNodeName($attrName);
233
234
		$php     = "\$this->out.=' '." . $phpAttrName;
235
		$content = $this->serializeChildren($attribute);
236
		if (!$this->isBooleanAttribute($attrName, $content))
237
		{
238 815
			$php .= ".'=\"';" . $content . "\$this->out.='\"'";
239
		}
240 815
		$php .= ';';
241 815
242
		return $php;
243 815
	}
244
245 815
	/**
246 815
	* Serialize all the children of given node into PHP
247
	*
248
	* @param  DOMElement $ir Internal representation
249
	* @return string
250 814
	*/
251
	protected function serializeChildren(DOMElement $ir)
252
	{
253
		$php = '';
254
		foreach ($ir->childNodes as $node)
255
		{
256
			if ($node instanceof DOMElement)
257
			{
258
				$methodName = 'serialize' . ucfirst($node->localName);
259 809
				$php .= $this->$methodName($node);
260
			}
261 809
		}
262 809
263
		return $php;
264 809
	}
265
266 1
	/**
267
	* Serialize a <closeTag/> node
268
	*
269 809
	* @param  DOMElement $closeTag <closeTag/> node
270
	* @return string
271 1
	*/
272
	protected function serializeCloseTag(DOMElement $closeTag)
273
	{
274 809
		$php = "\$this->out.='>';";
275
		$id  = $closeTag->getAttribute('id');
276
277 12
		if ($closeTag->hasAttribute('set'))
278
		{
279
			$php .= '$t' . $id . '=1;';
280 809
		}
281
282
		if ($closeTag->hasAttribute('check'))
283
		{
284
			$php = 'if(!isset($t' . $id . ')){' . $php . '}';
285
		}
286
287
		if ($this->isVoid[$id] === 'maybe')
288
		{
289 17
			// Check at runtime whether this element is not void
290
			$php .= 'if(!$v' . $id . '){';
291
		}
292 17
293 17
		return $php;
294
	}
295
296
	/**
297
	* Serialize a <comment/> node
298
	*
299
	* @param  DOMElement $comment <comment/> node
300
	* @return string
301
	*/
302 2
	protected function serializeComment(DOMElement $comment)
303
	{
304
		return "\$this->out.='<!--';"
305
		     . $this->serializeChildren($comment)
306
		     . "\$this->out.='-->';";
307
	}
308
309 2
	/**
310 2
	* Serialize a <copyOfAttributes/> node
311 2
	*
312
	* @param  DOMElement $copyOfAttributes <copyOfAttributes/> node
313
	* @return string
314
	*/
315
	protected function serializeCopyOfAttributes(DOMElement $copyOfAttributes)
316
	{
317
		return 'foreach($node->attributes as $attribute)'
318
		     . '{'
319
		     . "\$this->out.=' ';"
320 809
		     . "\$this->out.=\$attribute->name;"
321
		     . "\$this->out.='=\"';"
322 809
		     . "\$this->out.=htmlspecialchars(\$attribute->value," . ENT_COMPAT . ");"
323 809
		     . "\$this->out.='\"';"
324 809
		     . '}';
325 809
	}
326
327
	/**
328 809
	* Serialize an <element/> node
329
	*
330
	* @param  DOMElement $element <element/> node
331 809
	* @return string
332
	*/
333
	protected function serializeElement(DOMElement $element)
334 809
	{
335
		$php     = '';
336
		$elName  = $element->getAttribute('name');
337 809
		$id      = $element->getAttribute('id');
338
		$isVoid  = $element->getAttribute('void');
339 12
340
		// Test whether this element name is dynamic
341
		$isDynamic = (bool) (strpos($elName, '{') !== false);
342 12
343
		// PHP representation of this element's name
344
		$phpElName = $this->convertDynamicNodeName($elName);
345 12
346
		// If the element name is dynamic, we cache its name for convenience and performance
347
		if ($isDynamic)
348
		{
349 809
			$varName = '$e' . $id;
350
351 12
			// Add the var declaration to the source
352
			$php .= $varName . '=' . $phpElName . ';';
353
354
			// Replace the element name with the var
355 809
			$phpElName = $varName;
356
		}
357
358 809
		// Test whether this element is void if we need this information
359
		if ($isVoid === 'maybe')
360
		{
361 809
			$php .= '$v' . $id . '=preg_match(' . var_export(TemplateParser::$voidRegexp, true) . ',' . $phpElName . ');';
362
		}
363 808
364
		// Open the start tag
365
		$php .= "\$this->out.='<'." . $phpElName . ';';
366
367
		// Serialize this element's content
368 809
		$php .= $this->serializeChildren($element);
369
370 12
		// Close that element unless we know it's void
371
		if ($isVoid !== 'yes')
372
		{
373 809
			$php .= "\$this->out.='</'." . $phpElName . ".'>';";
374
		}
375
376
		// If this element was maybe void, serializeCloseTag() has put its content within an if
377
		// block. We need to close that block
378
		if ($isVoid === 'maybe')
379
		{
380
			$php .= '}';
381
		}
382 33
383
		return $php;
384 33
	}
385 33
386
	/**
387 32
	* Serialize a <switch/> node that has a branch-key attribute
388
	*
389 32
	* @param  DOMElement $switch <switch/> node
390
	* @return string
391
	*/
392 33
	protected function serializeHash(DOMElement $switch)
393
	{
394 1
		$statements = [];
395
		foreach ($this->xpath->query('case[@branch-values]', $switch) as $case)
396
		{
397 32
			foreach (unserialize($case->getAttribute('branch-values')) as $value)
398 32
			{
399 32
				$statements[$value] = $this->serializeChildren($case);
400
			}
401 32
		}
402
		if (!isset($case))
403
		{
404
			throw new RuntimeException;
405
		}
406
407
		$defaultCase = $this->xpath->query('case[not(@branch-values)]', $switch)->item(0);
408
		$defaultCode = ($defaultCase instanceof DOMElement) ? $this->serializeChildren($defaultCase) : '';
409
		$expr        = $this->convertXPath($switch->getAttribute('branch-key'));
410 703
411
		return SwitchStatement::generate($expr, $statements, $defaultCode);
412 703
	}
413
414 703
	/**
415 703
	* Serialize an <output/> node
416
	*
417 618
	* @param  DOMElement $output <output/> node
418
	* @return string
419
	*/
420
	protected function serializeOutput(DOMElement $output)
421 466
	{
422
		$context = $output->getAttribute('escape');
423 703
424
		$php = '$this->out.=';
425 703
		if ($output->getAttribute('type') === 'xpath')
426
		{
427
			$php .= $this->escapePHPOutput($this->convertXPath($output->textContent), $context);
428
		}
429
		else
430
		{
431
			$php .= var_export($this->escapeLiteral($output->textContent, $context), true);
432
		}
433
		$php .= ';';
434 339
435
		return $php;
436
	}
437 339
438
	/**
439 32
	* Serialize a <switch/> node
440
	*
441
	* @param  DOMElement $switch <switch/> node
442 331
	* @return string
443 331
	*/
444 331
	protected function serializeSwitch(DOMElement $switch)
445
	{
446 331
		// Use a specialized branch table if the minimum number of branches is reached
447
		if ($switch->hasAttribute('branch-key') && $this->hasMultipleCases($switch))
448 331
		{
449
			return $this->serializeHash($switch);
450
		}
451
452 188
		$php   = '';
453
		$if    = 'if';
454
		foreach ($this->xpath->query('case', $switch) as $case)
455 331
		{
456 331
			if ($case->hasAttribute('test'))
457
			{
458
				$php .= $if . '(' . $this->convertCondition($case->getAttribute('test')) . ')';
459 331
			}
460
			else
461
			{
462
				$php .= 'else';
463
			}
464
465
			$php .= '{' . $this->serializeChildren($case) . '}';
466
			$if   = 'elseif';
467
		}
468
469
		return $php;
470
	}
471
}