Completed
Push — master ( 685cd8...4744c5 )
by Josh
14:01
created

OptimizeChoose::isXslChoose()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 2
eloc 2
nc 2
nop 1
1
<?php
2
3
/**
4
* @package   s9e\TextFormatter
5
* @copyright Copyright (c) 2010-2017 The s9e Authors
6
* @license   http://www.opensource.org/licenses/mit-license.php The MIT License
7
*/
8
namespace s9e\TextFormatter\Configurator\TemplateNormalizations;
9
10
use DOMElement;
11
use DOMNode;
12
13
class OptimizeChoose extends AbstractNormalization
14
{
15
	/**
16
	* @var DOMElement Current xsl:choose element
17
	*/
18
	protected $choose;
19
20
	/**
21
	* {@inheritdoc}
22
	*/
23
	protected $queries = ['//xsl:choose'];
24
25
	/**
26
	* Adopt the children of given element's only child
27
	*
28
	* @param  DOMElement $branch
29
	* @return void
30
	*/
31
	protected function adoptChildren(DOMElement $branch)
32
	{
33
		while ($branch->firstChild->firstChild)
34
		{
35
			$branch->appendChild($branch->firstChild->removeChild($branch->firstChild->firstChild));
36
		}
37
		$branch->removeChild($branch->firstChild);
38
	}
39
40
	/**
41
	* Retrieve a list of attributes from given element
42
	*
43
	* @return array NamespaceURI#nodeName as keys, attribute values as values
44
	*/
45
	protected function getAttributes(DOMElement $element)
46
	{
47
		$attributes = array();
48
		foreach ($element->attributes as $attribute)
49
		{
50
			$key = $attribute->namespaceURI . '#' . $attribute->nodeName;
51
			$attributes[$key] = $attribute->nodeValue;
52
		}
53
54
		return $attributes;
55
	}
56
57
	/**
58
	* Return a list the xsl:when and xsl:otherwise children of current xsl:choose element
59
	*
60
	* @return DOMElement[]
61
	*/
62
	protected function getBranches()
63
	{
64
		$query = 'xsl:when|xsl:otherwise';
65
66
		return $this->xpath($query, $this->choose);
67
	}
68
69
	/**
70
	* Test whether current xsl:choose element has no content besides xsl:when and xsl:otherwise
71
	*
72
	* @return bool
73
	*/
74
	protected function hasNoContent()
75
	{
76
		$query = 'count(xsl:when/node() | xsl:otherwise/node())';
77
78
		return !$this->xpath->evaluate($query, $this->choose);
79
	}
80
81
	/**
82
	* Test whether current xsl:choose element has an xsl:otherwise child
83
	*
84
	* @return bool
85
	*/
86
	protected function hasOtherwise()
87
	{
88
		return (bool) $this->xpath->evaluate('count(xsl:otherwise)', $this->choose);
89
	}
90
91
	/**
92
	* Test whether two nodes are identical
93
	*
94
	* ext/dom does not support isEqualNode() from DOM Level 3 so this is a makeshift replacement.
95
	* Unlike the DOM 3 function, attributes order matters
96
	*
97
	* @param  DOMNode $node1
98
	* @param  DOMNode $node2
99
	* @return bool
100
	*/
101
	protected function isEqualNode(DOMNode $node1, DOMNode $node2)
102
	{
103
		return ($node1->ownerDocument->saveXML($node1) === $node2->ownerDocument->saveXML($node2));
104
	}
105
106
	/**
107
	* Test whether two elements have the same start tag
108
	*
109
	* @param  DOMElement $el1
110
	* @param  DOMElement $el2
111
	* @return bool
112
	*/
113
	protected function isEqualTag(DOMElement $el1, DOMElement $el2)
114
	{
115
		return ($el1->namespaceURI === $el2->namespaceURI && $el1->nodeName === $el2->nodeName && $this->getAttributes($el1) === $this->getAttributes($el2));
116
	}
117
118
	/**
119
	* Test whether all branches of current xsl:choose element share a common firstChild/lastChild
120
	*
121
	* @param  string $childType Either firstChild or lastChild
122
	* @return bool
123
	*/
124
	protected function matchBranches($childType)
125
	{
126
		$branches = $this->getBranches();
127
		if (!isset($branches[0]->$childType))
128
		{
129
			return false;
130
		}
131
132
		$childNode = $branches[0]->$childType;
133
		foreach ($branches as $branch)
134
		{
135
			if (!isset($branch->$childType) || !$this->isEqualNode($childNode, $branch->$childType))
136
			{
137
				return false;
138
			}
139
		}
140
141
		return true;
142
	}
143
144
	/**
145
	* Test whether all branches of current xsl:choose element have a single child with the same start tag
146
	*
147
	* @return bool
148
	*/
149
	protected function matchOnlyChild()
150
	{
151
		$branches = $this->getBranches();
152
		if (!isset($branches[0]->firstChild))
153
		{
154
			return false;
155
		}
156
157
		$firstChild = $branches[0]->firstChild;
158
		if ($this->isXsl($firstChild, 'choose'))
159
		{
160
			// Abort on xsl:choose because we can't move it without moving its children
161
			return false;
162
		}
163
164
		foreach ($branches as $branch)
165
		{
166
			if ($branch->childNodes->length !== 1 || !($branch->firstChild instanceof DOMElement))
167
			{
168
				return false;
169
			}
170
			if (!$this->isEqualTag($firstChild, $branch->firstChild))
171
			{
172
				return false;
173
			}
174
		}
175
176
		return true;
177
	}
178
179
	/**
180
	* Move the firstChild of each branch before current xsl:choose
181
	*
182
	* @return void
183
	*/
184
	protected function moveFirstChildBefore()
185
	{
186
		$branches = $this->getBranches();
187
		$this->choose->parentNode->insertBefore(array_pop($branches)->firstChild, $this->choose);
188
		foreach ($branches as $branch)
189
		{
190
			$branch->removeChild($branch->firstChild);
191
		}
192
	}
193
194
	/**
195
	* Move the lastChild of each branch after current xsl:choose
196
	*
197
	* @return void
198
	*/
199
	protected function moveLastChildAfter()
200
	{
201
		$branches = $this->getBranches();
202
		$node     = array_pop($branches)->lastChild;
203
		if (isset($this->choose->nextSibling))
204
		{
205
			$this->choose->parentNode->insertBefore($node, $this->choose->nextSibling);
206
		}
207
		else
208
		{
209
			$this->choose->parentNode->appendChild($node);
210
		}
211
		foreach ($branches as $branch)
212
		{
213
			$branch->removeChild($branch->lastChild);
214
		}
215
	}
216
217
	/**
218
	* {@inheritdoc}
219
	*/
220
	protected function normalizeElement(DOMElement $element)
221
	{
222
		$this->choose = $element;
223
		if ($this->hasOtherwise())
224
		{
225
			$this->optimizeCommonFirstChild();
226
			$this->optimizeCommonLastChild();
227
			$this->optimizeCommonOnlyChild();
228
			$this->optimizeEmptyOtherwise();
229
		}
230
		if ($this->hasNoContent())
231
		{
232
			$this->choose->parentNode->removeChild($this->choose);
233
		}
234
		else
235
		{
236
			$this->optimizeSingleBranch();
237
		}
238
	}
239
240
	/**
241
	* Optimize current xsl:choose by moving out the first child of each branch if they match
242
	*
243
	* @return void
244
	*/
245
	protected function optimizeCommonFirstChild()
246
	{
247
		while ($this->matchBranches('firstChild'))
248
		{
249
			$this->moveFirstChildBefore();
250
		}
251
	}
252
253
	/**
254
	* Optimize current xsl:choose by moving out the last child of each branch if they match
255
	*
256
	* @return void
257
	*/
258
	protected function optimizeCommonLastChild()
259
	{
260
		while ($this->matchBranches('lastChild'))
261
		{
262
			$this->moveLastChildAfter();
263
		}
264
	}
265
266
	/**
267
	* Optimize current xsl:choose by moving out only child of each branch if they match
268
	*
269
	* This will reorder xsl:choose/xsl:when/div into div/xsl:choose/xsl:when if every branch has
270
	* the same only child (excluding the child's own descendants)
271
	*
272
	* @return void
273
	*/
274
	protected function optimizeCommonOnlyChild()
275
	{
276
		while ($this->matchOnlyChild())
277
		{
278
			$this->reparentChild();
279
		}
280
	}
281
282
	/**
283
	* Optimize away the xsl:otherwise child of current xsl:choose if it's empty
284
	*
285
	* @return void
286
	*/
287
	protected function optimizeEmptyOtherwise()
288
	{
289
		$query = 'xsl:otherwise[count(node()) = 0]';
290
		foreach ($this->xpath($query, $this->choose) as $otherwise)
291
		{
292
			$this->choose->removeChild($otherwise);
293
		}
294
	}
295
296
	/**
297
	* Replace current xsl:choose with xsl:if if it has only one branch
298
	*
299
	* @return void
300
	*/
301
	protected function optimizeSingleBranch()
302
	{
303
		$query = 'count(xsl:when) = 1 and not(xsl:otherwise)';
304
		if (!$this->xpath->evaluate($query, $this->choose))
305
		{
306
			return;
307
		}
308
		$when = $this->xpath('xsl:when', $this->choose)[0];
309
		$if   = $this->createElement('xsl:if');
310
		$if->setAttribute('test', $when->getAttribute('test'));
311
		while ($when->firstChild)
312
		{
313
			$if->appendChild($when->removeChild($when->firstChild));
314
		}
315
316
		$this->choose->parentNode->replaceChild($if, $this->choose);
317
	}
318
319
	/**
320
	* Reorder the current xsl:choose tree to make it a child of the first child of its first branch
321
	*
322
	* This will reorder xsl:choose/xsl:when/div into div/xsl:choose/xsl:when
323
	*
324
	* @return void
325
	*/
326
	protected function reparentChild()
327
	{
328
		$branches  = $this->getBranches();
329
		$childNode = $branches[0]->firstChild->cloneNode();
330
		$childNode->appendChild($this->choose->parentNode->replaceChild($childNode, $this->choose));
331
332
		foreach ($branches as $branch)
333
		{
334
			$this->adoptChildren($branch);
335
		}
336
	}
337
338
	/**
339
	* {@inheritdoc}
340
	*/
341
	protected function reset()
342
	{
343
		$this->choose = null;
344
		parent::reset();
345
	}
346
}