Completed
Push — master ( 25559c...472c56 )
by Josh
20:25
created

Parser::appendAVT()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 14
rs 9.4285
cc 3
eloc 6
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
98
	* @return void
99
	*/
100
	protected function appendXPathOutput(DOMElement $parentNode, $expr)
101
	{
102
		$this->appendElement($parentNode, 'output', htmlspecialchars(trim($expr)))
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
}