Completed
Branch TemplateParserRefactor (2db88c)
by Josh
16:20 queued 01:59
created

Normalizer::markVoidElements()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 18
rs 9.2
cc 4
eloc 7
nc 4
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->markVoidElements($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
	* Mark switch elements that are used as branch tables
163
	*
164
	* If a switch is used for a series of equality tests against the same attribute or variable, the
165
	* attribute/variable is stored within the switch as "branch-key" and the values it is compared
166
	* against are stored JSON-encoded in the case as "branch-values". It can be used to create
167
	* optimized branch tables
168
	*
169
	* @param  DOMDocument $ir
170
	* @return void
171
	*/
172
	protected function markBranchTables(DOMDocument $ir)
173
	{
174
		// Iterate over switch elements that have at least two case children with a test attribute
175
		foreach ($this->query('//switch[case[2][@test]]') as $switch)
176
		{
177
			$key = null;
178
			$branchValues = [];
179
180
			foreach ($switch->childNodes as $i => $case)
181
			{
182
				if (!$case->hasAttribute('test'))
183
				{
184
					continue;
185
				}
186
187
				$map = XPathHelper::parseEqualityExpr($case->getAttribute('test'));
188
189
				// Test whether the expression matches an equality
190
				if ($map === false)
191
				{
192
					continue 2;
193
				}
194
195
				// Abort if there's more than 1 variable used
196
				if (count($map) !== 1)
197
				{
198
					continue 2;
199
				}
200
201
				// Test whether it uses the same key
202
				if (isset($key) && $key !== key($map))
203
				{
204
					continue 2;
205
				}
206
207
				$key = key($map);
208
				$branchValues[$i] = end($map);
209
			}
210
211
			$switch->setAttribute('branch-key', $key);
212
			foreach ($branchValues as $i => $values)
213
			{
214
				sort($values);
215
				$switch->childNodes->item($i)->setAttribute('branch-values', serialize($values));
216
			}
217
		}
218
	}
219
220
	/**
221
	* Mark conditional <closeTag/> nodes
222
	*
223
	* @param  DOMDocument $ir
224
	* @return void
225
	*/
226
	protected function markConditionalCloseTagElements(DOMDocument $ir)
227
	{
228
		foreach ($ir->getElementsByTagName('closeTag') as $closeTag)
229
		{
230
			$id = $closeTag->getAttribute('id');
231
232
			// For each <switch/> ancestor, look for a <closeTag/> and that is either a sibling or
233
			// the descendant of a sibling, and that matches the id
234
			$query = 'ancestor::switch/'
235
			       . 'following-sibling::*/'
236
			       . 'descendant-or-self::closeTag[@id = "' . $id . '"]';
237
			foreach ($this->query($query, $closeTag) as $following)
238
			{
239
				// Mark following <closeTag/> nodes to indicate that the status of this tag must
240
				// be checked before it is closed
241
				$following->setAttribute('check', '');
242
243
				// Mark the current <closeTag/> to indicate that it must set a flag to indicate
244
				// that its tag has been closed
245
				$closeTag->setAttribute('set', '');
246
			}
247
		}
248
	}
249
250
	/**
251
	* Mark void elements
252
	*
253
	* @param  DOMDocument $ir
254
	* @return void
255
	*/
256
	protected function markVoidElements(DOMDocument $ir)
257
	{
258
		foreach ($ir->getElementsByTagName('element') as $element)
259
		{
260
			// Test whether this element is (maybe) void
261
			$elName = $element->getAttribute('name');
262
			if (strpos($elName, '{') !== false)
263
			{
264
				// Dynamic element names must be checked at runtime
265
				$element->setAttribute('void', 'maybe');
266
			}
267
			elseif (preg_match($this->voidRegexp, $elName))
268
			{
269
				// Static element names can be checked right now
270
				$element->setAttribute('void', 'yes');
271
			}
272
		}
273
	}
274
275
	/**
276
	* Fill in output context
277
	*
278
	* @param  DOMDocument $ir
279
	* @return void
280
	*/
281
	protected function setOutputContext(DOMDocument $ir)
282
	{
283
		foreach ($ir->getElementsByTagName('output') as $output)
284
		{
285
			$output->setAttribute('escape', $this->getOutputContext($output));
286
		}
287
	}
288
}