Completed
Push — master ( e6f4a0...ec4877 )
by Josh
18:40
created

TemplateHelper::getParametersFromXSL()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 32
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 32
rs 8.439
c 0
b 0
f 0
cc 5
eloc 15
nc 8
nop 1
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;
9
10
use DOMAttr;
11
use DOMCharacterData;
12
use DOMDocument;
13
use DOMElement;
14
use DOMNode;
15
use DOMProcessingInstruction;
16
use DOMText;
17
use DOMXPath;
18
19
abstract class TemplateHelper
20
{
21
	/**
22
	* XSL namespace
23
	*/
24
	const XMLNS_XSL = 'http://www.w3.org/1999/XSL/Transform';
25
26
	/**
27
	* Return all attributes (literal or generated) that match given regexp
28
	*
29
	* @param  DOMDocument $dom    Document
30
	* @param  string      $regexp Regexp
31
	* @return array               Array of DOMNode instances
32
	*/
33
	public static function getAttributesByRegexp(DOMDocument $dom, $regexp)
34
	{
35
		return NodeLocator::getAttributesByRegexp($dom, $regexp);
36
	}
37
38
	/**
39
	* Return all DOMNodes whose content is CSS
40
	*
41
	* @param  DOMDocument $dom Document
42
	* @return array            Array of DOMNode instances
43
	*/
44
	public static function getCSSNodes(DOMDocument $dom)
45
	{
46
		return NodeLocator::getCSSNodes($dom);
47
	}
48
49
	/**
50
	* Return all elements (literal or generated) that match given regexp
51
	*
52
	* @param  DOMDocument $dom    Document
53
	* @param  string      $regexp Regexp
54
	* @return array               Array of DOMNode instances
55
	*/
56
	public static function getElementsByRegexp(DOMDocument $dom, $regexp)
57
	{
58
		return NodeLocator::getElementsByRegexp($dom, $regexp);
59
	}
60
61
	/**
62
	* Return all DOMNodes whose content is JavaScript
63
	*
64
	* @param  DOMDocument $dom Document
65
	* @return array            Array of DOMNode instances
66
	*/
67
	public static function getJSNodes(DOMDocument $dom)
68
	{
69
		return NodeLocator::getJSNodes($dom);
70
	}
71
72
	/**
73
	* Return all elements (literal or generated) that match given regexp
74
	*
75
	* Will return all <param/> descendants of <object/> and all attributes of <embed/> whose name
76
	* matches given regexp. This method will NOT catch <param/> elements whose 'name' attribute is
77
	* set via an <xsl:attribute/>
78
	*
79
	* @param  DOMDocument $dom    Document
80
	* @param  string      $regexp
81
	* @return array               Array of DOMNode instances
82
	*/
83
	public static function getObjectParamsByRegexp(DOMDocument $dom, $regexp)
84
	{
85
		return NodeLocator::getObjectParamsByRegexp($dom, $regexp);
86
	}
87
88
	/**
89
	* Return a list of parameters in use in given XSL
90
	*
91
	* @param  string $xsl XSL source
92
	* @return array       Alphabetically sorted list of unique parameter names
93
	*/
94
	public static function getParametersFromXSL($xsl)
95
	{
96
		$paramNames = [];
97
		$xpath      = new DOMXPath(TemplateLoader::load($xsl));
98
99
		// Start by collecting XPath expressions in XSL elements
100
		$query = '//xsl:*/@match | //xsl:*/@select | //xsl:*/@test';
101
		foreach ($xpath->query($query) as $attribute)
102
		{
103
			$expr        = $attribute->value;
104
			$paramNames += array_flip(self::getParametersFromExpression($attribute, $expr));
105
		}
106
107
		// Collect XPath expressions in attribute value templates
108
		$query = '//*[namespace-uri() != "' . self::XMLNS_XSL . '"]/@*[contains(., "{")]';
109
		foreach ($xpath->query($query) as $attribute)
110
		{
111
			foreach (AVTHelper::parse($attribute->value) as $token)
112
			{
113
				if ($token[0] === 'expression')
114
				{
115
					$expr        = $token[1];
116
					$paramNames += array_flip(self::getParametersFromExpression($attribute, $expr));
117
				}
118
			}
119
		}
120
121
		// Sort the parameter names and return them in a list
122
		ksort($paramNames);
123
124
		return array_keys($paramNames);
125
	}
126
127
	/**
128
	* Return all DOMNodes whose content is an URL
129
	*
130
	* NOTE: it will also return HTML4 nodes whose content is an URI
131
	*
132
	* @param  DOMDocument $dom Document
133
	* @return array            Array of DOMNode instances
134
	*/
135
	public static function getURLNodes(DOMDocument $dom)
136
	{
137
		return NodeLocator::getURLNodes($dom);
138
	}
139
140
	/**
141
	* Highlight the source of a node inside of a template
142
	*
143
	* @param  DOMNode $node    Node to highlight
144
	* @param  string  $prepend HTML to prepend
145
	* @param  string  $append  HTML to append
146
	* @return string           Template's source, as HTML
147
	*/
148
	public static function highlightNode(DOMNode $node, $prepend, $append)
149
	{
150
		// Add a unique token to the node
151
		$uniqid = uniqid('_');
152
		if ($node instanceof DOMAttr)
153
		{
154
			$node->value .= $uniqid;
155
		}
156
		elseif ($node instanceof DOMElement)
157
		{
158
			$node->setAttribute($uniqid, '');
159
		}
160
		elseif ($node instanceof DOMCharacterData || $node instanceof DOMProcessingInstruction)
161
		{
162
			$node->data .= $uniqid;
163
		}
164
165
		$dom = $node->ownerDocument;
166
		$dom->formatOutput = true;
167
168
		$docXml = TemplateLoader::innerXML($dom->documentElement);
169
		$docXml = trim(str_replace("\n  ", "\n", $docXml));
170
171
		$nodeHtml = htmlspecialchars(trim($dom->saveXML($node)));
172
		$docHtml  = htmlspecialchars($docXml);
173
174
		// Enclose the node's representation in our hilighting HTML
175
		$html = str_replace($nodeHtml, $prepend . $nodeHtml . $append, $docHtml);
176
177
		// Remove the unique token from HTML and from the node
178
		if ($node instanceof DOMAttr)
179
		{
180
			$node->value = substr($node->value, 0, -strlen($uniqid));
181
			$html = str_replace($uniqid, '', $html);
182
		}
183
		elseif ($node instanceof DOMElement)
184
		{
185
			$node->removeAttribute($uniqid);
186
			$html = str_replace(' ' . $uniqid . '=&quot;&quot;', '', $html);
187
		}
188
		elseif ($node instanceof DOMCharacterData || $node instanceof DOMProcessingInstruction)
189
		{
190
			$node->data .= $uniqid;
191
			$html = str_replace($uniqid, '', $html);
192
		}
193
194
		return $html;
195
	}
196
197
	/**
198
	* Load a template as an xsl:template node
199
	*
200
	* Will attempt to load it as XML first, then as HTML as a fallback. Either way, an xsl:template
201
	* node is returned
202
	*
203
	* @param  string      $template
204
	* @return DOMDocument
205
	*/
206
	public static function loadTemplate($template)
207
	{
208
		return TemplateLoader::load($template);
209
	}
210
211
	/**
212
	* Replace simple templates (in an array, in-place) with a common template
213
	*
214
	* In some situations, renderers can take advantage of multiple tags having the same template. In
215
	* any configuration, there's almost always a number of "simple" tags that are rendered as an
216
	* HTML element of the same name with no HTML attributes. For instance, the system tag "p" used
217
	* for paragraphs, "B" tags used for "b" HTML elements, etc... This method replaces those
218
	* templates with a common template that uses a dynamic element name based on the tag's name,
219
	* either its nodeName or localName depending on whether the tag is namespaced, and normalized to
220
	* lowercase using XPath's translate() function
221
	*
222
	* @param  array<string> &$templates Associative array of [tagName => template]
223
	* @param  integer       $minCount
224
	* @return void
225
	*/
226
	public static function replaceHomogeneousTemplates(array &$templates, $minCount = 3)
227
	{
228
		// Prepare the XPath expression used for the element's name
229
		$expr = 'name()';
230
231
		// Identify "simple" tags, whose template is one element of the same name. Their template
232
		// can be replaced with a dynamic template shared by all the simple tags
233
		$tagNames = [];
234
		foreach ($templates as $tagName => $template)
235
		{
236
			// Generate the element name based on the tag's localName, lowercased
237
			$elName = strtolower(preg_replace('/^[^:]+:/', '', $tagName));
238
			if ($template === '<' . $elName . '><xsl:apply-templates/></' . $elName . '>')
239
			{
240
				$tagNames[] = $tagName;
241
242
				// Use local-name() if any of the tags are namespaced
243
				if (strpos($tagName, ':') !== false)
244
				{
245
					$expr = 'local-name()';
246
				}
247
			}
248
		}
249
250
		// We only bother replacing their template if there are at least $minCount simple tags.
251
		// Otherwise it only makes the stylesheet bigger
252
		if (count($tagNames) < $minCount)
253
		{
254
			return;
255
		}
256
257
		// Generate a list of uppercase characters from the tags' names
258
		$chars = preg_replace('/[^A-Z]+/', '', count_chars(implode('', $tagNames), 3));
259
		if ($chars > '')
260
		{
261
			$expr = 'translate(' . $expr . ",'" . $chars . "','" . strtolower($chars) . "')";
262
		}
263
264
		// Prepare the common template
265
		$template = '<xsl:element name="{' . $expr . '}"><xsl:apply-templates/></xsl:element>';
266
267
		// Replace the templates
268
		foreach ($tagNames as $tagName)
269
		{
270
			$templates[$tagName] = $template;
271
		}
272
	}
273
274
	/**
275
	* Replace parts of a template that match given regexp
276
	*
277
	* Treats attribute values as plain text. Replacements within XPath expression is unsupported.
278
	* The callback must return an array with two elements. The first must be either of 'expression',
279
	* 'literal' or 'passthrough', and the second element depends on the first.
280
	*
281
	*  - 'expression' indicates that the replacement must be treated as an XPath expression such as
282
	*    '@foo', which must be passed as the second element.
283
	*  - 'literal' indicates a literal (plain text) replacement, passed as its second element.
284
	*  - 'passthrough' indicates that the replacement should the tag's content. It works differently
285
	*    whether it is inside an attribute's value or a text node. Within an attribute's value, the
286
	*    replacement will be the text content of the tag. Within a text node, the replacement
287
	*    becomes an <xsl:apply-templates/> node.
288
	*
289
	* @param  string   $template Original template
290
	* @param  string   $regexp   Regexp for matching parts that need replacement
291
	* @param  callback $fn       Callback used to get the replacement
292
	* @return string             Processed template
293
	*/
294
	public static function replaceTokens($template, $regexp, $fn)
295
	{
296
		return TemplateModifier::replaceTokens($template, $regexp, $fn);
297
	}
298
299
	/**
300
	* Serialize a loaded template back into a string
301
	*
302
	* NOTE: removes the root node created by loadTemplate()
303
	*
304
	* @param  DOMDocument $dom
305
	* @return string
306
	*/
307
	public static function saveTemplate(DOMDocument $dom)
308
	{
309
		return TemplateLoader::save($dom);
310
	}
311
312
	/**
313
	* Get a list of parameters from given XPath expression
314
	*
315
	* @param  DOMNode  $node Context node
316
	* @param  string   $expr XPath expression
317
	* @return string[]
318
	*/
319
	protected static function getParametersFromExpression(DOMNode $node, $expr)
320
	{
321
		$varNames   = XPathHelper::getVariables($expr);
322
		$paramNames = [];
323
		$xpath      = new DOMXPath($node->ownerDocument);
324
		foreach ($varNames as $name)
325
		{
326
			// Test whether this is the name of a local variable
327
			$query = 'ancestor-or-self::*/preceding-sibling::xsl:variable[@name="' . $name . '"]';
328
			if (!$xpath->query($query, $node)->length)
329
			{
330
				$paramNames[] = $name;
331
			}
332
		}
333
334
		return $paramNames;
335
	}
336
}