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