Completed
Push — master ( daf725...101f73 )
by Josh
03:56
created

TemplateHelper::getJSNodes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
/**
4
* @package   s9e\TextFormatter
5
* @copyright Copyright (c) 2010-2019 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 DOMNode[]        List of DOMNode instances
43
	*/
44 10
	public static function getCSSNodes(DOMDocument $dom)
45
	{
46 10
		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 1
	public static function getElementsByRegexp(DOMDocument $dom, $regexp)
57
	{
58 1
		return NodeLocator::getElementsByRegexp($dom, $regexp);
59
	}
60
61
	/**
62
	* Return all DOMNodes whose content is JavaScript
63
	*
64
	* @param  DOMDocument $dom Document
65
	* @return DOMNode[]        List of DOMNode instances
66
	*/
67 11
	public static function getJSNodes(DOMDocument $dom)
68
	{
69 11
		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 8
	public static function getObjectParamsByRegexp(DOMDocument $dom, $regexp)
84
	{
85 8
		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 9
	public static function getParametersFromXSL($xsl)
95
	{
96 9
		$paramNames = [];
97 9
		$xpath      = new DOMXPath(TemplateLoader::load($xsl));
98
99
		// Start by collecting XPath expressions in XSL elements
100 9
		$query = '//xsl:*/@match | //xsl:*/@select | //xsl:*/@test';
101 9
		foreach ($xpath->query($query) as $attribute)
102
		{
103 6
			$expr        = $attribute->value;
104 6
			$paramNames += array_flip(self::getParametersFromExpression($attribute, $expr));
105
		}
106
107
		// Collect XPath expressions in attribute value templates
108 9
		$query = '//*[namespace-uri() != "' . self::XMLNS_XSL . '"]/@*[contains(., "{")]';
109 9
		foreach ($xpath->query($query) as $attribute)
110
		{
111 4
			foreach (AVTHelper::parse($attribute->value) as $token)
112
			{
113 4
				if ($token[0] === 'expression')
114
				{
115 4
					$expr        = $token[1];
116 4
					$paramNames += array_flip(self::getParametersFromExpression($attribute, $expr));
117
				}
118
			}
119
		}
120
121
		// Sort the parameter names and return them in a list
122 9
		ksort($paramNames);
123
124 9
		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 DOMNode[]        List of DOMNode instances
134
	*/
135 22
	public static function getURLNodes(DOMDocument $dom)
136
	{
137 22
		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 10
	public static function highlightNode(DOMNode $node, $prepend, $append)
149
	{
150
		// Create a copy of the document that we can modify without side effects
151 10
		$dom = $node->ownerDocument->cloneNode(true);
152 10
		$dom->formatOutput = true;
153
154 10
		$xpath = new DOMXPath($dom);
155 10
		$node  = $xpath->query($node->getNodePath())->item(0);
156
157
		// Add a unique token to the node
158 10
		$uniqid = uniqid('_');
159 10
		if ($node instanceof DOMAttr)
160
		{
161 2
			$node->value .= $uniqid;
162
		}
163 8
		elseif ($node instanceof DOMElement)
164
		{
165 2
			$node->setAttribute($uniqid, '');
166
		}
167 6
		elseif ($node instanceof DOMCharacterData || $node instanceof DOMProcessingInstruction)
168
		{
169 6
			$node->data .= $uniqid;
170
		}
171
172 10
		$docXml = TemplateLoader::innerXML($dom->documentElement);
173 10
		$docXml = trim(str_replace("\n  ", "\n", $docXml));
174
175 10
		$nodeHtml = htmlspecialchars(trim($dom->saveXML($node)));
176 10
		$docHtml  = htmlspecialchars($docXml);
177
178
		// Enclose the node's representation in our highlighting HTML
179 10
		$html = str_replace($nodeHtml, $prepend . $nodeHtml . $append, $docHtml);
180
181
		// Remove the unique token from HTML
182 10
		$html = str_replace(' ' . $uniqid . '=&quot;&quot;', '', $html);
183 10
		$html = str_replace($uniqid, '', $html);
184
185 10
		return $html;
186
	}
187
188
	/**
189
	* Load a template as an xsl:template node
190
	*
191
	* Will attempt to load it as XML first, then as HTML as a fallback. Either way, an xsl:template
192
	* node is returned
193
	*
194
	* @param  string      $template
195
	* @return DOMDocument
196
	*/
197 11
	public static function loadTemplate($template)
198
	{
199 11
		return TemplateLoader::load($template);
200
	}
201
202
	/**
203
	* Replace simple templates (in an array, in-place) with a common template
204
	*
205
	* In some situations, renderers can take advantage of multiple tags having the same template. In
206
	* any configuration, there's almost always a number of "simple" tags that are rendered as an
207
	* HTML element of the same name with no HTML attributes. For instance, the system tag "p" used
208
	* for paragraphs, "B" tags used for "b" HTML elements, etc... This method replaces those
209
	* templates with a common template that uses a dynamic element name based on the tag's name,
210
	* either its nodeName or localName depending on whether the tag is namespaced, and normalized to
211
	* lowercase using XPath's translate() function
212
	*
213
	* @param  array<string> &$templates Associative array of [tagName => template]
214
	* @param  integer       $minCount
215
	* @return void
216
	*/
217 6
	public static function replaceHomogeneousTemplates(array &$templates, $minCount = 3)
218
	{
219
		// Prepare the XPath expression used for the element's name
220 6
		$expr = 'name()';
221
222
		// Identify "simple" tags, whose template is one element of the same name. Their template
223
		// can be replaced with a dynamic template shared by all the simple tags
224 6
		$tagNames = [];
225 6
		foreach ($templates as $tagName => $template)
226
		{
227
			// Generate the element name based on the tag's localName, lowercased
228 6
			$elName = strtolower(preg_replace('/^[^:]+:/', '', $tagName));
229 6
			if ($template === '<' . $elName . '><xsl:apply-templates/></' . $elName . '>')
230
			{
231 6
				$tagNames[] = $tagName;
232
233
				// Use local-name() if any of the tags are namespaced
234 6
				if (strpos($tagName, ':') !== false)
235
				{
236 2
					$expr = 'local-name()';
237
				}
238
			}
239
		}
240
241
		// We only bother replacing their template if there are at least $minCount simple tags.
242
		// Otherwise it only makes the stylesheet bigger
243 6
		if (count($tagNames) < $minCount)
244
		{
245 1
			return;
246
		}
247
248
		// Generate a list of uppercase characters from the tags' names
249 5
		$chars = preg_replace('/[^A-Z]+/', '', count_chars(implode('', $tagNames), 3));
250 5
		if ($chars > '')
251
		{
252 2
			$expr = 'translate(' . $expr . ",'" . $chars . "','" . strtolower($chars) . "')";
253
		}
254
255
		// Prepare the common template
256 5
		$template = '<xsl:element name="{' . $expr . '}"><xsl:apply-templates/></xsl:element>';
257
258
		// Replace the templates
259 5
		foreach ($tagNames as $tagName)
260
		{
261 5
			$templates[$tagName] = $template;
262
		}
263
	}
264
265
	/**
266
	* Replace parts of a template that match given regexp
267
	*
268
	* Treats attribute values as plain text. Replacements within XPath expression is unsupported.
269
	* The callback must return an array with two elements. The first must be either of 'expression',
270
	* 'literal' or 'passthrough', and the second element depends on the first.
271
	*
272
	*  - 'expression' indicates that the replacement must be treated as an XPath expression such as
273
	*    '@foo', which must be passed as the second element.
274
	*  - 'literal' indicates a literal (plain text) replacement, passed as its second element.
275
	*  - 'passthrough' indicates that the replacement should the tag's content. It works differently
276
	*    whether it is inside an attribute's value or a text node. Within an attribute's value, the
277
	*    replacement will be the text content of the tag. Within a text node, the replacement
278
	*    becomes an <xsl:apply-templates/> node.
279
	*
280
	* @param  string   $template Original template
281
	* @param  string   $regexp   Regexp for matching parts that need replacement
282
	* @param  callback $fn       Callback used to get the replacement
283
	* @return string             Processed template
284
	*/
285 15
	public static function replaceTokens($template, $regexp, $fn)
286
	{
287 15
		return TemplateModifier::replaceTokens($template, $regexp, $fn);
288
	}
289
290
	/**
291
	* Serialize a loaded template back into a string
292
	*
293
	* NOTE: removes the root node created by loadTemplate()
294
	*
295
	* @param  DOMDocument $dom
296
	* @return string
297
	*/
298 1
	public static function saveTemplate(DOMDocument $dom)
299
	{
300 1
		return TemplateLoader::save($dom);
301
	}
302
303
	/**
304
	* Get a list of parameters from given XPath expression
305
	*
306
	* @param  DOMNode  $node Context node
307
	* @param  string   $expr XPath expression
308
	* @return string[]
309
	*/
310 8
	protected static function getParametersFromExpression(DOMNode $node, $expr)
311
	{
312 8
		$varNames   = XPathHelper::getVariables($expr);
313 8
		$paramNames = [];
314 8
		$xpath      = new DOMXPath($node->ownerDocument);
315 8
		foreach ($varNames as $name)
316
		{
317
			// Test whether this is the name of a local variable
318 8
			$query = 'ancestor-or-self::*/preceding-sibling::xsl:variable[@name="' . $name . '"]';
319 8
			if (!$xpath->query($query, $node)->length)
320
			{
321 7
				$paramNames[] = $name;
322
			}
323
		}
324
325 8
		return $paramNames;
326
	}
327
}