Completed
Branch Scrutinizer (3da711)
by Josh
03:32
created

TemplateHelper   A

Complexity

Total Complexity 20

Size/Duplication

Total Lines 180
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 20
eloc 59
dl 0
loc 180
ccs 61
cts 61
cp 1
rs 10
c 0
b 0
f 0

4 Methods

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