Completed
Push — master ( 80694b...268392 )
by Josh
17:52 queued 06:21
created

TemplateHelper::removeInvalidAttributes()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 5
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 11
rs 9.4285
1
<?php
2
3
/**
4
* @package   s9e\TextFormatter
5
* @copyright Copyright (c) 2010-2016 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 DOMXPath;
17
use RuntimeException;
18
use s9e\TextFormatter\Configurator\Exceptions\InvalidXslException;
19
use s9e\TextFormatter\Configurator\Helpers\RegexpBuilder;
20
21
abstract class TemplateHelper
22
{
23
	/**
24
	* XSL namespace
25
	*/
26
	const XMLNS_XSL = 'http://www.w3.org/1999/XSL/Transform';
27
28
	/**
29
	* Load a template as an xsl:template node
30
	*
31
	* Will attempt to load it as XML first, then as HTML as a fallback. Either way, an xsl:template
32
	* node is returned
33
	*
34
	* @param  string      $template
35
	* @return DOMDocument
36
	*/
37
	public static function loadTemplate($template)
38
	{
39
		$dom = new DOMDocument;
40
41
		// First try as XML
42
		$xml = '<?xml version="1.0" encoding="utf-8" ?><xsl:template xmlns:xsl="' . self::XMLNS_XSL . '">' . $template . '</xsl:template>';
43
44
		$useErrors = libxml_use_internal_errors(true);
45
		$success   = $dom->loadXML($xml);
46
		libxml_use_internal_errors($useErrors);
47
48
		if ($success)
49
		{
50
			return $dom;
51
		}
52
53
		// Try fixing unescaped ampersands and replacing HTML entities
54
		$tmp = preg_replace('(&(?![A-Za-z0-9]+;|#\\d+;|#x[A-Fa-f0-9]+;))', '&amp;', $template);
55
		$tmp = preg_replace_callback(
56
			'(&(?!quot;|amp;|apos;|lt;|gt;)\\w+;)',
57
			function ($m)
58
			{
59
				return html_entity_decode($m[0], ENT_NOQUOTES, 'UTF-8');
60
			},
61
			$tmp
62
		);
63
		$xml = '<?xml version="1.0" encoding="utf-8" ?><xsl:template xmlns:xsl="' . self::XMLNS_XSL . '">' . $tmp . '</xsl:template>';
64
65
		$useErrors = libxml_use_internal_errors(true);
66
		$success   = $dom->loadXML($xml);
67
		libxml_use_internal_errors($useErrors);
68
69
		if ($success)
70
		{
71
			return $dom;
72
		}
73
74
		// If the template contains an XSL element, abort now. Otherwise, try reparsing it as HTML
75
		if (strpos($template, '<xsl:') !== false)
76
		{
77
			$error = libxml_get_last_error();
78
			throw new InvalidXslException($error->message);
79
		}
80
81
		// Fall back to loading it inside a div, as HTML
82
		$html = '<?xml version="1.0" encoding="utf-8" ?><html><body><div>' . $template . '</div></body></html>';
83
84
		$useErrors = libxml_use_internal_errors(true);
85
		$dom->loadHTML($html);
86
		libxml_use_internal_errors($useErrors);
87
88
		// Now dump the thing as XML and reload it with the proper namespace declaration
89
		$xml = self::innerXML($dom->documentElement->firstChild->firstChild);
90
91
		return self::loadTemplate($xml);
92
	}
93
94
	/**
95
	* Serialize a loaded template back into a string
96
	*
97
	* NOTE: removes the root node created by loadTemplate()
98
	*
99
	* @param  DOMDocument $dom
100
	* @return string
101
	*/
102
	public static function saveTemplate(DOMDocument $dom)
103
	{
104
		return self::innerXML($dom->documentElement);
105
	}
106
107
	/**
108
	* Get the XML content of an element
109
	*
110
	* @param  DOMElement $element
111
	* @return string
112
	*/
113
	protected static function innerXML(DOMElement $element)
114
	{
115
		// Serialize the XML then remove the outer element
116
		$xml = $element->ownerDocument->saveXML($element);
117
118
		$pos = 1 + strpos($xml, '>');
119
		$len = strrpos($xml, '<') - $pos;
120
121
		// If the template is empty, return an empty string
122
		if ($len < 1)
123
		{
124
			return '';
125
		}
126
127
		$xml = substr($xml, $pos, $len);
128
129
		return $xml;
130
	}
