Completed
Branch Scrutinizer (3da711)
by Josh
03:32
created

Parser::parseChildren()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 13
dl 0
loc 23
ccs 13
cts 13
cp 1
rs 9.2222
c 0
b 0
f 0
cc 6
nc 6
nop 2
crap 6
1
<?php
2
3
/**
4
* @package   s9e\TextFormatter
5
* @copyright Copyright (c) 2010-2019 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 DOMXPath;
13
use RuntimeException;
14
use s9e\TextFormatter\Configurator\Helpers\AVTHelper;
15
use s9e\TextFormatter\Configurator\Helpers\TemplateLoader;
16
17
class Parser extends IRProcessor
18
{
19
	/**
20
	* @var Normalizer
21
	*/
22
	protected $normalizer;
23
24
	/**
25
	* @param  Normalizer $normalizer
26
	* @return void
27
	*/
28 71
	public function __construct(Normalizer $normalizer)
29
	{
30 71
		$this->normalizer = $normalizer;
31
	}
32
33
	/**
34
	* Parse a template into an internal representation
35
	*
36
	* @param  string      $template Source template
37
	* @return DOMDocument           Internal representation
38
	*/
39 71
	public function parse($template)
40
	{
41 71
		$dom = TemplateLoader::load($template);
42
43 71
		$ir = new DOMDocument;
44 71
		$ir->loadXML('<template/>');
45
46 71
		$this->createXPath($dom);
47 71
		$this->parseChildren($ir->documentElement, $dom->documentElement);
48 68
		$this->normalizer->normalize($ir);
49
50 68
		return $ir;
51
	}
52
53
	/**
54
	* Append <output/> elements corresponding to given AVT
55
	*
56
	* @param  DOMElement $parentNode Parent node
57
	* @param  string     $avt        Attribute value template
58
	* @return void
59
	*/
60 9
	protected function appendAVT(DOMElement $parentNode, $avt)
61
	{
62 9
		foreach (AVTHelper::parse($avt) as $token)
63
		{
64 9
			if ($token[0] === 'expression')
65
			{
66 3
				$this->appendXPathOutput($parentNode, $token[1]);
67
			}
68
			else
69
			{
70 7
				$this->appendLiteralOutput($parentNode, $token[1]);
71
			}
72
		}
73
	}
74
75
	/**
76
	* Append an <output/> element with literal content to given node
77
	*
78
	* @param  DOMElement $parentNode Parent node
79
	* @param  string     $content    Content to output
80
	* @return void
81
	*/
82 51
	protected function appendLiteralOutput(DOMElement $parentNode, $content)
83
	{
84 51
		if ($content === '')
85
		{
86 1
			return;
87
		}
88
89 50
		$this->appendElement($parentNode, 'output', htmlspecialchars($content))
90 50
		     ->setAttribute('type', 'literal');
91
	}
92
93
	/**
94
	* Append the structure for a <xsl:copy-of/> element to given node
95
	*
96
	* @param  DOMElement $parentNode Parent node
97
	* @param  string     $expr       Select expression, which is should only contain attributes
98
	* @return void
99
	*/
100 2
	protected function appendConditionalAttributes(DOMElement $parentNode, $expr)
101
	{
102 2
		preg_match_all('(@([-\\w]+))', $expr, $matches);
103 2
		foreach ($matches[1] as $attrName)
104
		{
105
			// Create a switch element in the IR
106 2
			$switch = $this->appendElement($parentNode, 'switch');
107 2
			$case   = $this->appendElement($switch, 'case');
108 2
			$case->setAttribute('test', '@' . $attrName);
109
110
			// Append an attribute element
111 2
			$attribute = $this->appendElement($case, 'attribute');
112 2
			$attribute->setAttribute('name', $attrName);
113
114
			// Set the attribute's content, which is simply the copied attribute's value
115 2
			$this->appendXPathOutput($attribute, '@' . $attrName);
116
		}
117
	}
118
119
	/**
120
	* Append an <output/> element for given XPath expression to given node
121
	*
122
	* @param  DOMElement $parentNode Parent node
123
	* @param  string     $expr       XPath expression
124
	* @return void
125
	*/
126 10
	protected function appendXPathOutput(DOMElement $parentNode, $expr)
127
	{
128 10
		$this->appendElement($parentNode, 'output', htmlspecialchars(trim($expr)))
129 10
		     ->setAttribute('type', 'xpath');
130
	}
131
132
	/**
133
	* Parse all the children of a given element
134
	*
135
	* @param  DOMElement $ir     Node in the internal representation that represents the parent node
136
	* @param  DOMElement $parent Parent node
137
	* @return void
138
	*/
139 71
	protected function parseChildren(DOMElement $ir, DOMElement $parent)
