Completed
Push — master ( 2f74e5...c5dbe4 )
by Josh
17:21
created

TemplateHelper::getObjectParamsByRegexp()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
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;
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
98
		// Wrap the XSL in boilerplate code because it might not have a root element
99
		$xsl = '<xsl:stylesheet xmlns:xsl="' . self::XMLNS_XSL . '">'
100
		     . '<xsl:template>'
101
		     . $xsl
102
		     . '</xsl:template>'
103
		     . '</xsl:stylesheet>';
104
105
		$dom = new DOMDocument;
106
		$dom->loadXML($xsl);
107
108
		$xpath = new DOMXPath($dom);
109
110
		// Start by collecting XPath expressions in XSL elements
111
		$query = '//xsl:*/@match | //xsl:*/@select | //xsl:*/@test';
112
		foreach ($xpath->query($query) as $attribute)
113
		{
114
			foreach (XPathHelper::getVariables($attribute->value) as $varName)
115
			{
116
				// Test whether this is the name of a local variable
117
				$varQuery = 'ancestor-or-self::*/'
118
				          . 'preceding-sibling::xsl:variable[@name="' . $varName . '"]';
119
120
				if (!$xpath->query($varQuery, $attribute)->length)
121
				{
122
					$paramNames[] = $varName;
123
				}
124
			}
125
		}
126
127
		// Collecting XPath expressions in attribute value templates
128
		$query = '//*[namespace-uri() != "' . self::XMLNS_XSL . '"]'
129
		       . '/@*[contains(., "{")]';
130
		foreach ($xpath->query($query) as $attribute)
131
		{
132
			$tokens = AVTHelper::parse($attribute->value);
133
134
			foreach ($tokens as $token)
135
			{
136
				if ($token[0] !== 'expression')
137
				{
138
					continue;
139
				}
140
141
				foreach (XPathHelper::getVariables($token[1]) as $varName)
142
				{
143
					// Test whether this is the name of a local variable
144
					$varQuery = 'ancestor-or-self::*/'
145
					          . 'preceding-sibling::xsl:variable[@name="' . $varName . '"]';
146
147
					if (!$xpath->query($varQuery, $attribute)->length)
148
					{
149
						$paramNames[] = $varName;
150
					}
151
				}
152
			}
153
		}
154
155
		// Dedupe and sort names
156
		$paramNames = array_unique($paramNames);
157
		sort($paramNames);
158
159
		return $paramNames;
160
	}
161
162
	/**
163
	* Return all DOMNodes whose content is an URL
164
	*
165
	* NOTE: it will also return HTML4 nodes whose content is an URI
166
	*
167
	* @param  DOMDocument $dom Document
168
	* @return array            Array of DOMNode instances
169
	*/
170
	public static function getURLNodes(DOMDocument $dom)
171
	{
172
		return NodeLocator::getURLNodes($dom);
173
	}
174
175
	/**
176
	* Highlight the source of a node inside of a template
177
	*
178
	* @param  DOMNode $node    Node to highlight
179
	* @param  string  $prepend HTML to prepend
180
	* @param  string  $append  HTML to append
181
	* @return string           Template's source, as HTML
182
	*/
183
	public static function highlightNode(DOMNode $node, $prepend, $append)
184
	{
185
		// Add a unique token to the node
186
		$uniqid = uniqid('_');
187
		if ($node instanceof DOMAttr)
188
		{
189
			$node->value .= $uniqid;
190
		}
191
		elseif ($node instanceof DOMElement)
192
		{
193
			$node->setAttribute($uniqid, '');
194
		}
195
		elseif ($node instanceof DOMCharacterData || $node instanceof DOMProcessingInstruction)
196
		{
197
			$node->data .= $uniqid;
198
		}
199
200
		$dom = $node->ownerDocument;
201
		$dom->formatOutput = true;
202
203
		$docXml = TemplateLoader::innerXML($dom->documentElement);
204
		$docXml = trim(str_replace("\n  ", "\n", $docXml));
205
206
		$nodeHtml = htmlspecialchars(trim($dom->saveXML($node)));
207
		$docHtml  = htmlspecialchars($docXml);
208
209
		// Enclose the node's representation in our hilighting HTML
210
		$html = str_replace($nodeHtml, $prepend . $nodeHtml . $append, $docHtml);
211
212
		// Remove the unique token from HTML and from the node
213
		if ($node instanceof DOMAttr)
214
		{
215
			$node->value = substr($node->value, 0, -strlen($uniqid));
216
			$html = str_replace($uniqid, '', $html);
217
		}
218
		elseif ($node instanceof DOMElement)
219
		{
220
			$node->removeAttribute($uniqid);
221
			$html = str_replace(' ' . $uniqid . '=&quot;&quot;', '', $html);
222
		}
223
		elseif ($node instanceof DOMCharacterData || $node instanceof DOMProcessingInstruction)
224
		{
225
			$node->data .= $uniqid;
226
			$html = str_replace($uniqid, '', $html);
227
		}
228
229
		return $html;
230
	}
