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

Optimizer::removeEmptyDefaultCases()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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