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

Optimizer::cloneCloseTagElementsOutOfSwitch()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 19
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 19
rs 8.8571
cc 5
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
14
class Optimizer extends IRProcessor
15
{
16
	/**
17
	* Optimize an IR
18
	*
19
	* @param  DOMDocument $ir
20
	* @return void
21
	*/
22
	public function optimize(DOMDocument $ir)
23
	{
24
		$this->createXPath($ir);
25
26
		// Get a snapshot of current internal representation
27
		$xml = $ir->saveXML();
28
29
		// Set a maximum number of loops to ward against infinite loops
30
		$remainingLoops = 10;
31
32
		// From now on, keep looping until no further modifications are applied
33
		do
34
		{
35
			$old = $xml;
36
			$this->optimizeCloseTagElements($ir);
37
			$xml = $ir->saveXML();
38
		}
39
		while (--$remainingLoops > 0 && $xml !== $old);
40
41
		$this->removeCloseTagSiblings($ir);
42
		$this->removeContentFromVoidElements($ir);
43
		$this->mergeConsecutiveLiteralOutputElements($ir);
44
		$this->removeEmptyDefaultCases($ir);
45
	}
46
47
	/**
48
	* Clone closeTag elements that follow a switch into said switch
49
	*
50
	* If there's a <closeTag/> right after a <switch/>, clone the <closeTag/> at the end of
51
	* the every <case/> that does not end with a <closeTag/>
52
	*
53
	* @param  DOMDocument $ir
54
	* @return void
55
	*/
56
	protected function cloneCloseTagElementsIntoSwitch(DOMDocument $ir)
57
	{
58
		$query = '//switch[name(following-sibling::*) = "closeTag"]';
59
		foreach ($this->query($query) as $switch)
60
		{
61
			$closeTag = $switch->nextSibling;
62
			foreach ($switch->childNodes as $case)
63
			{
64
				if (!$case->lastChild || $case->lastChild->nodeName !== 'closeTag')
65
				{
66
					$case->appendChild($closeTag->cloneNode());
67
				}
68
			}
69
		}
70
	}
71
72
	/**
73
	* Clone closeTag elements from the head of a switch's cases before said switch
74
	*
75
	* If there's a <closeTag/> at the beginning of every <case/>, clone it and insert it
76
	* right before the <switch/> unless there's already one
77
	*
78
	* @param  DOMDocument $ir
79
	* @return void
80
	*/
81
	protected function cloneCloseTagElementsOutOfSwitch(DOMDocument $ir)
82
	{
83
		$query = '//switch[not(preceding-sibling::closeTag)]';
84
		foreach ($this->query($query) as $switch)
85
		{
86
			foreach ($switch->childNodes as $case)
87
			{
88
				if (!$case->firstChild || $case->firstChild->nodeName !== 'closeTag')
89
				{
90
					// This case is either empty or does not start with a <closeTag/> so we skip
91
					// to the next <switch/>
92
					continue 2;
93
				}
94
			}
95
			// Insert the first child of the last <case/>, which should be the same <closeTag/>
96
			// as every other <case/>
97
			$switch->parentNode->insertBefore($switch->lastChild->firstChild->cloneNode(), $switch);
98
		}
99
	}
100
101
	/**
102
	* Merge consecutive literal outputs
103
	*
104
	* @param  DOMDocument $ir
105
	* @return void
106
	*/
107
	protected function mergeConsecutiveLiteralOutputElements(DOMDocument $ir)
108
	{
109
		foreach ($this->query('//output[@type="literal"]') as $output)
110
		{
111
			$disableOutputEscaping = $output->getAttribute('disable-output-escaping');
112
			while ($this->nextSiblingIsLiteralOutput($output, $disableOutputEscaping))
113
			{
114
				$output->nodeValue = htmlspecialchars($output->nodeValue . $output->nextSibling->nodeValue);
115
				$output->parentNode->removeChild($output->nextSibling);
116
			}
117
		}
118
	}
119
120
	/**
121
	* Test whether the next sibling of an element is a literal output element with matching escaping
122
	*
123
	* @param  DOMElement $node
124
	* @param  string     $disableOutputEscaping
125
	* @return bool
126
	*/
127
	protected function nextSiblingIsLiteralOutput(DOMElement $node, $disableOutputEscaping)
128
	{
129
		return isset($node->nextSibling) && $node->nextSibling->nodeName === 'output' && $node->nextSibling->getAttribute('type') === 'literal' && $node->nextSibling->getAttribute('disable-output-escaping') === $disableOutputEscaping;
130
	}
131
132
	/**
133
	* Optimize closeTags elements
134
	*
135
	* @param  DOMDocument $ir
136
	* @return void
137
	*/
138
	protected function optimizeCloseTagElements(DOMDocument $ir)
139
	{
140
		$this->cloneCloseTagElementsIntoSwitch($ir);
141
		$this->cloneCloseTagElementsOutOfSwitch($ir);
142
		$this->removeRedundantCloseTagElementsInSwitch($ir);
143
		$this->removeRedundantCloseTagElements($ir);
144
	}
145
146
	/**
147
	* Remove redundant closeTag siblings after a switch
148
	*
149
	* If all branches of a switch have a closeTag we can remove any closeTag siblings of the switch
150
	*
151
	* @param  DOMDocument $ir
152
	* @return void
153
	*/
154
	protected function removeCloseTagSiblings(DOMDocument $ir)
155
	{
156
		$query = '//switch[not(case[not(closeTag)])]/following-sibling::closeTag';
157
		$this->removeNodes($ir, $query);
158
	}
159
160
	/**
161
	* Remove content from void elements
162
	*
163
	* For each void element, we find whichever <closeTag/> elements close it and remove everything
164
	* after
165
	*
166
	* @param  DOMDocument $ir
167
	* @return void
168
	*/
169
	protected function removeContentFromVoidElements(DOMDocument $ir)
170
	{
171
		foreach ($this->query('//element[@void="yes"]') as $element)
172
		{
173
			$id    = $element->getAttribute('id');
174
			$query = './/closeTag[@id="' . $id . '"]/following-sibling::*';
175
176
			$this->removeNodes($ir, $query, $element);
177
		}
178
	}
179
180
	/**
181
	* Remove empty default cases (no test and no descendants)
182
	*
183
	* @param  DOMDocument $ir
184
	* @return void
185
	*/
186
	protected function removeEmptyDefaultCases(DOMDocument $ir)
187
	{
188
		$query = '//case[not(@test | node())]';
189
		$this->removeNodes($ir, $query);
190
	}
191
192
	/**
193
	* Remove all nodes that match given XPath query
194
	*
195
	* @param  DOMDocument $ir
196
	* @param  string      $query
197
	* @param  DOMNode     $contextNode
198
	* @return void
199
	*/
200
	protected function removeNodes(DOMDocument $ir, $query, DOMNode $contextNode = null)
201
	{
202
		foreach ($this->query($query, $contextNode) as $node)
203
		{
204
			if ($node->parentNode instanceof DOMElement)
205
			{
206
				$node->parentNode->removeChild($node);
207
			}
208
		}
209
	}
210
211
	/**
212
	* Remove redundant closeTag elements from the tail of a switch's cases
213
	*
214
	* For each <closeTag/> remove duplicate <closeTag/> nodes that are either siblings or
215
	* descendants of a sibling
216
	*
217
	* @param  DOMDocument $ir
218
	* @return void
219
	*/
220
	protected function removeRedundantCloseTagElements(DOMDocument $ir)
221
	{
222
		foreach ($this->query('//closeTag') as $closeTag)
223
		{
224
			$id    = $closeTag->getAttribute('id');
225
			$query = 'following-sibling::*/descendant-or-self::closeTag[@id="' . $id . '"]';
226
227
			$this->removeNodes($ir, $query, $closeTag);
228
		}
229
	}
230
231
	/**
232
	* Remove redundant closeTag elements from the tail of a switch's cases
233
	*
234
	* If there's a <closeTag/> right after a <switch/>, remove all <closeTag/> nodes at the
235
	* end of every <case/>
236
	*
237
	* @param  DOMDocument $ir
238
	* @return void
239
	*/
240
	protected function removeRedundantCloseTagElementsInSwitch(DOMDocument $ir)
241
	{
242
		$query = '//switch[name(following-sibling::*) = "closeTag"]';
243
		foreach ($this->query($query) as $switch)
244
		{
245
			foreach ($switch->childNodes as $case)
246
			{
247
				while ($case->lastChild && $case->lastChild->nodeName === 'closeTag')
248
				{
249
					$case->removeChild($case->lastChild);
250
				}
251
			}
252
		}
253
	}
254
}