Completed
Branch TemplateParserRefactor (c8b347)
by Josh
14:10
created

Parser::parseXslCopyOf()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 32
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 32
rs 8.8571
cc 3
eloc 14
nc 3
nop 2
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 DOMXPath;
13
use RuntimeException;
14
use s9e\TextFormatter\Configurator\Helpers\AVTHelper;
15
use s9e\TextFormatter\Configurator\Helpers\TemplateHelper;
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
	public function __construct(Normalizer $normalizer)
29
	{
30
		$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
	public function parse($template)
40
	{
41
		$dom = TemplateHelper::loadTemplate($template);
42
43
		$ir = new DOMDocument;
44
		$ir->loadXML('<template/>');
45
46
		$this->createXPath($dom);
47
		$this->parseChildren($ir->documentElement, $dom->documentElement);
48
		$this->normalizer->normalize($ir);
49
50
		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
	protected function appendAVT(DOMElement $parentNode, $avt)
61
	{
62
		foreach (AVTHelper::parse($avt) as $token)
63
		{
64
			if ($token[0] === 'expression')
65
			{
66
				$this->appendXPathOutput($parentNode, $token[1]);
67
			}
68
			else
69
			{
70
				$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
	protected function appendLiteralOutput(DOMElement $parentNode, $content)
83
	{
84
		if ($content === '')
85
		{
86
			return;
87
		}
88
89
		$this->appendElement($parentNode, 'output', htmlspecialchars($content))
90
		     ->setAttribute('type', 'literal');
91
	}
92
93
	/**
94
	* Append an <output/> element for given XPath expression to given node
95
	*
96
	* @param  DOMElement $parentNode Parent node
97
	* @param  string     $expr       XPath expression
0 ignored issues
show
Bug introduced by
There is no parameter named $expr. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
98
	* @return void
99
	*/
100
	protected function appendXPathOutput(DOMElement $parentNode, $content)
101
	{
102
		$this->appendElement($parentNode, 'output', htmlspecialchars(trim($content)))
103
		     ->setAttribute('type', 'xpath');
104
	}
105
106
	/**
107
	* Parse all the children of a given element
108
	*
109
	* @param  DOMElement $ir     Node in the internal representation that represents the parent node
110
	* @param  DOMElement $parent Parent node
111
	* @return void
112
	*/
113
	protected function parseChildren(DOMElement $ir, DOMElement $parent)
114
	{
115
		foreach ($parent->childNodes as $child)
116
		{
117
			switch ($child->nodeType)
118
			{
119
				case XML_COMMENT_NODE:
120
					// Do nothing
121
					break;
122
123
				case XML_TEXT_NODE:
124
					if (trim($child->textContent) !== '')
125
					{
126
						$this->appendLiteralOutput($ir, $child->textContent);
127
					}
128
					break;
129
130
				case XML_ELEMENT_NODE:
131
					$this->parseNode($ir, $child);
132
					break;
133
134
				default:
135
					throw new RuntimeException("Cannot parse node '" . $child->nodeName . "''");
136
			}
137
		}
138
	}
139
140
	/**
141
	* Parse a given node into the internal representation
142
	*
143
	* @param  DOMElement $ir   Node in the internal representation that represents the node's parent
144
	* @param  DOMElement $node Node to parse
145
	* @return void
146
	*/
147
	protected function parseNode(DOMElement $ir, DOMElement $node)
148
	{
149
		// XSL elements are parsed by the corresponding parseXsl* method
150
		if ($node->namespaceURI === self::XMLNS_XSL)
151
		{
152
			$methodName = 'parseXsl' . str_replace(' ', '', ucwords(str_replace('-', ' ', $node->localName)));
153
			if (!method_exists($this, $methodName))
154
			{
155
				throw new RuntimeException("Element '" . $node->nodeName . "' is not supported");
156
			}
157
158
			return $this->$methodName($ir, $node);
159
		}
160
161
		// Create an <element/> with a name attribute equal to given node's name
162
		$element = $this->appendElement($ir, 'element');
163
		$element->setAttribute('name', $node->nodeName);
164
165
		// Append an <attribute/> element for each namespace declaration
166
		$xpath = new DOMXPath($node->ownerDocument);
167
		foreach ($xpath->query('namespace::*', $node) as $ns)
168
		{
169
			if ($node->hasAttribute($ns->nodeName))
170
			{
171
				$irAttribute = $this->appendElement($element, 'attribute');
172
				$irAttribute->setAttribute('name', $ns->nodeName);
173
				$this->appendLiteralOutput($irAttribute, $ns->nodeValue);
174
			}
175
		}
176
177
		// Append an <attribute/> element for each of this node's attribute
178
		foreach ($node->attributes as $attribute)
179
		{
180
			$irAttribute = $this->appendElement($element, 'attribute');
181
			$irAttribute->setAttribute('name', $attribute->nodeName);
182
183
			// Append an <output/> element to represent the attribute's value
184
			$this->appendAVT($irAttribute, $attribute->value);
185
		}
186
187
		// Parse the content of this node
188
		$this->parseChildren($element, $node);
189
	}
190
191
	/**
192
	* Parse an <xsl:apply-templates/> node into the internal representation
193
	*
194
	* @param  DOMElement $ir   Node in the internal representation that represents the node's parent
195
	* @param  DOMElement $node <xsl:apply-templates/> node
196
	* @return void
197
	*/
198
	protected function parseXslApplyTemplates(DOMElement $ir, DOMElement $node)
199
	{
200
		$applyTemplates = $this->appendElement($ir, 'applyTemplates');
201
		if ($node->hasAttribute('select'))
202
		{
203
			$applyTemplates->setAttribute('select', $node->getAttribute('select'));
204
		}
205
	}
206
207
	/**
208
	* Parse an <xsl:attribute/> node into the internal representation
209
	*
210
	* @param  DOMElement $ir   Node in the internal representation that represents the node's parent
211
	* @param  DOMElement $node <xsl:attribute/> node
212
	* @return void
213
	*/
214
	protected function parseXslAttribute(DOMElement $ir, DOMElement $node)
215
	{
216
		$attribute = $this->appendElement($ir, 'attribute');
217
		$attribute->setAttribute('name', $node->getAttribute('name'));
218
		$this->parseChildren($attribute, $node);
219
	}
220
221
	/**
222
	* Parse an <xsl:choose/> node and its <xsl:when/> and <xsl:otherwise/> children into the
223
	* internal representation
224
	*
225
	* @param  DOMElement $ir   Node in the internal representation that represents the node's parent
226
	* @param  DOMElement $node <xsl:choose/> node
227
	* @return void
228
	*/
229
	protected function parseXslChoose(DOMElement $ir, DOMElement $node)
230
	{
231
		$switch = $this->appendElement($ir, 'switch');
232
		foreach ($this->query('./xsl:when', $node) as $when)
233
		{
234
			// Create a <case/> element with the original test condition in @test
235
			$case = $this->appendElement($switch, 'case');
236
			$case->setAttribute('test', $when->getAttribute('test'));
237
			$this->parseChildren($case, $when);
238
		}
239
240
		// Add the default branch, which is presumed to be last
241
		foreach ($this->query('./xsl:otherwise', $node) as $otherwise)
242
		{
243
			$case = $this->appendElement($switch, 'case');
244
			$this->parseChildren($case, $otherwise);
245
246
			// There should be only one <xsl:otherwise/> but we'll break anyway
247
			break;
248
		}
249
	}
250
251
	/**
252
	* Parse an <xsl:comment/> node into the internal representation
253
	*
254
	* @param  DOMElement $ir   Node in the internal representation that represents the node's parent
255
	* @param  DOMElement $node <xsl:comment/> node
256
	* @return void
257
	*/
258
	protected function parseXslComment(DOMElement $ir, DOMElement $node)
259
	{
260
		$comment = $this->appendElement($ir, 'comment');
261
		$this->parseChildren($comment, $node);
262
	}
263
264
	/**
265
	* Parse an <xsl:copy-of/> node into the internal representation
266
	*
267
	* NOTE: only attributes are supported
268
	*
269
	* @param  DOMElement $ir   Node in the internal representation that represents the node's parent
270
	* @param  DOMElement $node <xsl:copy-of/> node
271
	* @return void
272
	*/
273
	protected function parseXslCopyOf(DOMElement $ir, DOMElement $node)
274
	{
275
		$expr = $node->getAttribute('select');
276
277
		// <xsl:copy-of select="@foo"/>
278
		if (preg_match('#^@([-\\w]+)$#', $expr, $m))
279
		{
280
			// Create a switch element in the IR
281
			$switch = $this->appendElement($ir, 'switch');
282
			$case   = $this->appendElement($switch, 'case');
283
			$case->setAttribute('test', $expr);
284
285
			// Append an attribute element
286
			$attribute = $this->appendElement($case, 'attribute');
287
			$attribute->setAttribute('name', $m[1]);
288
289
			// Set the attribute's content, which is simply the copied attribute's value
290
			$this->appendXPathOutput($attribute, $expr);
291
292
			return;
293
		}
294
295
		// <xsl:copy-of select="@*"/>
296
		if ($expr === '@*')
297
		{
298
			$this->appendElement($ir, 'copyOfAttributes');
299
300
			return;
301
		}
302
303
		throw new RuntimeException("Unsupported <xsl:copy-of/> expression '" . $expr . "'");
304
	}
305
306
	/**
307
	* Parse an <xsl:element/> node into the internal representation
308
	*
309
	* @param  DOMElement $ir   Node in the internal representation that represents the node's parent
310
	* @param  DOMElement $node <xsl:element/> node
311
	* @return void
312
	*/
313
	protected function parseXslElement(DOMElement $ir, DOMElement $node)
314
	{
315
		$element = $this->appendElement($ir, 'element');
316
		$element->setAttribute('name', $node->getAttribute('name'));
317
		$this->parseChildren($element, $node);
318
	}
319
320
	/**
321
	* Parse an <xsl:if/> node into the internal representation
322
	*
323
	* @param  DOMElement $ir   Node in the internal representation that represents the node's parent
324
	* @param  DOMElement $node <xsl:if/> node
325
	* @return void
326
	*/
327
	protected function parseXslIf(DOMElement $ir, DOMElement $node)
328
	{
329
		// An <xsl:if/> is represented by a <switch/> with only one <case/>
330
		$switch = $this->appendElement($ir, 'switch');
331
		$case   = $this->appendElement($switch, 'case');
332
		$case->setAttribute('test', $node->getAttribute('test'));
333
334
		// Parse this branch's content
335
		$this->parseChildren($case, $node);
336
	}
337
338
	/**
339
	* Parse an <xsl:text/> node into the internal representation
340
	*
341
	* @param  DOMElement $ir   Node in the internal representation that represents the node's parent
342
	* @param  DOMElement $node <xsl:text/> node
343
	* @return void
344
	*/
345
	protected function parseXslText(DOMElement $ir, DOMElement $node)
346
	{
347
		$this->appendLiteralOutput($ir, $node->textContent);
348
		if ($node->getAttribute('disable-output-escaping') === 'yes')
349
		{
350
			$ir->lastChild->setAttribute('disable-output-escaping', 'yes');
351
		}
352
	}
353
354
	/**
355
	* Parse an <xsl:value-of/> node into the internal representation
356
	*
357
	* @param  DOMElement $ir   Node in the internal representation that represents the node's parent
358
	* @param  DOMElement $node <xsl:value-of/> node
359
	* @return void
360
	*/
361
	protected function parseXslValueOf(DOMElement $ir, DOMElement $node)
362
	{
363
		$this->appendXPathOutput($ir, $node->getAttribute('select'));
364
		if ($node->getAttribute('disable-output-escaping') === 'yes')
365
		{
366
			$ir->lastChild->setAttribute('disable-output-escaping', 'yes');
367
		}
368
	}
369
}