Completed
Push — master ( 3755ed...f1a382 )
by Josh
15:22
created

Serializer::convertXPath()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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