Completed
Push — master ( 6d8d0e...baba58 )
by Josh
04:02
created

Serializer::convertXPath()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 9
ccs 5
cts 5
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) 2010-2019 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 785
	public function __construct()
37
	{
38 785
		$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 313
	public function convertCondition($expr)
51
	{
52 313
		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 602
	public function convertXPath($expr)
62
	{
63 602
		$php = $this->convertor->convertXPath($expr);
64 602
		if (is_numeric($php))
65
		{
66 7
			$php = "'" . $php . "'";
67
		}
68
69 602
		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 780
	public function serialize(DOMElement $ir)
79
	{
80 780
		$this->xpath  = new DOMXPath($ir->ownerDocument);
81 780
		$this->isVoid = [];
82 780
		foreach ($this->xpath->query('//element') as $element)
83
		{
84 774
			$this->isVoid[$element->getAttribute('id')] = $element->getAttribute('void');
85
		}
86
87 780
		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 774
	protected function convertAttributeValueTemplate($attrValue)
101
	{
102 774
		$phpExpressions = [];
103 774
		foreach (AVTHelper::parse($attrValue) as $token)
104
		{
105 774
			if ($token[0] === 'literal')
106
			{
107 774
				$phpExpressions[] = var_export($token[1], true);
108
			}
109
			else
110
			{
111 12
				$phpExpressions[] = $this->convertXPath($token[1]);
112
			}
113
		}
114
115 774
		return implode('.', $phpExpressions);
116
	}
117
118
	/**
119
	* Escape given literal
120
	*
121
	* @param  string $text    Literal
122
	* @param  string $context Either "raw", "text" or "attribute"
123
	* @return string          Escaped literal
124
	*/
125 434
	protected function escapeLiteral($text, $context)
126
	{
127 434
		if ($context === 'raw')
128
		{
129 3
			return $text;
130
		}
131
132 431
		$escapeMode = ($context === 'attribute') ? ENT_COMPAT : ENT_NOQUOTES;
133
134 431
		return htmlspecialchars($text, $escapeMode);
135
	}
136
137
	/**
138
	* Escape the output of given PHP expression
139
	*
140
	* @param  string $php     PHP expression
141
	* @param  string $context Either "raw", "text" or "attribute"
142
	* @return string          PHP expression, including escaping mechanism
143
	*/
144 588
	protected function escapePHPOutput($php, $context)
145
	{
146 588
		if ($context === 'raw')
147
		{
148 3
			return $php;
149
		}
150
151 585
		$escapeMode = ($context === 'attribute') ? ENT_COMPAT : ENT_NOQUOTES;
152
153 585
		return 'htmlspecialchars(' . $php . ',' . $escapeMode . ')';
154
	}
155
156
	/**
157
	* Test whether given switch has more than one non-default case
158
	*
159
	* @param  DOMElement $switch <switch/> node
160
	* @return bool
161
	*/
162 33
	protected function hasMultipleCases(DOMElement $switch)
163
	{
164 33
		return $this->xpath->evaluate('count(case[@test]) > 1', $switch);
165
	}
166
167
	/**
168
	* Serialize an <applyTemplates/> node
169
	*
170
	* @param  DOMElement $applyTemplates <applyTemplates/> node
171
	* @return string
172
	*/
173 773
	protected function serializeApplyTemplates(DOMElement $applyTemplates)
174
	{
175 773
		$php = '$this->at($node';
176 773
		if ($applyTemplates->hasAttribute('select'))
177
		{
178 2
			$php .= ',' . var_export($applyTemplates->getAttribute('select'), true);
179
		}
180 773
		$php .= ');';
181
182 773
		return $php;
183
	}
184
185
	/**
186
	* Serialize an <attribute/> node
187
	*
188
	* @param  DOMElement $attribute <attribute/> node
189
	* @return string
190
	*/
191 452
	protected function serializeAttribute(DOMElement $attribute)
192
	{
193 452
		$attrName = $attribute->getAttribute('name');
194
195
		// PHP representation of this attribute's name
196 452
		$phpAttrName = $this->convertAttributeValueTemplate($attrName);
197
198
		// NOTE: the attribute name is escaped by default to account for dynamically-generated names
199 452
		$phpAttrName = 'htmlspecialchars(' . $phpAttrName . ',' . ENT_QUOTES . ')';
200
201 452
		return "\$this->out.=' '." . $phpAttrName . ".'=\"';"
202 452
		     . $this->serializeChildren($attribute)
203 452
		     . "\$this->out.='\"';";
204
	}
205
206
	/**
207
	* Serialize all the children of given node into PHP
208
	*
209
	* @param  DOMElement $ir Internal representation
210
	* @return string
211
	*/
212 780
	protected function serializeChildren(DOMElement $ir)
213
	{
214 780
		$php = '';
215 780
		foreach ($ir->childNodes as $node)
216
		{
217 780
			if ($node instanceof DOMElement)
218
			{
219 780
				$methodName = 'serialize' . ucfirst($node->localName);
220 780
				$php .= $this->$methodName($node);
221
			}
222
		}
223
224 779
		return $php;
225
	}
226
227
	/**
228
	* Serialize a <closeTag/> node
229
	*
230
	* @param  DOMElement $closeTag <closeTag/> node
231
	* @return string
232
	*/
233 774
	protected function serializeCloseTag(DOMElement $closeTag)
234
	{
235 774
		$php = "\$this->out.='>';";
236 774
		$id  = $closeTag->getAttribute('id');
237
238 774
		if ($closeTag->hasAttribute('set'))
239
		{
240 1
			$php .= '$t' . $id . '=1;';
241
		}
242
243 774
		if ($closeTag->hasAttribute('check'))
244
		{
245 1
			$php = 'if(!isset($t' . $id . ')){' . $php . '}';
246
		}
247
248 774
		if ($this->isVoid[$id] === 'maybe')
249
		{
250
			// Check at runtime whether this element is not void
251 12
			$php .= 'if(!$v' . $id . '){';
252
		}
253
254 774
		return $php;
255
	}
256
257
	/**
258
	* Serialize a <comment/> node
259
	*
260
	* @param  DOMElement $comment <comment/> node
261
	* @return string
262
	*/
263 17
	protected function serializeComment(DOMElement $comment)
264
	{
265
		return "\$this->out.='<!--';"
266 17
		     . $this->serializeChildren($comment)
267 17
		     . "\$this->out.='-->';";
268
	}
269
270
	/**
271
	* Serialize a <copyOfAttributes/> node
272
	*
273
	* @param  DOMElement $copyOfAttributes <copyOfAttributes/> node
274
	* @return string
275
	*/
276 2
	protected function serializeCopyOfAttributes(DOMElement $copyOfAttributes)
277
	{
278
		return 'foreach($node->attributes as $attribute)'
279
		     . '{'
280
		     . "\$this->out.=' ';"
281
		     . "\$this->out.=\$attribute->name;"
282
		     . "\$this->out.='=\"';"
283 2
		     . "\$this->out.=htmlspecialchars(\$attribute->value," . ENT_COMPAT . ");"
284 2
		     . "\$this->out.='\"';"
285 2
		     . '}';
286
	}
287
288
	/**
289
	* Serialize an <element/> node
290
	*
291
	* @param  DOMElement $element <element/> node
292
	* @return string
293
	*/
294 774
	protected function serializeElement(DOMElement $element)
295
	{
296 774
		$php     = '';
297 774
		$elName  = $element->getAttribute('name');
298 774
		$id      = $element->getAttribute('id');
299 774
		$isVoid  = $element->getAttribute('void');
300
301
		// Test whether this element name is dynamic
302 774
		$isDynamic = (bool) (strpos($elName, '{') !== false);
303
304
		// PHP representation of this element's name
305 774
		$phpElName = $this->convertAttributeValueTemplate($elName);
306
307
		// NOTE: the element name is escaped by default to account for dynamically-generated names
308 774
		$phpElName = 'htmlspecialchars(' . $phpElName . ',' . ENT_QUOTES . ')';
309
310
		// If the element name is dynamic, we cache its name for convenience and performance
311 774
		if ($isDynamic)
312
		{
313 12
			$varName = '$e' . $id;
314
315
			// Add the var declaration to the source
316 12
			$php .= $varName . '=' . $phpElName . ';';
317
318
			// Replace the element name with the var
319 12
			$phpElName = $varName;
320
		}
321
322
		// Test whether this element is void if we need this information
323 774
		if ($isVoid === 'maybe')
324
		{
325 12
			$php .= '$v' . $id . '=preg_match(' . var_export(TemplateParser::$voidRegexp, true) . ',' . $phpElName . ');';
326
		}
327
328
		// Open the start tag
329 774
		$php .= "\$this->out.='<'." . $phpElName . ';';
330
331
		// Serialize this element's content
332 774
		$php .= $this->serializeChildren($element);
333
334
		// Close that element unless we know it's void
335 774
		if ($isVoid !== 'yes')
336
		{
337 773
			$php .= "\$this->out.='</'." . $phpElName . ".'>';";
338
		}
339
340
		// If this element was maybe void, serializeCloseTag() has put its content within an if
341
		// block. We need to close that block
342 774
		if ($isVoid === 'maybe')
343
		{
344 12
			$php .= '}';
345
		}
346
347 774
		return $php;
348
	}
349
350
	/**
351
	* Serialize a <switch/> node that has a branch-key attribute
352
	*
353
	* @param  DOMElement $switch <switch/> node
354
	* @return string
355
	*/
356 33
	protected function serializeHash(DOMElement $switch)
357
	{
358 33
		$statements = [];
359 33
		foreach ($this->xpath->query('case[@branch-values]', $switch) as $case)
360
		{
361 32
			foreach (unserialize($case->getAttribute('branch-values')) as $value)
362
			{
363 32
				$statements[$value] = $this->serializeChildren($case);
364
			}
365
		}
366 33
		if (!isset($case))
367
		{
368 1
			throw new RuntimeException;
369
		}
370
371 32
		$defaultCase = $this->xpath->query('case[not(@branch-values)]', $switch)->item(0);
372 32
		$defaultCode = ($defaultCase instanceof DOMElement) ? $this->serializeChildren($defaultCase) : '';
373 32
		$expr        = $this->convertXPath($switch->getAttribute('branch-key'));
374
375 32
		return SwitchStatement::generate($expr, $statements, $defaultCode);
376
	}
377
378
	/**
379
	* Serialize an <output/> node
380
	*
381
	* @param  DOMElement $output <output/> node
382
	* @return string
383
	*/
384 671
	protected function serializeOutput(DOMElement $output)
385
	{
386 671
		$context = $output->getAttribute('escape');
387
388 671
		$php = '$this->out.=';
389 671
		if ($output->getAttribute('type') === 'xpath')
390
		{
391 588
			$php .= $this->escapePHPOutput($this->convertXPath($output->textContent), $context);
392
		}
393
		else
394
		{
395 434
			$php .= var_export($this->escapeLiteral($output->textContent, $context), true);
396
		}
397 671
		$php .= ';';
398
399 671
		return $php;
400
	}
401
402
	/**
403
	* Serialize a <switch/> node
404
	*
405
	* @param  DOMElement $switch <switch/> node
406
	* @return string
407
	*/
408 321
	protected function serializeSwitch(DOMElement $switch)
409
	{
410
		// Use a specialized branch table if the minimum number of branches is reached
411 321
		if ($switch->hasAttribute('branch-key') && $this->hasMultipleCases($switch))
412
		{
413 32
			return $this->serializeHash($switch);
414
		}
415
416 313
		$php   = '';
417 313
		$if    = 'if';
418 313
		foreach ($this->xpath->query('case', $switch) as $case)
419
		{
420 313
			if ($case->hasAttribute('test'))
421
			{
422 313
				$php .= $if . '(' . $this->convertCondition($case->getAttribute('test')) . ')';
423
			}
424
			else
425
			{
426 172
				$php .= 'else';
427
			}
428
429 313
			$php .= '{' . $this->serializeChildren($case) . '}';
430 313
			$if   = 'elseif';
431
		}
432
433 313
		return $php;
434
	}
435
}