131
132
	/**
133
	* Return a list of parameters in use in given XSL
134
	*
135
	* @param  string $xsl XSL source
136
	* @return array       Alphabetically sorted list of unique parameter names
137
	*/
138
	public static function getParametersFromXSL($xsl)
139
	{
140
		$paramNames = [];
141
142
		// Wrap the XSL in boilerplate code because it might not have a root element
143
		$xsl = '<xsl:stylesheet xmlns:xsl="' . self::XMLNS_XSL . '">'
144
		     . '<xsl:template>'
145
		     . $xsl
146
		     . '</xsl:template>'
147
		     . '</xsl:stylesheet>';
148
149
		$dom = new DOMDocument;
150
		$dom->loadXML($xsl);
151
152
		$xpath = new DOMXPath($dom);
153
154
		// Start by collecting XPath expressions in XSL elements
155
		$query = '//xsl:*/@match | //xsl:*/@select | //xsl:*/@test';
156
		foreach ($xpath->query($query) as $attribute)
157
		{
158
			foreach (XPathHelper::getVariables($attribute->value) as $varName)
159
			{
160
				// Test whether this is the name of a local variable
161
				$varQuery = 'ancestor-or-self::*/'
162
				          . 'preceding-sibling::xsl:variable[@name="' . $varName . '"]';
163
164
				if (!$xpath->query($varQuery, $attribute)->length)
165
				{
166
					$paramNames[] = $varName;
167
				}
168
			}
169
		}
170
171
		// Collecting XPath expressions in attribute value templates
172
		$query = '//*[namespace-uri() != "' . self::XMLNS_XSL . '"]'
173
		       . '/@*[contains(., "{")]';
174
		foreach ($xpath->query($query) as $attribute)
175
		{
176
			$tokens = AVTHelper::parse($attribute->value);
177
178
			foreach ($tokens as $token)
179
			{
180
				if ($token[0] !== 'expression')
181
				{
182
					continue;
183
				}
184
185
				foreach (XPathHelper::getVariables($token[1]) as $varName)
186
				{
187
					// Test whether this is the name of a local variable
188
					$varQuery = 'ancestor-or-self::*/'
189
					          . 'preceding-sibling::xsl:variable[@name="' . $varName . '"]';
190
191
					if (!$xpath->query($varQuery, $attribute)->length)
192
					{
193
						$paramNames[] = $varName;
194
					}
195
				}
196
			}
197
		}
198
199
		// Dedupe and sort names
200
		$paramNames = array_unique($paramNames);
201
		sort($paramNames);
202
203
		return $paramNames;
204
	}
205
206
	/**
207
	* Return all attributes (literal or generated) that match given regexp
208
	*
209
	* @param  DOMDocument $dom    Document
210
	* @param  string      $regexp Regexp
211
	* @return array               Array of DOMNode instances
212
	*/
213
	public static function getAttributesByRegexp(DOMDocument $dom, $regexp)
214
	{
215
		$xpath = new DOMXPath($dom);
216
		$nodes = [];
217
218
		// Get literal attributes
219
		foreach ($xpath->query('//@*') as $attribute)
220
		{
221
			if (preg_match($regexp, $attribute->name))
222
			{
223
				$nodes[] = $attribute;
224
			}
225
		}
226
227
		// Get generated attributes
228
		foreach ($xpath->query('//xsl:attribute') as $attribute)
229
		{
230
			if (preg_match($regexp, $attribute->getAttribute('name')))
231
			{
232
				$nodes[] = $attribute;
233
			}
234
		}
235
236
		// Get attributes created with <xsl:copy-of/>
237
		foreach ($xpath->query('//xsl:copy-of') as $node)
238
		{
239
			$expr = $node->getAttribute('select');
240
241
			if (preg_match('/^@(\\w+)$/', $expr, $m)
242
			 && preg_match($regexp, $m[1]))
243
			{
244
				$nodes[] = $node;
245
			}
246
		}
247
248
		return $nodes;
249
	}
250
251
	/**
252
	* Return all elements (literal or generated) that match given regexp
253
	*
254
	* @param  DOMDocument $dom    Document
255
	* @param  string      $regexp Regexp
256
	* @return array               Array of DOMNode instances
257
	*/
258
	public static function getElementsByRegexp(DOMDocument $dom, $regexp)