140
	{
141 71
		foreach ($parent->childNodes as $child)
142
		{
143 71
			switch ($child->nodeType)
144
			{
145 71
				case XML_COMMENT_NODE:
146
					// Do nothing
147 3
					break;
148
149 71
				case XML_TEXT_NODE:
150 40
					if (trim($child->textContent) !== '')
151
					{
152 40
						$this->appendLiteralOutput($ir, $child->textContent);
153
					}
154 40
					break;
155
156 70
				case XML_ELEMENT_NODE:
157 69
					$this->parseNode($ir, $child);
158 67
					break;
159
160
				default:
161 1
					throw new RuntimeException("Cannot parse node '" . $child->nodeName . "''");
162
			}
163
		}
164
	}
165
166
	/**
167
	* Parse a given node into the internal representation
168
	*
169
	* @param  DOMElement $ir   Node in the internal representation that represents the node's parent
170
	* @param  DOMElement $node Node to parse
171
	* @return void
172
	*/
173 69
	protected function parseNode(DOMElement $ir, DOMElement $node)
174
	{
175
		// XSL elements are parsed by the corresponding parseXsl* method
176 69
		if ($node->namespaceURI === self::XMLNS_XSL)
177
		{
178 60
			$methodName = 'parseXsl' . str_replace(' ', '', ucwords(str_replace('-', ' ', $node->localName)));
179 60
			if (!method_exists($this, $methodName))
180
			{
181 1
				throw new RuntimeException("Element '" . $node->nodeName . "' is not supported");
182
			}
183
184 59
			return $this->$methodName($ir, $node);
185
		}
186
187
		// Create an <element/> with a name attribute equal to given node's name
188 36
		$element = $this->appendElement($ir, 'element');
189 36
		$element->setAttribute('name', $node->nodeName);
190
191
		// Append an <attribute/> element for each namespace declaration
192 36
		$xpath = new DOMXPath($node->ownerDocument);
193 36
		foreach ($xpath->query('namespace::*', $node) as $ns)
194
		{
195 36
			if ($node->hasAttribute($ns->nodeName))
196
			{
197 3
				$irAttribute = $this->appendElement($element, 'attribute');
198 3
				$irAttribute->setAttribute('name', $ns->nodeName);
199 3
				$this->appendLiteralOutput($irAttribute, $ns->nodeValue);
200
			}
201
		}
202
203
		// Append an <attribute/> element for each of this node's attribute
204 36
		foreach ($node->attributes as $attribute)
205
		{
206 9
			$irAttribute = $this->appendElement($element, 'attribute');
207 9
			$irAttribute->setAttribute('name', $attribute->nodeName);
208
209
			// Append an <output/> element to represent the attribute's value
210 9
			$this->appendAVT($irAttribute, $attribute->value);
211
		}
212
213
		// Parse the content of this node
214 36
		$this->parseChildren($element, $node);
215
	}
216
217
	/**
218
	* Parse an <xsl:apply-templates/> node into the internal representation
219
	*
220
	* @param  DOMElement $ir   Node in the internal representation that represents the node's parent
221
	* @param  DOMElement $node <xsl:apply-templates/> node
222
	* @return void
223
	*/
224 19
	protected function parseXslApplyTemplates(DOMElement $ir, DOMElement $node)
225
	{
226 19
		$applyTemplates = $this->appendElement($ir, 'applyTemplates');
227 19
		if ($node->hasAttribute('select'))
228
		{
229 1
			$applyTemplates->setAttribute('select', $node->getAttribute('select'));
230
		}
231
	}
232
233
	/**
234
	* Parse an <xsl:attribute/> node into the internal representation
235
	*
236
	* @param  DOMElement $ir   Node in the internal representation that represents the node's parent
237
	* @param  DOMElement $node <xsl:attribute/> node
238
	* @return void
239
	*/
240 17
	protected function parseXslAttribute(DOMElement $ir, DOMElement $node)
241
	{
242 17
		$attribute = $this->appendElement($ir, 'attribute');
243 17
		$attribute->setAttribute('name', $node->getAttribute('name'));
244 17
		$this->parseChildren($attribute, $node);
245
	}
246
247
	/**
248
	* Parse an <xsl:choose/> node and its <xsl:when/> and <xsl:otherwise/> children into the
249
	* internal representation
250
	*
251
	* @param  DOMElement $ir   Node in the internal representation that represents the node's parent
252
	* @param  DOMElement $node <xsl:choose/> node
253
	* @return void
254
	*/
255 20
	protected function parseXslChoose(DOMElement $ir, DOMElement $node)
