Passed
Push — master ( b5d664...f14bd4 )
by Josh
02:35
created

Normalizer::markBooleanAttributes()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 4
c 0
b 0
f 0
dl 0
loc 8
ccs 5
cts 5
cp 1
rs 10
cc 3
nc 3
nop 1
crap 3
1
<?php
2
3
/**
4
* @package   s9e\TextFormatter
5
* @copyright Copyright (c) 2010-2021 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 71
	public function __construct(Optimizer $optimizer)
33
	{
34 71
		$this->optimizer = $optimizer;
35
	}
36
37
	/**
38
	* Normalize an IR
39
	*
40
	* @param  DOMDocument $ir
41
	* @return void
42
	*/
43 68
	public function normalize(DOMDocument $ir)
44
	{
45 68
		$this->createXPath($ir);
46 68
		$this->addDefaultCase($ir);
47 68
		$this->addElementIds($ir);
48 68
		$this->addCloseTagElements($ir);
49 68
		$this->markVoidElements($ir);
50 68
		$this->optimizer->optimize($ir);
51 68
		$this->markConditionalCloseTagElements($ir);
52 68
		$this->setOutputContext($ir);
53 68
		$this->markBranchTables($ir);
54 68
		$this->markBooleanAttributes($ir);
55
	}
56
57
	/**
58
	* Add <closeTag/> elements everywhere an open start tag should be closed
59
	*
60
	* @param  DOMDocument $ir
61
	* @return void
62
	*/
63 68
	protected function addCloseTagElements(DOMDocument $ir)
64
	{
65
		$exprs = [
66 68
			'//applyTemplates[not(ancestor::attribute)]',
67
			'//comment',
68
			'//element',
69
			'//output[not(ancestor::attribute)]'
70
		];
71 68
		foreach ($this->query(implode('|', $exprs)) as $node)
72
		{
73 67
			$parentElementId = $this->getParentElementId($node);
74 67
			if (isset($parentElementId))
75
			{
76 37
				$node->parentNode
77 37
				     ->insertBefore($ir->createElement('closeTag'), $node)
78 37
				     ->setAttribute('id', $parentElementId);
79
			}
80
81
			// Append a <closeTag/> to <element/> nodes to ensure that empty elements get closed
82 67
			if ($node->nodeName === 'element')
83
			{
84 47
				$id = $node->getAttribute('id');
85 47
				$this->appendElement($node, 'closeTag')->setAttribute('id', $id);
86
			}
87
		}
88
	}
89
90
	/**
91
	* Add an empty default <case/> to <switch/> nodes that don't have one
92
	*
93
	* @param  DOMDocument $ir
94
	* @return void
95
	*/
96 68
	protected function addDefaultCase(DOMDocument $ir)
0 ignored issues
show
Unused Code introduced by
The parameter $ir is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

96
	protected function addDefaultCase(/** @scrutinizer ignore-unused */ DOMDocument $ir)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
97
	{
98 68
		foreach ($this->query('//switch[not(case[not(@test)])]') as $switch)
99
		{
100 16
			$this->appendElement($switch, 'case');
101
		}
102
	}
103
104
	/**
105
	* Add an id attribute to <element/> nodes
106
	*
107
	* @param  DOMDocument $ir
108
	* @return void
109
	*/
110 68
	protected function addElementIds(DOMDocument $ir)
0 ignored issues
show
Unused Code introduced by
The parameter $ir is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

110
	protected function addElementIds(/** @scrutinizer ignore-unused */ DOMDocument $ir)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
111
	{
112 68
		$id = 0;
113 68
		foreach ($this->query('//element') as $element)
114
		{
115 47
			$element->setAttribute('id', ++$id);
116
		}
117
	}
118
119
	/**
120
	* Get the context type for given output element
121
	*
122
	* @param  DOMNode $output
123
	* @return string
124
	*/
125 57
	protected function getOutputContext(DOMNode $output)
126
	{
127
		$contexts = [
128 57
			'boolean(ancestor::attribute)'             => 'attribute',
129
			'@disable-output-escaping="yes"'           => 'raw',
130
			'count(ancestor::element[@name="script"])' => 'raw'
131
		];
132 57
		foreach ($contexts as $expr => $context)
133
		{
134 57
			if ($this->evaluate($expr, $output))
135
			{
136 31
				return $context;
137
			}
138
		}
139
140 35
		return 'text';
141
	}
142
143
	/**
144
	* Get the ID of the closest "element" ancestor
145
	*
146
	* @param  DOMNode     $node Context node
147
	* @return string|null
148
	*/
149 67
	protected function getParentElementId(DOMNode $node)
150
	{
151 67
		$parentNode = $node->parentNode;
152 67
		while (isset($parentNode))
153
		{
154 67
			if ($parentNode->nodeName === 'element')
155
			{
156 37
				return $parentNode->getAttribute('id');
157
			}
158 67
			$parentNode = $parentNode->parentNode;
159
		}
160
	}
161
162
	/**
163
	* Mark switch elements that are used as branch tables
164
	*
165
	* If a switch is used for a series of equality tests against the same attribute or variable, the
166
	* attribute/variable is stored within the switch as "branch-key" and the values it is compared
167
	* against are stored JSON-encoded in the case as "branch-values". It can be used to create
168
	* optimized branch tables
169
	*
170
	* @param  DOMDocument $ir
171
	* @return void
172
	*/
