Completed
Push — master ( a3852d...087a6c )
by Josh
15:59
created

Serializer::serializeHash()   B

Complexity

Conditions 5
Paths 9

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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