259
	{
260
		$xpath = new DOMXPath($dom);
261
		$nodes = [];
262
263
		// Get literal attributes
264
		foreach ($xpath->query('//*') as $element)
265
		{
266
			if (preg_match($regexp, $element->localName))
267
			{
268
				$nodes[] = $element;
269
			}
270
		}
271
272
		// Get generated elements
273
		foreach ($xpath->query('//xsl:element') as $element)
274
		{
275
			if (preg_match($regexp, $element->getAttribute('name')))
276
			{
277
				$nodes[] = $element;
278
			}
279
		}
280
281
		// Get elements created with <xsl:copy-of/>
282
		// NOTE: this method of creating elements is disallowed by default
283
		foreach ($xpath->query('//xsl:copy-of') as $node)
284
		{
285
			$expr = $node->getAttribute('select');
286
287
			if (preg_match('/^\\w+$/', $expr)
288
			 && preg_match($regexp, $expr))
289
			{
290
				$nodes[] = $node;
291
			}
292
		}
293
294
		return $nodes;
295
	}
296
297
	/**
298
	* Return all elements (literal or generated) that match given regexp
299
	*
300
	* Will return all <param/> descendants of <object/> and all attributes of <embed/> whose name
301
	* matches given regexp. This method will NOT catch <param/> elements whose 'name' attribute is
302
	* set via an <xsl:attribute/>
303
	*
304
	* @param  DOMDocument $dom    Document
305
	* @param  string      $regexp
306
	* @return array               Array of DOMNode instances
307
	*/
308
	public static function getObjectParamsByRegexp(DOMDocument $dom, $regexp)
309
	{
310
		$xpath = new DOMXPath($dom);
311
		$nodes = [];
312
313
		// Collect attributes from <embed/> elements
314
		foreach (self::getAttributesByRegexp($dom, $regexp) as $attribute)
315
		{
316
			if ($attribute->nodeType === XML_ATTRIBUTE_NODE)
317
			{
318
				if (strtolower($attribute->parentNode->localName) === 'embed')
319
				{
320
					$nodes[] = $attribute;
321
				}
322
			}
323
			elseif ($xpath->evaluate('ancestor::embed', $attribute))
324
			{
325
				// Assuming <xsl:attribute/> or <xsl:copy-of/>
326
				$nodes[] = $attribute;
327
			}
328
		}
329
330
		// Collect <param/> descendants of <object/> elements
331
		foreach ($dom->getElementsByTagName('object') as $object)
332
		{
333
			foreach ($object->getElementsByTagName('param') as $param)
334
			{
335
				if (preg_match($regexp, $param->getAttribute('name')))
336
				{
337
					$nodes[] = $param;
338
				}
339
			}
340
		}
341
342
		return $nodes;
343
	}
344
345
	/**
346
	* Return all DOMNodes whose content is CSS
347
	*
348
	* @param  DOMDocument $dom Document
349
	* @return array            Array of DOMNode instances
350
	*/
351
	public static function getCSSNodes(DOMDocument $dom)
352
	{
353
		$regexp = '/^style$/i';
354
		$nodes  = array_merge(
355
			self::getAttributesByRegexp($dom, $regexp),
356
			self::getElementsByRegexp($dom, '/^style$/i')
357
		);
358
359
		return $nodes;
360
	}
361
362
	/**
363
	* Return all DOMNodes whose content is JavaScript
364
	*
365
	* @param  DOMDocument $dom Document
366
	* @return array            Array of DOMNode instances
367
	*/
368
	public static function getJSNodes(DOMDocument $dom)
369
	{
370
		$regexp = '/^(?>data-s9e-livepreview-postprocess$|on)/i';
371
		$nodes  = array_merge(
372
			self::getAttributesByRegexp($dom, $regexp),
373
			self::getElementsByRegexp($dom, '/^script$/i')
374
		);
375
376
		return $nodes;
377
	}
378
379
	/**
380
	* Return all DOMNodes whose content is an URL
381
	*
382
	* NOTE: it will also return HTML4 nodes whose content is an URI
383
	*
384
	* @param  DOMDocument $dom Document
385
	* @return array            Array of DOMNode instances
386
	*/
387
	public static function getURLNodes(DOMDocument $dom)