173 68
	protected function markBranchTables(DOMDocument $ir)
0 ignored issues
show
Unused Code introduced by
The parameter $ir is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

173
	protected function markBranchTables(/** @scrutinizer ignore-unused */ DOMDocument $ir)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
174
	{
175
		// Iterate over switch elements that have at least two case children with a test attribute
176 68
		foreach ($this->query('//switch[case[2][@test]]') as $switch)
177
		{
178 11
			$this->markSwitchTable($switch);
179
		}
180
	}
181
182
	/**
183
	* Mark given switch element if it's used as a branch table
184
	*
185
	* @param  DOMElement $switch
186
	* @return void
187
	*/
188 11
	protected function markSwitchTable(DOMElement $switch)
189
	{
190 11
		$cases = [];
191 11
		$maps  = [];
192 11
		foreach ($this->query('./case[@test]', $switch) as $i => $case)
193
		{
194 11
			$map = XPathHelper::parseEqualityExpr($case->getAttribute('test'));
195 11
			if ($map === false)
196
			{
197 3
				return;
198
			}
199 9
			$maps     += $map;
200 9
			$cases[$i] = [$case, end($map)];
201
		}
202 8
		if (count($maps) !== 1)
203
		{
204 3
			return;
205
		}
206
207 5
		$switch->setAttribute('branch-key', key($maps));
208 5
		foreach ($cases as list($case, $values))
209
		{
210 5
			sort($values);
211 5
			$case->setAttribute('branch-values', serialize($values));
212
		}
213
	}
214
215
	/**
216
	* Mark conditional <closeTag/> nodes
217
	*
218
	* @param  DOMDocument $ir
219
	* @return void
220
	*/
221 68
	protected function markConditionalCloseTagElements(DOMDocument $ir)
0 ignored issues
show
Unused Code introduced by
The parameter $ir is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

221
	protected function markConditionalCloseTagElements(/** @scrutinizer ignore-unused */ DOMDocument $ir)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
222
	{
223 68
		foreach ($this->query('//closeTag') as $closeTag)
224
		{
225 47
			$id = $closeTag->getAttribute('id');
226
227
			// For each <switch/> ancestor, look for a <closeTag/> and that is either a sibling or
228
			// the descendant of a sibling, and that matches the id
229
			$query = 'ancestor::switch/'
230
			       . 'following-sibling::*/'
231 47
			       . 'descendant-or-self::closeTag[@id = "' . $id . '"]';
232 47
			foreach ($this->query($query, $closeTag) as $following)
233
			{
234
				// Mark following <closeTag/> nodes to indicate that the status of this tag must
235
				// be checked before it is closed
236 3
				$following->setAttribute('check', '');
237
238
				// Mark the current <closeTag/> to indicate that it must set a flag to indicate
239
				// that its tag has been closed
240 3
				$closeTag->setAttribute('set', '');
241
			}
242
		}
243
	}
244
245
	/**
246
	* Mark boolean attributes
247
	*
248
	* The test is case-sensitive and only covers attribute that are minimized by libxslt
249
	*
250
	* @param  DOMDocument $ir
251
	* @return void
252
	*/
253 68
	protected function markBooleanAttributes(DOMDocument $ir): void
0 ignored issues
show
Unused Code introduced by
The parameter $ir is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

253
	protected function markBooleanAttributes(/** @scrutinizer ignore-unused */ DOMDocument $ir): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
254
	{
255 68
		$attrNames = ['checked', 'compact', 'declare', 'defer', 'disabled', 'ismap', 'multiple', 'nohref', 'noresize', 'noshade', 'nowrap', 'readonly', 'selected'];
256 68
		foreach ($this->query('//attribute') as $attribute)
257
		{
258 28
			if (in_array($attribute->getAttribute('name'), $attrNames, true))
259
			{
260 1
				$attribute->setAttribute('boolean', 'yes');
261
			}
262
		}
263
	}
264
265
	/**
266
	* Mark void elements
267
	*
268
	* @param  DOMDocument $ir
269
	* @return void
270
	*/
271 68
	protected function markVoidElements(DOMDocument $ir)
0 ignored issues
show
Unused Code introduced by
The parameter $ir is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

271
	protected function markVoidElements(/** @scrutinizer ignore-unused */ DOMDocument $ir)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
272
	{
273 68
		foreach ($this->query('//element') as $element)
274
		{
275
			// Test whether this element is (maybe) void
276 47
			$elName = $element->getAttribute('name');
277 47
			if (strpos($elName, '{') !== false)
278
			{
279
				// Dynamic element names must be checked at runtime
280 5
				$element->setAttribute('void', 'maybe');
281
			}
282 42
			elseif (preg_match($this->voidRegexp, $elName))
283
			{
284
				// Static element names can be checked right now
285 8
				$element->setAttribute('void', 'yes');
286
			}
287
		}
288
	}
289
290
	/**
291
	* Fill in output context
292
	*
293
	* @param  DOMDocument $ir
294
	* @return void
295
	*/
296 68
	protected function setOutputContext(DOMDocument $ir)
0 ignored issues
show
Unused Code introduced by
The parameter $ir is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

296
	protected function setOutputContext(/** @scrutinizer ignore-unused */ DOMDocument $ir)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
297
	{
298 68
		foreach ($this->query('//output') as $output)
299
		{
300 57
			$output->setAttribute('escape', $this->getOutputContext($output));
301
		}
302
	}
303
}