Completed
Branch TemplateParserRefactor (06d644)
by Josh
14:21
created

Normalizer::addDefaultCase()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 3
nc 2
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\Helpers\TemplateParser;
9
10
use DOMDocument;
11
use DOMElement;
12
use DOMNode;
13
use s9e\TextFormatter\Configurator\Helpers\XPathHelper;
14
15
class Normalizer extends IRProcessor
16
{
17
	/**
18
	* @var Optimizer
19
	*/
20
	protected $optimizer;
21
22
	/**
23
	* @var string Regexp that matches the names of all void elements
24
	* @link http://www.w3.org/TR/html-markup/syntax.html#void-elements
25
	*/
26
	public $voidRegexp = '/^(?:area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/Di';
27
28
	/**
29
	* @param  Optimizer $optimizer
30
	* @return void
31
	*/
32
	public function __construct(Optimizer $optimizer)
33
	{
34
		$this->optimizer = $optimizer;
35
	}
36
37
	/**
38
	* Normalize an IR
39
	*
40
	* @param  DOMDocument $ir
41
	* @return void
42
	*/
43
	public function normalize(DOMDocument $ir)
44
	{
45
		$this->createXPath($ir);
46
		$this->addDefaultCase($ir);
47
		$this->addElementIds($ir);
48
		$this->addCloseTagElements($ir);
49
		$this->markEmptyElements($ir);
50
		$this->optimizer->optimize($ir);
51
		$this->markConditionalCloseTagElements($ir);
52
		$this->setOutputContext($ir);
53
		$this->markBranchTables($ir);
54
	}
55
56
	/**
57
	* Add <closeTag/> elements everywhere an open start tag should be closed
58
	*
59
	* @param  DOMDocument $ir
60
	* @return void
61
	*/
62
	protected function addCloseTagElements(DOMDocument $ir)
63
	{
64
		$exprs = [
65
			'//applyTemplates[not(ancestor::attribute)]',
66
			'//comment',
67
			'//element',
68
			'//output[not(ancestor::attribute)]'
69
		];
70
		foreach ($this->query(implode('|', $exprs)) as $node)
71
		{
72
			$parentElementId = $this->getParentElementId($node);
73
			if (isset($parentElementId))
74
			{
75
				$node->parentNode
76
				     ->insertBefore($ir->createElement('closeTag'), $node)
77
				     ->setAttribute('id', $parentElementId);
78
			}
79
80
			// Append a <closeTag/> to <element/> nodes to ensure that empty elements get closed
81
			if ($node->nodeName === 'element')
82
			{
83
				$id = $node->getAttribute('id');
84
				$this->appendElement($node, 'closeTag')->setAttribute('id', $id);
85
			}
86
		}
87
	}
88
89
	/**
90
	* Add an empty default <case/> to <switch/> nodes that don't have one
91
	*
92
	* @param  DOMDocument $ir
93
	* @return void
94
	*/
95
	protected function addDefaultCase(DOMDocument $ir)
96
	{
97
		foreach ($this->query('//switch[not(case[not(@test)])]') as $switch)
98
		{
99
			$this->appendElement($switch, 'case');
100
		}
101
	}
102
103
	/**
104
	* Add an id attribute to <element/> nodes
105
	*
106
	* @param  DOMDocument $ir
107
	* @return void
108
	*/
109
	protected function addElementIds(DOMDocument $ir)
110
	{
111
		$id = 0;
112
		foreach ($ir->getElementsByTagName('element') as $element)
113
		{
114
			$element->setAttribute('id', ++$id);
115
		}
116
	}
117
118
	/**
119
	* Get the context type for given output element
120
	*
121
	* @param  DOMNode $output
122
	* @return string
123
	*/
124
	protected function getOutputContext(DOMNode $output)
125
	{
126
		$contexts = [
127
			'boolean(ancestor::attribute)'             => 'attribute',
128
			'@disable-output-escaping="yes"'           => 'raw',
129
			'count(ancestor::element[@name="script"])' => 'raw'
130
		];
131
		foreach ($contexts as $expr => $context)
132
		{
133
			if ($this->evaluate($expr, $output))
134
			{
135
				return $context;
136
			}
137
		}
138
139
		return 'text';
140
	}
141
142
	/**
143
	* Get the ID of the closest "element" ancestor
144
	*
145
	* @param  DOMNode     $node Context node
146
	* @return string|null
147
	*/
148
	protected function getParentElementId(DOMNode $node)
149
	{
150
		$parentNode = $node->parentNode;
151
		while (isset($parentNode))
152
		{
153
			if ($parentNode->nodeName === 'element')
154
			{
155
				return $parentNode->getAttribute('id');
156
			}
157
			$parentNode = $parentNode->parentNode;
158
		}
159
	}
160
161
	/**
162
	* Test whether given element will be empty at runtime (no content, no children)
163
	*
164
	* @param  DOMElement $ir Element in the IR
165
	* @return string         'yes', 'maybe' or 'no'
166
	*/
167
	protected function isEmpty(DOMElement $ir)
168
	{
169
		// Comments and elements count as not-empty and literal output is sure to output something
170
		if ($this->evaluate('count(comment | element | output[@type="literal"])', $ir))
171
		{
172
			return 'no';
173
		}
174
175
		// Test all branches of a <switch/>
176
		// NOTE: this assumes that <switch/> are normalized to always have a default <case/>
177
		$cases = [];
178
		foreach ($this->query('switch/case', $ir) as $case)
179
		{
180
			$cases[$this->isEmpty($case)] = 1;
181
		}
182
183
		if (isset($cases['maybe']))
184
		{
185
			return 'maybe';
186
		}
187
188
		if (isset($cases['no']))
189
		{
190
			// If all the cases are not-empty, the element is not-empty
191
			if (!isset($cases['yes']))
192
			{
193
				return 'no';
194
			}
195
196
			// Some 'yes' and some 'no', the element is a 'maybe'
197
			return 'maybe';
198
		}
199
200
		// Test for <apply-templates/> or XPath output
201
		if ($this->evaluate('count(applyTemplates | output[@type="xpath"])', $ir))
202
		{
203
			// We can't know in advance whether those will produce output
204
			return 'maybe';
205
		}
206
207
		return 'yes';
208
	}
209
210
	/**
211
	* Mark switch elements that are used as branch tables
212
	*
213
	* If a switch is used for a series of equality tests against the same attribute or variable, the
214
	* attribute/variable is stored within the switch as "branch-key" and the values it is compared
215
	* against are stored JSON-encoded in the case as "branch-values". It can be used to create
216
	* optimized branch tables
217
	*
218
	* @param  DOMDocument $ir
219
	* @return void
220
	*/
221
	protected function markBranchTables(DOMDocument $ir)
222
	{
223
224
		// Iterate over switch elements that have at least two case children with a test attribute
225
		foreach ($this->query('//switch[case[2][@test]]') as $switch)
226
		{
227
			$key = null;
228
			$branchValues = [];
229
230
			foreach ($switch->childNodes as $i => $case)
231
			{
232
				if (!$case->hasAttribute('test'))
233
				{
234
					continue;
235
				}
236
237
				$map = XPathHelper::parseEqualityExpr($case->getAttribute('test'));
238
239
				// Test whether the expression matches an equality
240
				if ($map === false)
241
				{
242
					continue 2;
243
				}
244
245
				// Abort if there's more than 1 variable used
246
				if (count($map) !== 1)
247
				{
248
					continue 2;
249
				}
250
251
				// Test whether it uses the same key
252
				if (isset($key) && $key !== key($map))
253
				{
254
					continue 2;
255
				}
256
257
				$key = key($map);
258
				$branchValues[$i] = end($map);
259
			}
260
261
			$switch->setAttribute('branch-key', $key);
262
			foreach ($branchValues as $i => $values)
263
			{
264
				sort($values);
265
				$switch->childNodes->item($i)->setAttribute('branch-values', serialize($values));
266
			}
267
		}
268
	}
269
270
	/**
271
	* Mark conditional <closeTag/> nodes
272
	*
273
	* @param  DOMDocument $ir
274
	* @return void
275
	*/
276
	protected function markConditionalCloseTagElements(DOMDocument $ir)
277
	{
278
		foreach ($ir->getElementsByTagName('closeTag') as $closeTag)
279
		{
280
			$id = $closeTag->getAttribute('id');
281
282
			// For each <switch/> ancestor, look for a <closeTag/> and that is either a sibling or
283
			// the descendant of a sibling, and that matches the id
284
			$query = 'ancestor::switch/'
285
			       . 'following-sibling::*/'
286
			       . 'descendant-or-self::closeTag[@id = "' . $id . '"]';
287
			foreach ($this->query($query, $closeTag) as $following)
288
			{
289
				// Mark following <closeTag/> nodes to indicate that the status of this tag must
290
				// be checked before it is closed
291
				$following->setAttribute('check', '');
292
293
				// Mark the current <closeTag/> to indicate that it must set a flag to indicate
294
				// that its tag has been closed
295
				$closeTag->setAttribute('set', '');
296
			}
297
		}
298
	}
299
300
	/**
301
	* Mark void elements and elements with no content
302
	*
303
	* @param  DOMDocument $ir
304
	* @return void
305
	*/
306
	protected function markEmptyElements(DOMDocument $ir)
307
	{
308
		foreach ($ir->getElementsByTagName('element') as $element)
309
		{
310
			// Test whether this element is (maybe) void
311
			$elName = $element->getAttribute('name');
312
			if (strpos($elName, '{') !== false)
313
			{
314
				// Dynamic element names must be checked at runtime
315
				$element->setAttribute('void', 'maybe');
316
			}
317
			elseif (preg_match($this->voidRegexp, $elName))
318
			{
319
				// Static element names can be checked right now
320
				$element->setAttribute('void', 'yes');
321
			}
322
323
			// Find whether this element is empty
324
			$isEmpty = $this->isEmpty($element);
325
			if ($isEmpty === 'yes' || $isEmpty === 'maybe')
326
			{
327
				$element->setAttribute('empty', $isEmpty);
328
			}
329
		}
330
	}
331
332
	/**
333
	* Fill in output context
334
	*
335
	* @param  DOMDocument $ir
336
	* @return void
337
	*/
338
	protected function setOutputContext(DOMDocument $ir)
339
	{
340
		foreach ($ir->getElementsByTagName('output') as $output)
341
		{
342
			$output->setAttribute('escape', $this->getOutputContext($output));
343
		}
344
	}
345
}