Completed
Branch TemplateParserRefactor (06d644)
by Josh
14:21
created

Parser::parseXslCopyOf()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 32
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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