Completed
Push — master ( e0c943...40adb2 )
by Josh
04:05
created

TemplateHelper::getParametersFromXSL()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 32
ccs 15
cts 15
cp 1
rs 9.0968
c 0
b 0
f 0
cc 5
nc 8
nop 1
crap 5
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 DOMNodes whose content is JavaScript
28
	*
29
	* @param  DOMDocument $dom Document
30
	* @return DOMNode[]        List of DOMNode instances
31
	*/
32
	public static function getJSNodes(DOMDocument $dom)
33
	{
34
		return NodeLocator::getJSNodes($dom);
35
	}
36
37
	/**
38
	* Return a list of parameters in use in given XSL
39
	*
40
	* @param  string $xsl XSL source
41
	* @return array       Alphabetically sorted list of unique parameter names
42
	*/
43 9
	public static function getParametersFromXSL($xsl)
44
	{
45 9
		$paramNames = [];
46 9
		$xpath      = new DOMXPath(TemplateLoader::load($xsl));
47
48
		// Start by collecting XPath expressions in XSL elements
49 9
		$query = '//xsl:*/@match | //xsl:*/@select | //xsl:*/@test';
50 9
		foreach ($xpath->query($query) as $attribute)
51
		{
52 6
			$expr        = $attribute->value;
53 6
			$paramNames += array_flip(self::getParametersFromExpression($attribute, $expr));
54
		}
55
56
		// Collect XPath expressions in attribute value templates
57 9
		$query = '//*[namespace-uri() != "' . self::XMLNS_XSL . '"]/@*[contains(., "{")]';
58 9
		foreach ($xpath->query($query) as $attribute)
59
		{
60 4
			foreach (AVTHelper::parse($attribute->value) as $token)
61
			{
62 4
				if ($token[0] === 'expression')
63
				{
64 4
					$expr        = $token[1];
65 4
					$paramNames += array_flip(self::getParametersFromExpression($attribute, $expr));
66
				}
67
			}
68
		}
69
70
		// Sort the parameter names and return them in a list
71 9
		ksort($paramNames);
72
73 9
		return array_keys($paramNames);
74
	}
75
76
	/**
77
	* Return all DOMNodes whose content is an URL
78
	*
79
	* NOTE: it will also return HTML4 nodes whose content is an URI
80
	*
81
	* @param  DOMDocument $dom Document
82
	* @return DOMNode[]        List of DOMNode instances
83
	*/
84
	public static function getURLNodes(DOMDocument $dom)
85
	{
86
		return NodeLocator::getURLNodes($dom);
87
	}
88
89
	/**
90
	* Highlight the source of a node inside of a template
91
	*
92
	* @param  DOMNode $node    Node to highlight
93
	* @param  string  $prepend HTML to prepend
94
	* @param  string  $append  HTML to append
95
	* @return string           Template's source, as HTML
96
	*/
97 10
	public static function highlightNode(DOMNode $node, $prepend, $append)
98
	{
99
		// Create a copy of the document that we can modify without side effects
100 10
		$dom = $node->ownerDocument->cloneNode(true);
101 10
		$dom->formatOutput = true;
102
103 10
		$xpath = new DOMXPath($dom);
104 10
		$node  = $xpath->query($node->getNodePath())->item(0);
105
106
		// Add a unique token to the node
107 10
		$uniqid = uniqid('_');
108 10
		if ($node instanceof DOMAttr)
109
		{
110 2
			$node->value .= $uniqid;
111
		}
112 8
		elseif ($node instanceof DOMElement)
113
		{
114 2
			$node->setAttribute($uniqid, '');
115
		}
116 6
		elseif ($node instanceof DOMCharacterData || $node instanceof DOMProcessingInstruction)
117
		{
118 6
			$node->data .= $uniqid;
119
		}
120
121 10
		$docXml = TemplateLoader::innerXML($dom->documentElement);
122 10
		$docXml = trim(str_replace("\n  ", "\n", $docXml));
123
124 10
		$nodeHtml = htmlspecialchars(trim($dom->saveXML($node)));
125 10
		$docHtml  = htmlspecialchars($docXml);
126
127
		// Enclose the node's representation in our highlighting HTML
128 10
		$html = str_replace($nodeHtml, $prepend . $nodeHtml . $append, $docHtml);
129
130
		// Remove the unique token from HTML
131 10
		$html = str_replace(' ' . $uniqid . '=&quot;&quot;', '', $html);
132 10
		$html = str_replace($uniqid, '', $html);
133
134 10
		return $html;
135
	}