388
	{
389
		$regexp = '/(?>^(?>action|background|c(?>ite|lassid|odebase)|data|formaction|href|icon|longdesc|manifest|p(?>luginspage|oster|rofile)|usemap)|src)$/i';
390
		$nodes  = self::getAttributesByRegexp($dom, $regexp);
391
392
		/**
393
		* @link http://helpx.adobe.com/flash/kb/object-tag-syntax-flash-professional.html
394
		* @link http://www.sitepoint.com/control-internet-explorer/
395
		*/
396
		foreach (self::getObjectParamsByRegexp($dom, '/^(?:dataurl|movie)$/i') as $param)
397
		{
398
			$node = $param->getAttributeNode('value');
399
			if ($node)
400
			{
401
				$nodes[] = $node;
402
			}
403
		}
404
405
		return $nodes;
406
	}
407
408
	/**
409
	* Replace parts of a template that match given regexp
410
	*
411
	* Treats attribute values as plain text. Replacements within XPath expression is unsupported.
412
	* The callback must return an array with two elements. The first must be either of 'expression',
413
	* 'literal' or 'passthrough', and the second element depends on the first.
414
	*
415
	*  - 'expression' indicates that the replacement must be treated as an XPath expression such as
416
	*    '@foo', which must be passed as the second element.
417
	*  - 'literal' indicates a literal (plain text) replacement, passed as its second element.
418
	*  - 'passthrough' indicates that the replacement should the tag's content. It works differently
419
	*    whether it is inside an attribute's value or a text node. Within an attribute's value, the
420
	*    replacement will be the text content of the tag. Within a text node, the replacement
421
	*    becomes an <xsl:apply-templates/> node.
422
	*
423
	* @param  string   $template Original template
424
	* @param  string   $regexp   Regexp for matching parts that need replacement
425
	* @param  callback $fn       Callback used to get the replacement
426
	* @return string             Processed template
427
	*/
428
	public static function replaceTokens($template, $regexp, $fn)
429
	{
430
		if ($template === '')
431
		{
432
			return $template;
433
		}
434
435
		$dom   = self::loadTemplate($template);
436
		$xpath = new DOMXPath($dom);
437
438
		// Replace tokens in attributes
439
		foreach ($xpath->query('//@*') as $attribute)
440
		{
441
			// Generate the new value
442
			$attrValue = preg_replace_callback(
443
				$regexp,
444
				function ($m) use ($fn, $attribute)
445
				{
446
					$replacement = $fn($m, $attribute);
447
448
					if ($replacement[0] === 'expression')
449
					{
450
						return '{' . $replacement[1] . '}';
451
					}
452
					elseif ($replacement[0] === 'passthrough')
453
					{
454
						return '{.}';
455
					}
456
					else
457
					{
458
						// Literal replacement
459
						return $replacement[1];
460
					}
461
				},
462
				$attribute->value
463
			);
464
465
			// Replace the attribute value
466
			$attribute->value = htmlspecialchars($attrValue, ENT_COMPAT, 'UTF-8');
467
		}
468
469
		// Replace tokens in text nodes
470
		foreach ($xpath->query('//text()') as $node)
471
		{
472
			preg_match_all(
473
				$regexp,
474
				$node->textContent,
475
				$matches,
476
				PREG_SET_ORDER | PREG_OFFSET_CAPTURE
477
			);
478
479
			if (empty($matches))
480
			{
481
				continue;
482
			}
483
484
			// Grab the node's parent so that we can rebuild the text with added variables right
485
			// before the node, using DOM's insertBefore(). Technically, it would make more sense
486
			// to create a document fragment, append nodes then replace the node with the fragment
487
			// but it leads to namespace redeclarations, which looks ugly
488
			$parentNode = $node->parentNode;
489
490
			$lastPos = 0;
491
			foreach ($matches as $m)
492
			{
493
				$pos = $m[0][1];
494
495
				// Catch-up to current position
496
				if ($pos > $lastPos)
497
				{
498
					$parentNode->insertBefore(
499
						$dom->createTextNode(
500
							substr($node->textContent, $lastPos, $pos - $lastPos)
501
						),
502
						$node
503
					);
504
				}
505
				$lastPos = $pos + strlen($m[0][0]);
506
507
				// Remove the offset data from the array, keep only the content of captures so that
508
				// $_m contains the same data that preg_match() or preg_replace() would return
509
				$_m = [];
510
				foreach ($m as $capture)
511
				{
512
					$_m[] = $capture[0];
513
				}
514
515
				// Get the replacement for this token
516
				$replacement = $fn($_m, $node);
517
518
				if ($replacement[0] === 'expression')
519
				{
520
					// Expressions are evaluated in a <xsl:value-of/> node
521
					$parentNode
522
						->insertBefore(
523
							$dom->createElementNS(self::XMLNS_XSL, 'xsl:value-of'),
524
							$node
525
						)
526
						->setAttribute('select', $replacement[1]);
527
				}
528
				elseif ($replacement[0] === 'passthrough')
529
				{
530
					// Passthrough token, replace with <xsl:apply-templates/>
531
					$parentNode->insertBefore(
532
						$dom->createElementNS(self::XMLNS_XSL, 'xsl:apply-templates'),
533
						$node
534
					);
535
				}
536
				else
537
				{
538
					// Literal replacement
539
					$parentNode->insertBefore($dom->createTextNode($replacement[1]), $node);
540
				}
541
			}
542
543
			// Append the rest of the text
544
			$text = substr($node->textContent, $lastPos);
545
			if ($text > '')
546
			{
547
				$parentNode->insertBefore($dom->createTextNode($text), $node);
548
			}
549
550
			// Now remove the old text node
551
			$parentNode->removeChild($node);
552
		}
553
554
		return self::saveTemplate($dom);
555
	}