231
232
	/**
233
	* Load a template as an xsl:template node
234
	*
235
	* Will attempt to load it as XML first, then as HTML as a fallback. Either way, an xsl:template
236
	* node is returned
237
	*
238
	* @param  string      $template
239
	* @return DOMDocument
240
	*/
241
	public static function loadTemplate($template)
242
	{
243
		return TemplateLoader::load($template);
244
	}
245
246
	/**
247
	* Replace simple templates (in an array, in-place) with a common template
248
	*
249
	* In some situations, renderers can take advantage of multiple tags having the same template. In
250
	* any configuration, there's almost always a number of "simple" tags that are rendered as an
251
	* HTML element of the same name with no HTML attributes. For instance, the system tag "p" used
252
	* for paragraphs, "B" tags used for "b" HTML elements, etc... This method replaces those
253
	* templates with a common template that uses a dynamic element name based on the tag's name,
254
	* either its nodeName or localName depending on whether the tag is namespaced, and normalized to
255
	* lowercase using XPath's translate() function
256
	*
257
	* @param  array<string> &$templates Associative array of [tagName => template]
258
	* @param  integer       $minCount
259
	* @return void
260
	*/
261
	public static function replaceHomogeneousTemplates(array &$templates, $minCount = 3)
262
	{
263
		$tagNames = [];
264
265
		// Prepare the XPath expression used for the element's name
266
		$expr = 'name()';
267
268
		// Identify "simple" tags, whose template is one element of the same name. Their template
269
		// can be replaced with a dynamic template shared by all the simple tags
270
		foreach ($templates as $tagName => $template)
271
		{
272
			// Generate the element name based on the tag's localName, lowercased
273
			$elName = strtolower(preg_replace('/^[^:]+:/', '', $tagName));
274
275
			if ($template === '<' . $elName . '><xsl:apply-templates/></' . $elName . '>')
276
			{
277
				$tagNames[] = $tagName;
278
279
				// Use local-name() if any of the tags are namespaced
280
				if (strpos($tagName, ':') !== false)
281
				{
282
					$expr = 'local-name()';
283
				}
284
			}
285
		}
286
287
		// We only bother replacing their template if there are at least $minCount simple tags.
288
		// Otherwise it only makes the stylesheet bigger
289
		if (count($tagNames) < $minCount)
290
		{
291
			return;
292
		}
293
294
		// Generate a list of uppercase characters from the tags' names
295
		$chars = preg_replace('/[^A-Z]+/', '', count_chars(implode('', $tagNames), 3));
296
297
		if (is_string($chars) && $chars !== '')
298
		{
299
			$expr = 'translate(' . $expr . ",'" . $chars . "','" . strtolower($chars) . "')";
300
		}
301
302
		// Prepare the common template
303
		$template = '<xsl:element name="{' . $expr . '}">'
304
		          . '<xsl:apply-templates/>'
305
		          . '</xsl:element>';
306
307
		// Replace the templates
308
		foreach ($tagNames as $tagName)
309
		{
310
			$templates[$tagName] = $template;
311
		}
312
	}
313
314
	/**
315
	* Replace parts of a template that match given regexp
316
	*
317
	* Treats attribute values as plain text. Replacements within XPath expression is unsupported.
318
	* The callback must return an array with two elements. The first must be either of 'expression',
319
	* 'literal' or 'passthrough', and the second element depends on the first.
320
	*
321
	*  - 'expression' indicates that the replacement must be treated as an XPath expression such as
322
	*    '@foo', which must be passed as the second element.
323
	*  - 'literal' indicates a literal (plain text) replacement, passed as its second element.
324
	*  - 'passthrough' indicates that the replacement should the tag's content. It works differently
325
	*    whether it is inside an attribute's value or a text node. Within an attribute's value, the
326
	*    replacement will be the text content of the tag. Within a text node, the replacement
327
	*    becomes an <xsl:apply-templates/> node.
328
	*
329
	* @param  string   $template Original template
330
	* @param  string   $regexp   Regexp for matching parts that need replacement
331
	* @param  callback $fn       Callback used to get the replacement
332
	* @return string             Processed template
333
	*/
334
	public static function replaceTokens($template, $regexp, $fn)
335
	{
336
		return TemplateModifier::replaceTokens($template, $regexp, $fn);
337
	}
338
339
	/**
340
	* Serialize a loaded template back into a string
341
	*
342
	* NOTE: removes the root node created by loadTemplate()
343
	*
344
	* @param  DOMDocument $dom
345
	* @return string
346
	*/
347
	public static function saveTemplate(DOMDocument $dom)
348
	{
349
		return TemplateLoader::save($dom);
350
	}
351
}