136
137
	/**
138
	* Load a template as an xsl:template node
139
	*
140
	* Will attempt to load it as XML first, then as HTML as a fallback. Either way, an xsl:template
141
	* node is returned
142
	*
143
	* @param  string      $template
144
	* @return DOMDocument
145
	*/
146
	public static function loadTemplate($template)
147
	{
148
		return TemplateLoader::load($template);
149
	}
150
151
	/**
152
	* Replace simple templates (in an array, in-place) with a common template
153
	*
154
	* In some situations, renderers can take advantage of multiple tags having the same template. In
155
	* any configuration, there's almost always a number of "simple" tags that are rendered as an
156
	* HTML element of the same name with no HTML attributes. For instance, the system tag "p" used
157
	* for paragraphs, "B" tags used for "b" HTML elements, etc... This method replaces those
158
	* templates with a common template that uses a dynamic element name based on the tag's name,
159
	* either its nodeName or localName depending on whether the tag is namespaced, and normalized to
160
	* lowercase using XPath's translate() function
161
	*
162
	* @param  array<string> &$templates Associative array of [tagName => template]
163
	* @param  integer       $minCount
164
	* @return void
165
	*/
166 6
	public static function replaceHomogeneousTemplates(array &$templates, $minCount = 3)
167
	{
168
		// Prepare the XPath expression used for the element's name
169 6
		$expr = 'name()';
170
171
		// Identify "simple" tags, whose template is one element of the same name. Their template
172
		// can be replaced with a dynamic template shared by all the simple tags
173 6
		$tagNames = [];
174 6
		foreach ($templates as $tagName => $template)
175
		{
176
			// Generate the element name based on the tag's localName, lowercased
177 6
			$elName = strtolower(preg_replace('/^[^:]+:/', '', $tagName));
178 6
			if ($template === '<' . $elName . '><xsl:apply-templates/></' . $elName . '>')
179
			{
180 6
				$tagNames[] = $tagName;
181
182
				// Use local-name() if any of the tags are namespaced
183 6
				if (strpos($tagName, ':') !== false)
184
				{
185 2
					$expr = 'local-name()';
186
				}
187
			}
188
		}
189
190
		// We only bother replacing their template if there are at least $minCount simple tags.
191
		// Otherwise it only makes the stylesheet bigger
192 6
		if (count($tagNames) < $minCount)
193
		{
194 1
			return;
195
		}
196
197
		// Generate a list of uppercase characters from the tags' names
198 5
		$chars = preg_replace('/[^A-Z]+/', '', count_chars(implode('', $tagNames), 3));
199 5
		if ($chars > '')
200
		{
201 2
			$expr = 'translate(' . $expr . ",'" . $chars . "','" . strtolower($chars) . "')";
202
		}
203
204
		// Prepare the common template
205 5
		$template = '<xsl:element name="{' . $expr . '}"><xsl:apply-templates/></xsl:element>';
206
207
		// Replace the templates
208 5
		foreach ($tagNames as $tagName)
209
		{
210 5
			$templates[$tagName] = $template;
211
		}
212
	}
213
214
	/**
215
	* Serialize a loaded template back into a string
216
	*
217
	* NOTE: removes the root node created by loadTemplate()
218
	*
219
	* @param  DOMDocument $dom
220
	* @return string
221
	*/
222
	public static function saveTemplate(DOMDocument $dom)
223
	{
224
		return TemplateLoader::save($dom);
225
	}
226
227
	/**
228
	* Get a list of parameters from given XPath expression
229
	*
230
	* @param  DOMNode  $node Context node
231
	* @param  string   $expr XPath expression
232
	* @return string[]
233
	*/
234 8
	protected static function getParametersFromExpression(DOMNode $node, $expr)
235
	{
236 8
		$varNames   = XPathHelper::getVariables($expr);
237 8
		$paramNames = [];
238 8
		$xpath      = new DOMXPath($node->ownerDocument);
239 8
		foreach ($varNames as $name)
240
		{
241
			// Test whether this is the name of a local variable
242 8
			$query = 'ancestor-or-self::*/preceding-sibling::xsl:variable[@name="' . $name . '"]';
243 8
			if (!$xpath->query($query, $node)->length)
244
			{
245 7
				$paramNames[] = $name;
246
			}
247
		}
248
249 8
		return $paramNames;
250
	}
251
}