556
557
	/**
558
	* Highlight the source of a node inside of a template
559
	*
560
	* @param  DOMNode $node    Node to highlight
561
	* @param  string  $prepend HTML to prepend
562
	* @param  string  $append  HTML to append
563
	* @return string           Template's source, as HTML
564
	*/
565
	public static function highlightNode(DOMNode $node, $prepend, $append)
566
	{
567
		// Add a unique token to the node
568
		$uniqid = uniqid('_');
569
		if ($node instanceof DOMAttr)
570
		{
571
			$node->value .= $uniqid;
572
		}
573
		elseif ($node instanceof DOMElement)
574
		{
575
			$node->setAttribute($uniqid, '');
576
		}
577
		elseif ($node instanceof DOMCharacterData
578
		     || $node instanceof DOMProcessingInstruction)
579
		{
580
			$node->data .= $uniqid;
581
		}
582
583
		$dom = $node->ownerDocument;
584
		$dom->formatOutput = true;
585
586
		$docXml = self::innerXML($dom->documentElement);
587
		$docXml = trim(str_replace("\n  ", "\n", $docXml));
588
589
		$nodeHtml = htmlspecialchars(trim($dom->saveXML($node)));
590
		$docHtml  = htmlspecialchars($docXml);
591
592
		// Enclose the node's representation in our hilighting HTML
593
		$html = str_replace($nodeHtml, $prepend . $nodeHtml . $append, $docHtml);
594
595
		// Remove the unique token from HTML and from the node
596
		if ($node instanceof DOMAttr)
597
		{
598
			$node->value = substr($node->value, 0, -strlen($uniqid));
599
			$html = str_replace($uniqid, '', $html);
600
		}
601
		elseif ($node instanceof DOMElement)
602
		{
603
			$node->removeAttribute($uniqid);
604
			$html = str_replace(' ' . $uniqid . '=&quot;&quot;', '', $html);
605
		}
606
		elseif ($node instanceof DOMCharacterData
607
		     || $node instanceof DOMProcessingInstruction)
608
		{
609
			$node->data .= $uniqid;
610
			$html = str_replace($uniqid, '', $html);
611
		}
612
613
		return $html;
614
	}
615
616
	/**
617
	* Get the regexp used to remove meta elements from the intermediate representation
618
	*
619
	* @param  array  $templates
620
	* @return string
621
	*/
622
	public static function getMetaElementsRegexp(array $templates)
