Completed
Push — master ( 25559c...472c56 )
by Josh
20:25
created

Normalizer::getOutputContext()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 17
rs 9.4285
c 1
b 0
f 0
cc 3
eloc 9
nc 3
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
			$this->markSwitchTable($switch);
178
		}
179
	}
180
181
	/**
182
	* Mark given switch element if it's used as a branch table
183
	*
184
	* @param  DOMElement $switch
185
	* @return void
186
	*/
187
	protected function markSwitchTable(DOMElement $switch)
188
	{
189
		$branches = [];
190
		$values   = [];
191
		foreach ($this->query('./case[@test]', $switch) as $i => $case)
192
		{
193
			$map = XPathHelper::parseEqualityExpr($case->getAttribute('test'));
194
			if ($map === false)
195
			{
196
				return;
197
			}
198
			$values      += $map;
199
			$branches[$i] = end($map);
200
		}
201
		if (count($values) !== 1)
202
		{
203
			return;
204
		}
205
206
		$switch->setAttribute('branch-key', key($values));
207
		foreach ($branches as $i => $values)
208
		{
209
			sort($values);
210
			$switch->childNodes->item($i)->setAttribute('branch-values', serialize($values));
211
		}
212
	}
213
214
	/**
215
	* Mark conditional <closeTag/> nodes
216
	*
217
	* @param  DOMDocument $ir
218
	* @return void
219
	*/
220
	protected function markConditionalCloseTagElements(DOMDocument $ir)
221
	{
222
		foreach ($ir->getElementsByTagName('closeTag') as $closeTag)
223
		{
224
			$id = $closeTag->getAttribute('id');
225
226
			// For each <switch/> ancestor, look for a <closeTag/> and that is either a sibling or
227
			// the descendant of a sibling, and that matches the id
228
			$query = 'ancestor::switch/'
229
			       . 'following-sibling::*/'
230
			       . 'descendant-or-self::closeTag[@id = "' . $id . '"]';
231
			foreach ($this->query($query, $closeTag) as $following)
232
			{
233
				// Mark following <closeTag/> nodes to indicate that the status of this tag must
234
				// be checked before it is closed
235
				$following->setAttribute('check', '');
236
237
				// Mark the current <closeTag/> to indicate that it must set a flag to indicate
238
				// that its tag has been closed
239
				$closeTag->setAttribute('set', '');
240
			}
241
		}
242
	}
243
244
	/**
245
	* Mark void elements
246
	*
247
	* @param  DOMDocument $ir
248
	* @return void
249
	*/
250
	protected function markVoidElements(DOMDocument $ir)
251
	{
252
		foreach ($ir->getElementsByTagName('element') as $element)
253
		{
254
			// Test whether this element is (maybe) void
255
			$elName = $element->getAttribute('name');
256
			if (strpos($elName, '{') !== false)
257
			{
258
				// Dynamic element names must be checked at runtime
259
				$element->setAttribute('void', 'maybe');
260
			}
261
			elseif (preg_match($this->voidRegexp, $elName))
262
			{
263
				// Static element names can be checked right now
264
				$element->setAttribute('void', 'yes');
265
			}
266
		}
267
	}
268
269
	/**
270
	* Fill in output context
271
	*
272
	* @param  DOMDocument $ir
273
	* @return void
274
	*/
275
	protected function setOutputContext(DOMDocument $ir)
276
	{
277
		foreach ($ir->getElementsByTagName('output') as $output)
278
		{
279
			$output->setAttribute('escape', $this->getOutputContext($output));
280
		}
281
	}
282
}