256
	{
257 20
		$switch = $this->appendElement($ir, 'switch');
258 20
		foreach ($this->query('./xsl:when', $node) as $when)
259
		{
260
			// Create a <case/> element with the original test condition in @test
261 20
			$case = $this->appendElement($switch, 'case');
262 20
			$case->setAttribute('test', $when->getAttribute('test'));
263 20
			$this->parseChildren($case, $when);
264
		}
265
266
		// Add the default branch, which is presumed to be last
267 20
		foreach ($this->query('./xsl:otherwise', $node) as $otherwise)
268
		{
269 10
			$case = $this->appendElement($switch, 'case');
270 10
			$this->parseChildren($case, $otherwise);
271
272
			// There should be only one <xsl:otherwise/> but we'll break anyway
273 10
			break;
274
		}
275
	}
276
277
	/**
278
	* Parse an <xsl:comment/> node into the internal representation
279
	*
280
	* @param  DOMElement $ir   Node in the internal representation that represents the node's parent
281
	* @param  DOMElement $node <xsl:comment/> node
282
	* @return void
283
	*/
284 3
	protected function parseXslComment(DOMElement $ir, DOMElement $node)
285
	{
286 3
		$comment = $this->appendElement($ir, 'comment');
287 3
		$this->parseChildren($comment, $node);
288
	}
289
290
	/**
291
	* Parse an <xsl:copy-of/> node into the internal representation
292
	*
293
	* NOTE: only attributes are supported
294
	*
295
	* @param  DOMElement $ir   Node in the internal representation that represents the node's parent
296
	* @param  DOMElement $node <xsl:copy-of/> node
297
	* @return void
298
	*/
299 4
	protected function parseXslCopyOf(DOMElement $ir, DOMElement $node)
300
	{
301 4
		$expr = $node->getAttribute('select');
302 4
		if (preg_match('#^@[-\\w]+(?:\\s*\\|\\s*@[-\\w]+)*$#', $expr, $m))
303
		{
304
			// <xsl:copy-of select="@foo"/>
305 2
			$this->appendConditionalAttributes($ir, $expr);
306
		}
307 2
		elseif ($expr === '@*')
308
		{
309
			// <xsl:copy-of select="@*"/>
310 1
			$this->appendElement($ir, 'copyOfAttributes');
311
		}
312
		else
313
		{
314 1
			throw new RuntimeException("Unsupported <xsl:copy-of/> expression '" . $expr . "'");
315
		}
316
	}
317
318
	/**
319
	* Parse an <xsl:element/> node into the internal representation
320
	*
321
	* @param  DOMElement $ir   Node in the internal representation that represents the node's parent
322
	* @param  DOMElement $node <xsl:element/> node
323
	* @return void
324
	*/
325 11
	protected function parseXslElement(DOMElement $ir, DOMElement $node)
326
	{
327 11
		$element = $this->appendElement($ir, 'element');
328 11
		$element->setAttribute('name', $node->getAttribute('name'));
329 11
		$this->parseChildren($element, $node);
330
	}
331
332
	/**
333
	* Parse an <xsl:if/> node into the internal representation
334
	*
335
	* @param  DOMElement $ir   Node in the internal representation that represents the node's parent
336
	* @param  DOMElement $node <xsl:if/> node
337
	* @return void
338
	*/
339 3
	protected function parseXslIf(DOMElement $ir, DOMElement $node)
340
	{
341
		// An <xsl:if/> is represented by a <switch/> with only one <case/>
342 3
		$switch = $this->appendElement($ir, 'switch');
343 3
		$case   = $this->appendElement($switch, 'case');
344 3
		$case->setAttribute('test', $node->getAttribute('test'));
345
346
		// Parse this branch's content
347 3
		$this->parseChildren($case, $node);
348
	}
349
350
	/**
351
	* Parse an <xsl:text/> node into the internal representation
352
	*
353
	* @param  DOMElement $ir   Node in the internal representation that represents the node's parent
354
	* @param  DOMElement $node <xsl:text/> node
355
	* @return void
356
	*/
357 10
	protected function parseXslText(DOMElement $ir, DOMElement $node)
358
	{
359 10
		$this->appendLiteralOutput($ir, $node->textContent);
360 10
		if ($node->getAttribute('disable-output-escaping') === 'yes')
361
		{
362 2
			$ir->lastChild->setAttribute('disable-output-escaping', 'yes');
363
		}
364
	}
365
366
	/**
367
	* Parse an <xsl:value-of/> node into the internal representation
368
	*
369
	* @param  DOMElement $ir   Node in the internal representation that represents the node's parent
370
	* @param  DOMElement $node <xsl:value-of/> node
371
	* @return void
372
	*/
373 6
	protected function parseXslValueOf(DOMElement $ir, DOMElement $node)
374
	{
375 6
		$this->appendXPathOutput($ir, $node->getAttribute('select'));
376 6
		if ($node->getAttribute('disable-output-escaping') === 'yes')
377
		{
378 1
			$ir->lastChild->setAttribute('disable-output-escaping', 'yes');
379
		}
380
	}
381
}