623
	{
624
		$exprs = [];
625
626
		// Coalesce all templates and load them into DOM
627
		$xsl = '<xsl:template xmlns:xsl="http://www.w3.org/1999/XSL/Transform">' . implode('', $templates) . '</xsl:template>';
628
		$dom = new DOMDocument;
629
		$dom->loadXML($xsl);
630
		$xpath = new DOMXPath($dom);
631
632
		// Collect the values of all the "match", "select" and "test" attributes of XSL elements
633
		$query = '//xsl:*/@*[contains("matchselectest", name())]';
634
		foreach ($xpath->query($query) as $attribute)
635
		{
636
			$exprs[] = $attribute->value;
637
		}
638
639
		// Collect the XPath expressions used in all the attributes of non-XSL elements
640
		$query = '//*[namespace-uri() != "' . self::XMLNS_XSL . '"]/@*';
641
		foreach ($xpath->query($query) as $attribute)
642
		{
643
			foreach (AVTHelper::parse($attribute->value) as $token)
644
			{
645
				if ($token[0] === 'expression')
646
				{
647
					$exprs[] = $token[1];
648
				}
649
			}
650
		}
651
652
		// Names of the meta elements
653
		$tagNames = [
654
			'e' => true,
655
			'i' => true,
656
			's' => true
657
		];
658
659
		// In the highly unlikely event the meta elements are rendered, we remove them from the list
660
		foreach (array_keys($tagNames) as $tagName)
661
		{
662
			if (isset($templates[$tagName]) && $templates[$tagName] !== '')
663
			{
664
				unset($tagNames[$tagName]);
665
			}
666
		}
667
668
		// Create a regexp that matches the tag names used as element names, e.g. "s" in "//s" but
669
		// not in "@s" or "$s"
670
		$regexp = '(\\b(?<![$@])(' . implode('|', array_keys($tagNames)) . ')(?!-)\\b)';
671
672
		// Now look into all of the expressions that we've collected
673
		preg_match_all($regexp, implode("\n", $exprs), $m);
674
675
		foreach ($m[0] as $tagName)
676
		{
677
			unset($tagNames[$tagName]);
678
		}
679
680
		if (empty($tagNames))
681
		{
682
			// Always-false regexp
683
			return '((?!))';
684
		}
685
686
		return '(<' . RegexpBuilder::fromList(array_keys($tagNames)) . '>[^<]*</[^>]+>)';
687
	}
688
689
	/**
690
	* Replace simple templates (in an array, in-place) with a common template
691
	*
692
	* In some situations, renderers can take advantage of multiple tags having the same template. In
693
	* any configuration, there's almost always a number of "simple" tags that are rendered as an
694
	* HTML element of the same name with no HTML attributes. For instance, the system tag "p" used
695
	* for paragraphs, "B" tags used for "b" HTML elements, etc... This method replaces those
696
	* templates with a common template that uses a dynamic element name based on the tag's name,
697
	* either its nodeName or localName depending on whether the tag is namespaced, and normalized to
698
	* lowercase using XPath's translate() function
699
	*
700
	* @param  array<string> &$templates Associative array of [tagName => template]
701
	* @param  integer       $minCount
702
	* @return void
703
	*/
704
	public static function replaceHomogeneousTemplates(array &$templates, $minCount = 3)
705
	{
706
		$tagNames = [];
707
708
		// Prepare the XPath expression used for the element's name
709
		$expr = 'name()';
710
711
		// Identify "simple" tags, whose template is one element of the same name. Their template
712
		// can be replaced with a dynamic template shared by all the simple tags
713
		foreach ($templates as $tagName => $template)
714
		{
715
			// Generate the element name based on the tag's localName, lowercased
716
			$elName = strtolower(preg_replace('/^[^:]+:/', '', $tagName));
717
718
			if ($template === '<' . $elName . '><xsl:apply-templates/></' . $elName . '>')
719
			{
720
				$tagNames[] = $tagName;
721
722
				// Use local-name() if any of the tags are namespaced
723
				if (strpos($tagName, ':') !== false)
724
				{
725
					$expr = 'local-name()';
726
				}
727
			}
728
		}
729
730
		// We only bother replacing their template if there are at least $minCount simple tags.
731
		// Otherwise it only makes the stylesheet bigger
732
		if (count($tagNames) < $minCount)
733
		{
734
			return;
735
		}
736
737
		// Generate a list of uppercase characters from the tags' names
738
		$chars = preg_replace('/[^A-Z]+/', '', count_chars(implode('', $tagNames), 3));
739
740
		if (is_string($chars) && $chars !== '')
741
		{
742
			$expr = 'translate(' . $expr . ",'" . $chars . "','" . strtolower($chars) . "')";
743
		}
744
745
		// Prepare the common template
746
		$template = '<xsl:element name="{' . $expr . '}">'
747
		          . '<xsl:apply-templates/>'
748
		          . '</xsl:element>';
749
750
		// Replace the templates
751
		foreach ($tagNames as $tagName)
752
		{
753
			$templates[$tagName] = $template;
754
		}
755
	}
756
}