Completed
Branch TemplateInspector (b6c926)
by Josh
12:57
created

TemplateInspector::computeIsTransparent()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 16
rs 9.2
c 0
b 0
f 0
cc 4
eloc 7
nc 4
nop 0
1
<?php
2
3
/**
4
* @package   s9e\TextFormatter
5
* @copyright Copyright (c) 2010-2017 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 DOMDocument;
11
use DOMElement;
12
use DOMXPath;
13
14
/**
15
* This class helps the RulesGenerator by analyzing a given template in order to answer questions
16
* such as "can this tag be a child/descendant of that other tag?" and others related to the HTML5
17
* content model.
18
*
19
* We use the HTML5 specs to determine which children or descendants should be allowed or denied
20
* based on HTML5 content models. While it does not exactly match HTML5 content models, it gets
21
* pretty close. We also use HTML5 "optional end tag" rules to create closeParent rules.
22
*
23
* Currently, this method does not evaluate elements created with <xsl:element> correctly, or
24
* attributes created with <xsl:attribute> and may never will due to the increased complexity it
25
* would entail. Additionally, it does not evaluate the scope of <xsl:apply-templates/>. For
26
* instance, it will treat <xsl:apply-templates select="LI"/> as if it was <xsl:apply-templates/>
27
*
28
* @link http://dev.w3.org/html5/spec/content-models.html#content-models
29
* @link http://dev.w3.org/html5/spec/syntax.html#optional-tags
30
* @see  /scripts/patchTemplateInspector.php
31
*/
32
class TemplateInspector
33
{
34
	/**
35
	* @var string[] allowChild bitfield for each branch
36
	*/
37
	protected $allowChildBitfields = [];
38
39
	/**
40
	* @var bool Whether elements are allowed as children
41
	*/
42
	protected $allowsChildElements;
43
44
	/**
45
	* @var bool Whether text nodes are allowed as children
46
	*/
47
	protected $allowsText;
48
49
	/**
50
	* @var string OR-ed bitfield representing all of the categories used by this template
51
	*/
52
	protected $contentBitfield = "\0";
53
54
	/**
55
	* @var string denyDescendant bitfield
56
	*/
57
	protected $denyDescendantBitfield = "\0";
58
59
	/**
60
	* @var DOMDocument Document containing the template
61
	*/
62
	protected $dom;
63
64
	/**
65
	* @var bool Whether this template contains any HTML elements
66
	*/
67
	protected $hasElements = false;
68
69
	/**
70
	* @var bool Whether this template renders non-whitespace text nodes at its root
71
	*/
72
	protected $hasRootText = false;
73
74
	/**
75
	* @var bool Whether this template should be considered a block-level element
76
	*/
77
	protected $isBlock = false;
78
79
	/**
80
	* @var bool Whether the template uses the "empty" content model
81
	*/
82
	protected $isEmpty;
83
84
	/**
85
	* @var bool Whether this template adds to the list of active formatting elements
86
	*/
87
	protected $isFormattingElement;
88
89
	/**
90
	* @var bool Whether this template lets content through via an xsl:apply-templates element
91
	*/
92
	protected $isPassthrough = false;
93
94
	/**
95
	* @var bool Whether all branches use the transparent content model
96
	*/
97
	protected $isTransparent = false;
98
99
	/**
100
	* @var bool Whether all branches have an ancestor that is a void element
101
	*/
102
	protected $isVoid;
103
104
	/**
105
	* @var array Names of every last HTML element that precedes an <xsl:apply-templates/> node
106
	*/
107
	protected $leafNodes = [];
108
109
	/**
110
	* @var bool Whether any branch has an element that preserves new lines by default (e.g. <pre>)
111
	*/
112
	protected $preservesNewLines = false;
113
114
	/**
115
	* @var array Bitfield of the first HTML element of every branch
116
	*/
117
	protected $rootBitfields = [];
118
119
	/**
120
	* @var array Names of every HTML element that have no HTML parent
121
	*/
122
	protected $rootNodes = [];
123
124
	/**
125
	* @var DOMXPath XPath engine associated with $this->dom
126
	*/
127
	protected $xpath;
128
129
	/**
130
	* Constructor
131
	*
132
	* @param  string $template Template content
133
	*/
134
	public function __construct($template)
135
	{
136
		$this->dom   = TemplateHelper::loadTemplate($template);
137
		$this->xpath = new DOMXPath($this->dom);
138
139
		$this->analyseRootNodes();
140
		$this->analyseBranches();
141
		$this->analyseContent();
142
	}
143
144
	/**
145
	* Return whether this template allows a given child
146
	*
147
	* @param  TemplateInspector $child
148
	* @return bool
149
	*/
150
	public function allowsChild(TemplateInspector $child)
151
	{
152
		// Sometimes, a template can technically be allowed as a child but denied as a descendant
153
		if (!$this->allowsDescendant($child))
154
		{
155
			return false;
156
		}
157
158
		foreach ($child->rootBitfields as $rootBitfield)
159
		{
160
			foreach ($this->allowChildBitfields as $allowChildBitfield)
161
			{
162
				if (!self::match($rootBitfield, $allowChildBitfield))
163
				{
164
					return false;
165
				}
166
			}
167
		}
168
169
		if (!$this->allowsText && $child->hasRootText)
170
		{
171
			return false;
172
		}
173
174
		return true;
175
	}
176
177
	/**
178
	* Return whether this template allows a given descendant
179
	*
180
	* @param  TemplateInspector $descendant
181
	* @return bool
182
	*/
183
	public function allowsDescendant(TemplateInspector $descendant)
184
	{
185
		// Test whether the descendant is explicitly disallowed
186
		if (self::match($descendant->contentBitfield, $this->denyDescendantBitfield))
187
		{
188
			return false;
189
		}
190
191
		// Test whether the descendant contains any elements and we disallow elements
192
		if (!$this->allowsChildElements && $descendant->hasElements)
193
		{
194
			return false;
195
		}
196
197
		return true;
198
	}
199
200
	/**
201
	* Return whether this template allows elements as children
202
	*
203
	* @return bool
204
	*/
205
	public function allowsChildElements()
206
	{
207
		return $this->allowsChildElements;
208
	}
209
210
	/**
211
	* Return whether this template allows text nodes as children
212
	*
213
	* @return bool
214
	*/
215
	public function allowsText()
216
	{
217
		return $this->allowsText;
218
	}
219
220
	/**
221
	* Return whether this template automatically closes given parent template
222
	*
223
	* @param  TemplateInspector $parent
224
	* @return bool
225
	*/
226
	public function closesParent(TemplateInspector $parent)
227
	{
228
		foreach ($this->rootNodes as $rootName)
229
		{
230
			if (empty(self::$htmlElements[$rootName]['cp']))
231
			{
232
				continue;
233
			}
234
235
			foreach ($parent->leafNodes as $leafName)
236
			{
237
				if (in_array($leafName, self::$htmlElements[$rootName]['cp'], true))
238
				{
239
					// If any of this template's root node closes one of the parent's leaf node, we
240
					// consider that this template closes the other one
241
					return true;
242
				}
243
			}
244
		}
245
246
		return false;
247
	}
248
249
	/**
250
	* Return the source template as a DOMDocument
251
	*
252
	* NOTE: the document should not be modified
253
	*
254
	* @return DOMDocument
255
	*/
256
	public function getDOM()
257
	{
258
		return $this->dom;
259
	}
260
261
	/**
262
	* Return whether this template should be considered a block-level element
263
	*
264
	* @return bool
265
	*/
266
	public function isBlock()
267
	{
268
		return $this->isBlock;
269
	}
270
271
	/**
272
	* Return whether this template adds to the list of active formatting elements
273
	*
274
	* @return bool
275
	*/
276
	public function isFormattingElement()
277
	{
278
		return $this->isFormattingElement;
279
	}
280
281
	/**
282
	* Return whether this template uses the "empty" content model
283
	*
284
	* @return bool
285
	*/
286
	public function isEmpty()
287
	{
288
		return $this->isEmpty;
289
	}
290
291
	/**
292
	* Return whether this template lets content through via an xsl:apply-templates element
293
	*
294
	* @return bool
295
	*/
296
	public function isPassthrough()
297
	{
298
		return $this->isPassthrough;
299
	}
300
301
	/**
302
	* Return whether this template uses the "transparent" content model
303
	*
304
	* @return bool
305
	*/
306
	public function isTransparent()
307
	{
308
		return $this->isTransparent;
309
	}
310
311
	/**
312
	* Return whether all branches have an ancestor that is a void element
313
	*
314
	* @return bool
315
	*/
316
	public function isVoid()
317
	{
318
		return $this->isVoid;
319
	}
320
321
	/**
322
	* Return whether this template preserves the whitespace in its descendants
323
	*
324
	* @return bool
325
	*/
326
	public function preservesNewLines()
327
	{
328
		return $this->preservesNewLines;
329
	}
330
331
	/**
332
	* Analyses the content of the whole template and set $this->contentBitfield accordingly
333
	*/
334
	protected function analyseContent()
335
	{
336
		// Get all non-XSL elements
337
		$query = '//*[namespace-uri() != "http://www.w3.org/1999/XSL/Transform"]';
338
339
		foreach ($this->xpath->query($query) as $node)
340
		{
341
			$this->contentBitfield |= $this->getBitfield($node->localName, 'c', $node);
342
			$this->hasElements = true;
343
		}
344
345
		// Test whether this template is passthrough
346
		$this->isPassthrough = (bool) $this->xpath->evaluate('count(//xsl:apply-templates)');
347
	}
348
349
	/**
350
	* Records the HTML elements (and their bitfield) rendered at the root of the template
351
	*/
352
	protected function analyseRootNodes()
353
	{
354
		// Get every non-XSL element with no non-XSL ancestor. This should return us the first
355
		// HTML element of every branch
356
		$query = '//*[namespace-uri() != "http://www.w3.org/1999/XSL/Transform"]'
357
		       . '[not(ancestor::*[namespace-uri() != "http://www.w3.org/1999/XSL/Transform"])]';
358
359
		foreach ($this->xpath->query($query) as $node)
360
		{
361
			$elName = $node->localName;
362
363
			// Save the actual name of the root node
364
			$this->rootNodes[] = $elName;
365
366
			if (!isset(self::$htmlElements[$elName]))
367
			{
368
				// Unknown elements are treated as if they were a <span> element
369
				$elName = 'span';
370
			}
371
372
			// If any root node is a block-level element, we'll mark the template as such
373
			if ($this->elementIsBlock($elName, $node))
374
			{
375
				$this->isBlock = true;
376
			}
377
378
			$this->rootBitfields[] = $this->getBitfield($elName, 'c', $node);
379
		}
380
381
		// Test for non-whitespace text nodes at the root. For that we need a predicate that filters
382
		// out: nodes with a non-XSL ancestor,
383
		$predicate = '[not(ancestor::*[namespace-uri() != "http://www.w3.org/1999/XSL/Transform"])]';
384
385
		// ..and nodes with an <xsl:attribute/>, <xsl:comment/> or <xsl:variable/> ancestor
386
		$predicate .= '[not(ancestor::xsl:attribute | ancestor::xsl:comment | ancestor::xsl:variable)]';
387
388
		$query = '//text()[normalize-space() != ""]' . $predicate
389
		       . '|'
390
		       . '//xsl:text[normalize-space() != ""]' . $predicate
391
		       . '|'
392
		       . '//xsl:value-of' . $predicate;
393
394
		if ($this->evaluate($query, $this->dom->documentElement))
395
		{
396
			$this->hasRootText = true;
397
		}
398
	}
399
400
	/**
401
	* Analyses each branch that leads to an <xsl:apply-templates/> tag
402
	*/
403
	protected function analyseBranches()
404
	{
405
		$branches = [];
406
		foreach ($this->getXSLElements('apply-templates') as $applyTemplates)
407
		{
408
			$query      = 'ancestor::*[namespace-uri() != "http://www.w3.org/1999/XSL/Transform"]';
409
			$branches[] = iterator_to_array($this->xpath->query($query, $applyTemplates));
410
		}
411
		$this->branches = $branches;
0 ignored issues
show
Bug introduced by
The property branches does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
412
413
		foreach (array_filter($branches) as $branch)
414
		{
415
			$this->leafNodes[] = end($branch)->localName;
416
		}
417
418
		$this->computeAllowsChildElements();
419
		$this->computeAllowsText();
420
		$this->computeBitfields();
421
		$this->computeFormattingElement();
422
		$this->computeIsEmpty();
423
		$this->computeIsTransparent();
424
		$this->computeIsVoid();
425
426
		foreach ($branches as $branch)
427
		{
428
			/**
429
			* @var boolean Whether this branch preserves new lines
430
			*/
431
			$preservesNewLines = false;
432
433
			foreach ($branch as $node)
434
			{
435
				$elName = $node->localName;
436
437
				// Test whether this branch preserves whitespace by inspecting the current element
438
				// and the value of its style attribute. Technically, this block of code also tests
439
				// this element's descendants' style attributes but the result is the same as we
440
				// need to check every element of this branch in order
441
				$style = '';
442
443
				if ($this->hasProperty($elName, 'pre', $node))
444
				{
445
					$style .= 'white-space:pre;';
446
				}
447
448
				if ($node->hasAttribute('style'))
449
				{
450
					$style .= $node->getAttribute('style') . ';';
451
				}
452
453
				$attributes = $this->xpath->query('.//xsl:attribute[@name="style"]', $node);
454
				foreach ($attributes as $attribute)
455
				{
456
					$style .= $attribute->textContent;
457
				}
458
459
				preg_match_all(
460
					'/white-space\\s*:\\s*(no|pre)/i',
461
					strtolower($style),
462
					$matches
463
				);
464
				foreach ($matches[1] as $match)
465
				{
466
					// TRUE:  "pre", "pre-line" and "pre-wrap"
467
					// FALSE: "normal", "nowrap"
468
					$preservesNewLines = ($match === 'pre');
469
				}
470
			}
471
472
			// If any branch preserves new lines, the template preserves new lines
473
			if ($preservesNewLines)
474
			{
475
				$this->preservesNewLines = true;
476
			}
477
		}
478
	}
479
480
	/**
481
	* 
482
	*
483
	* @return void
484
	*/
485
	protected function computeBitfields()
486
	{
487
		if (empty($this->branches))
488
		{
489
			$this->allowChildBitfields = ["\0"];
490
491
			return;
492
		}
493
		foreach ($this->branches as $branch)
494
		{
495
			/**
496
			* @var string allowChild bitfield for current branch. Starts with the value associated
497
			*             with <div> in order to approximate a value if the whole branch uses the
498
			*             transparent content model
499
			*/
500
			$branchBitfield = self::$htmlElements['div']['ac'];
501
502
			foreach ($branch as $element)
503
			{
504
				$elName = $element->localName;
505
				if (!isset(self::$htmlElements[$elName]))
506
				{
507
					// Unknown elements are treated as if they were a <span> element
508
					$elName = 'span';
509
				}
510
				if (!$this->hasProperty($elName, 't', $element))
511
				{
512
					// If the element isn't transparent, we reset its bitfield
513
					$branchBitfield = "\0";
514
				}
515
516
				// allowChild rules are cumulative if transparent, and reset above otherwise
517
				$branchBitfield |= $this->getBitfield($elName, 'ac', $element);
518
519
				// denyDescendant rules are cumulative
520
				$this->denyDescendantBitfield |= $this->getBitfield($elName, 'dd', $element);
521
			}
522
523
			// Add this branch's bitfield to the list
524
			$this->allowChildBitfields[] = $branchBitfield;
525
		}
526
	}
527
528
	/**
529
	* Compute the allowsChildElements property
530
	*
531
	* A template allows child Elements if it has at least one xsl:apply-templates and none of its
532
	* ancestors have the text-only ("to") property
533
	*
534
	* @return void
535
	*/
536
	protected function computeAllowsChildElements()
537
	{
538
		$this->allowsChildElements = !empty($this->branches);
539
		foreach ($this->branches as $branch)
540
		{
541
			foreach ($branch as $element)
542
			{
543
				if ($this->hasProperty($element->nodeName, 'to', $element))
544
				{
545
					$this->allowsChildElements = false;
546
547
					return;
548
				}
549
			}
550
		}
551
	}
552
553
	/**
554
	* Compute the allowsText property
555
	*
556
	* A template is said to allow text if none of the leaf elements disallow text
557
	*
558
	* @return void
559
	*/
560
	protected function computeAllowsText()
561
	{
562
		$this->allowsText = true;
563
		foreach (array_filter($this->branches) as $branch)
564
		{
565
			$element = end($branch);
566
			if ($this->hasProperty($element->nodeName, 'nt', $element))
567
			{
568
				$this->allowsText = false;
569
570
				return;
571
			}
572
		}
573
	}
574
575
	/**
576
	* Compute the isFormattingElement property
577
	*
578
	* A template is said to be a formatting element if all (non-zero) of its branches are entirely
579
	* composed of formatting elements
580
	*
581
	* @return void
582
	*/
583
	protected function computeFormattingElement()
584
	{
585
		$this->isFormattingElement = (bool) count(array_filter($this->branches));
586
		foreach ($this->branches as $branch)
587
		{
588
			foreach ($branch as $element)
589
			{
590
				if (!$this->hasProperty($element->nodeName, 'fe', $element) && !$this->isFormattingSpan($element))
591
				{
592
					$this->isFormattingElement = false;
593
594
					return;
595
				}
596
			}
597
		}
598
	}
599
600
	/**
601
	* Compute the isEmpty property
602
	*
603
	* A template is said to be empty if it has no xsl:apply-templates elements or any there is a empty
604
	* element ancestor to an xsl:apply-templates element
605
	*
606
	* @return void
607
	*/
608
	protected function computeIsEmpty()
609
	{
610
		$this->isEmpty = $this->computeIsEmptyVoid('e');
611
	}
612
613
	/**
614
	* Compute and return given property according to isEmpty/isVoid rules
615
	*
616
	* @param  string Property's name: "e" or "v"
617
	* @return bool
618
	*/
619
	protected function computeIsEmptyVoid($propName)
620
	{
621
		foreach ($this->branches as $branch)
622
		{
623
			foreach ($branch as $element)
624
			{
625
				if ($this->hasProperty($element->nodeName, $propName, $element))
626
				{
627
					return true;
628
				}
629
			}
630
		}
631
632
		return empty($this->branches);
633
	}
634
635
	/**
636
	* Compute the isTransparent property
637
	*
638
	* A template is said to be transparent if it has at least one branch and no non-transparent
639
	* elements in its path
640
	*
641
	* @return void
642
	*/
643
	protected function computeIsTransparent()
644
	{
645
		$this->isTransparent = !empty($this->branches);
646
		foreach ($this->branches as $branch)
647
		{
648
			foreach ($branch as $element)
649
			{
650
				if (!$this->hasProperty($element->nodeName, 't', $element))
651
				{
652
					$this->isTransparent = false;
653
654
					return;
655
				}
656
			}
657
		}
658
	}
659
660
	/**
661
	* Compute the isVoid property
662
	*
663
	* A template is said to be void if it has no xsl:apply-templates elements or any there is a void
664
	* element ancestor to an xsl:apply-templates element
665
	*
666
	* @return void
667
	*/
668
	protected function computeIsVoid()
669
	{
670
		$this->isVoid = $this->computeIsEmptyVoid('v');
671
672
	}
673
674
	/**
675
	* Test whether given element is a block-level element
676
	*
677
	* @param  string     $elName Element name
678
	* @param  DOMElement $node   Context node
679
	* @return bool
680
	*/
681
	protected function elementIsBlock($elName, DOMElement $node)
682
	{
683
		$style = $this->getStyle($node);
684
		if (preg_match('(\\bdisplay\\s*:\\s*block)i', $style))
685
		{
686
			return true;
687
		}
688
		if (preg_match('(\\bdisplay\\s*:\\s*(?:inli|no)ne)i', $style))
689
		{
690
			return false;
691
		}
692
693
		return $this->hasProperty($elName, 'b', $node);
694
	}
695
696
	/**
697
	* Evaluate a boolean XPath query
698
	*
699
	* @param  string     $query XPath query
700
	* @param  DOMElement $node  Context node
701
	* @return boolean
702
	*/
703
	protected function evaluate($query, DOMElement $node)
704
	{
705
		return $this->xpath->evaluate('boolean(' . $query . ')', $node);
706
	}
707
708
	/**
709
	* Retrieve and return the inline style assigned to given element
710
	*
711
	* @param  DOMElement $node Context node
712
	* @return string
713
	*/
714
	protected function getStyle(DOMElement $node)
715
	{
716
		// Start with the inline attribute
717
		$style = $node->getAttribute('style');
718
719
		// Add the content of any xsl:attribute named "style". This will miss optional attributes
720
		$xpath = new DOMXPath($node->ownerDocument);
721
		$query = 'xsl:attribute[@name="style"]';
722
		foreach ($xpath->query($query, $node) as $attribute)
723
		{
724
			$style .= ';' . $attribute->textContent;
725
		}
726
727
		return $style;
728
	}
729
730
	/**
731
	* Get all XSL elements of given name
732
	*
733
	* @param  string      $elName XSL element's name, e.g. "apply-templates"
734
	* @return \DOMNodeList
735
	*/
736
	protected function getXSLElements($elName)
737
	{
738
		return $this->dom->getElementsByTagNameNS('http://www.w3.org/1999/XSL/Transform', $elName);
739
	}
740
741
	/**
742
	* Test whether given node is a span element used for formatting
743
	*
744
	* Will return TRUE if the node is a span element with a class attribute and/or a style attribute
745
	* and no other attributes
746
	*
747
	* @param  DOMElement $node
748
	* @return boolean
749
	*/
750
	protected function isFormattingSpan(DOMElement $node)
751
	{
752
		if ($node->nodeName !== 'span')
753
		{
754
			return false;
755
		}
756
757
		if ($node->getAttribute('class') === '' && $node->getAttribute('style') === '')
758
		{
759
			return false;
760
		}
761
762
		foreach ($node->attributes as $attrName => $attribute)
763
		{
764
			if ($attrName !== 'class' && $attrName !== 'style')
765
			{
766
				return false;
767
			}
768
		}
769
770
		return true;
771
	}
772
773
	/**
774
	* "What is this?" you might ask. This is basically a compressed version of the HTML5 content
775
	* models and rules, with some liberties taken.
776
	*
777
	* For each element, up to three bitfields are defined: "c", "ac" and "dd". Bitfields are stored
778
	* as raw bytes, formatted using the octal notation to keep the sources ASCII.
779
	*
780
	*   "c" represents the categories the element belongs to. The categories are comprised of HTML5
781
	*   content models (such as "phrasing content" or "interactive content") plus a few special
782
	*   categories created to cover the parts of the specs that refer to "a group of X and Y
783
	*   elements" rather than a specific content model.
784
	*
785
	*   "ac" represents the categories that are allowed as children of given element.
786
	*
787
	*   "dd" represents the categories that must not appear as a descendant of given element.
788
	*
789
	* Sometimes, HTML5 specifies some restrictions on when an element can accept certain children,
790
	* or what categories the element belongs to. For example, an <img> element is only part of the
791
	* "interactive content" category if it has a "usemap" attribute. Those restrictions are
792
	* expressed as an XPath expression and stored using the concatenation of the key of the bitfield
793
	* plus the bit number of the category. For instance, if "interactive content" got assigned to
794
	* bit 2, the definition of the <img> element will contain a key "c2" with value "@usemap".
795
	*
796
	* Additionally, other flags are set:
797
	*
798
	*   "t" indicates that the element uses the "transparent" content model.
799
	*   "e" indicates that the element uses the "empty" content model.
800
	*   "v" indicates that the element is a void element.
801
	*   "nt" indicates that the element does not accept text nodes. (no text)
802
	*   "to" indicates that the element should only contain text. (text-only)
803
	*   "fe" indicates that the element is a formatting element. It will automatically be reopened
804
	*   when closed by an end tag of a different name.
805
	*   "b" indicates that the element is not phrasing content, which makes it likely to act like
806
	*   a block element.
807
	*
808
	* Finally, HTML5 defines "optional end tag" rules, where one element automatically closes its
809
	* predecessor. Those are used to generate closeParent rules and are stored in the "cp" key.
810
	*
811
	* @var array
812
	* @see /scripts/patchTemplateInspector.php
813
	*/
814
	protected static $htmlElements = [
815
		'a'=>['c'=>"\17\0\0\0\0\1",'c3'=>'@href','ac'=>"\0",'dd'=>"\10\0\0\0\0\1",'t'=>1,'fe'=>1],
816
		'abbr'=>['c'=>"\7",'ac'=>"\4"],
817
		'address'=>['c'=>"\3\40",'ac'=>"\1",'dd'=>"\0\45",'b'=>1,'cp'=>['p']],
818
		'article'=>['c'=>"\3\4",'ac'=>"\1",'b'=>1,'cp'=>['p']],
819
		'aside'=>['c'=>"\3\4",'ac'=>"\1",'dd'=>"\0\0\0\0\10",'b'=>1,'cp'=>['p']],
820
		'audio'=>['c'=>"\57",'c3'=>'@controls','c1'=>'@controls','ac'=>"\0\0\0\104",'ac26'=>'not(@src)','dd'=>"\0\0\0\0\0\2",'dd41'=>'@src','t'=>1],
821
		'b'=>['c'=>"\7",'ac'=>"\4",'fe'=>1],
822
		'base'=>['c'=>"\20",'nt'=>1,'e'=>1,'v'=>1,'b'=>1],
823
		'bdi'=>['c'=>"\7",'ac'=>"\4"],
824
		'bdo'=>['c'=>"\7",'ac'=>"\4"],
825
		'blockquote'=>['c'=>"\203",'ac'=>"\1",'b'=>1,'cp'=>['p']],
826
		'body'=>['c'=>"\200\0\4",'ac'=>"\1",'b'=>1],
827
		'br'=>['c'=>"\5",'nt'=>1,'e'=>1,'v'=>1],
828
		'button'=>['c'=>"\117",'ac'=>"\4",'dd'=>"\10"],
829
		'canvas'=>['c'=>"\47",'ac'=>"\0",'t'=>1],
830
		'caption'=>['c'=>"\0\2",'ac'=>"\1",'dd'=>"\0\0\0\200",'b'=>1],
831
		'cite'=>['c'=>"\7",'ac'=>"\4"],
832
		'code'=>['c'=>"\7",'ac'=>"\4",'fe'=>1],
833
		'col'=>['c'=>"\0\0\20",'nt'=>1,'e'=>1,'v'=>1,'b'=>1],
834
		'colgroup'=>['c'=>"\0\2",'ac'=>"\0\0\20",'ac20'=>'not(@span)','nt'=>1,'e'=>1,'e0'=>'@span','b'=>1],
835
		'data'=>['c'=>"\7",'ac'=>"\4"],
836
		'datalist'=>['c'=>"\5",'ac'=>"\4\200\0\10"],
837
		'dd'=>['c'=>"\0\0\200",'ac'=>"\1",'b'=>1,'cp'=>['dd','dt']],
838
		'del'=>['c'=>"\5",'ac'=>"\0",'t'=>1],
839
		'details'=>['c'=>"\213",'ac'=>"\1\0\0\2",'b'=>1,'cp'=>['p']],
840
		'dfn'=>['c'=>"\7\0\0\0\40",'ac'=>"\4",'dd'=>"\0\0\0\0\40"],
841
		'div'=>['c'=>"\3",'ac'=>"\1",'b'=>1,'cp'=>['p']],
842
		'dl'=>['c'=>"\3",'c1'=>'dt and dd','ac'=>"\0\200\200",'nt'=>1,'b'=>1,'cp'=>['p']],
843
		'dt'=>['c'=>"\0\0\200",'ac'=>"\1",'dd'=>"\0\5\0\40",'b'=>1,'cp'=>['dd','dt']],
844
		'em'=>['c'=>"\7",'ac'=>"\4",'fe'=>1],
845
		'embed'=>['c'=>"\57",'nt'=>1,'e'=>1,'v'=>1],
846
		'fieldset'=>['c'=>"\303",'ac'=>"\1\0\0\20",'b'=>1,'cp'=>['p']],
847
		'figcaption'=>['c'=>"\0\0\0\0\0\4",'ac'=>"\1",'b'=>1,'cp'=>['p']],
848
		'figure'=>['c'=>"\203",'ac'=>"\1\0\0\0\0\4",'b'=>1,'cp'=>['p']],
849
		'footer'=>['c'=>"\3\40",'ac'=>"\1",'dd'=>"\0\0\0\0\10",'b'=>1,'cp'=>['p']],
850
		'form'=>['c'=>"\3\0\0\0\20",'ac'=>"\1",'dd'=>"\0\0\0\0\20",'b'=>1,'cp'=>['p']],
851
		'h1'=>['c'=>"\3\1",'ac'=>"\4",'b'=>1,'cp'=>['p']],
852
		'h2'=>['c'=>"\3\1",'ac'=>"\4",'b'=>1,'cp'=>['p']],
853
		'h3'=>['c'=>"\3\1",'ac'=>"\4",'b'=>1,'cp'=>['p']],
854
		'h4'=>['c'=>"\3\1",'ac'=>"\4",'b'=>1,'cp'=>['p']],
855
		'h5'=>['c'=>"\3\1",'ac'=>"\4",'b'=>1,'cp'=>['p']],
856
		'h6'=>['c'=>"\3\1",'ac'=>"\4",'b'=>1,'cp'=>['p']],
857
		'head'=>['c'=>"\0\0\4",'ac'=>"\20",'nt'=>1,'b'=>1],
858
		'header'=>['c'=>"\3\40\0\40",'ac'=>"\1",'dd'=>"\0\0\0\0\10",'b'=>1,'cp'=>['p']],
859
		'hr'=>['c'=>"\1\100",'nt'=>1,'e'=>1,'v'=>1,'b'=>1,'cp'=>['p']],
860
		'html'=>['c'=>"\0",'ac'=>"\0\0\4",'nt'=>1,'b'=>1],
861
		'i'=>['c'=>"\7",'ac'=>"\4",'fe'=>1],
862
		'iframe'=>['c'=>"\57",'ac'=>"\4"],
863
		'img'=>['c'=>"\57\20\10",'c3'=>'@usemap','nt'=>1,'e'=>1,'v'=>1],
864
		'input'=>['c'=>"\17\20",'c3'=>'@type!="hidden"','c12'=>'@type!="hidden" or @type="hidden"','c1'=>'@type!="hidden"','nt'=>1,'e'=>1,'v'=>1],
865
		'ins'=>['c'=>"\7",'ac'=>"\0",'t'=>1],
866
		'kbd'=>['c'=>"\7",'ac'=>"\4"],
867
		'keygen'=>['c'=>"\117",'nt'=>1,'e'=>1,'v'=>1],
868
		'label'=>['c'=>"\17\20\0\0\4",'ac'=>"\4",'dd'=>"\0\0\1\0\4"],
869
		'legend'=>['c'=>"\0\0\0\20",'ac'=>"\4",'b'=>1],
870
		'li'=>['c'=>"\0\0\0\0\200",'ac'=>"\1",'b'=>1,'cp'=>['li']],
871
		'link'=>['c'=>"\20",'nt'=>1,'e'=>1,'v'=>1,'b'=>1],
872
		'main'=>['c'=>"\3\0\0\0\10",'ac'=>"\1",'b'=>1,'cp'=>['p']],
873
		'mark'=>['c'=>"\7",'ac'=>"\4"],
874
		'media element'=>['c'=>"\0\0\0\0\0\2",'nt'=>1,'b'=>1],
875
		'menu'=>['c'=>"\1\100",'ac'=>"\0\300",'nt'=>1,'b'=>1,'cp'=>['p']],
876
		'menuitem'=>['c'=>"\0\100",'nt'=>1,'e'=>1,'v'=>1,'b'=>1],
877
		'meta'=>['c'=>"\20",'nt'=>1,'e'=>1,'v'=>1,'b'=>1],
878
		'meter'=>['c'=>"\7\0\1\0\2",'ac'=>"\4",'dd'=>"\0\0\0\0\2"],
879
		'nav'=>['c'=>"\3\4",'ac'=>"\1",'dd'=>"\0\0\0\0\10",'b'=>1,'cp'=>['p']],
880
		'noscript'=>['c'=>"\25",'nt'=>1],
881
		'object'=>['c'=>"\147",'ac'=>"\0\0\0\0\1",'t'=>1],
882
		'ol'=>['c'=>"\3",'c1'=>'li','ac'=>"\0\200\0\0\200",'nt'=>1,'b'=>1,'cp'=>['p']],
883
		'optgroup'=>['c'=>"\0\0\2",'ac'=>"\0\200\0\10",'nt'=>1,'b'=>1,'cp'=>['optgroup','option']],
884
		'option'=>['c'=>"\0\0\2\10",'b'=>1,'cp'=>['option']],
885
		'output'=>['c'=>"\107",'ac'=>"\4"],
886
		'p'=>['c'=>"\3",'ac'=>"\4",'b'=>1,'cp'=>['p']],
887
		'param'=>['c'=>"\0\0\0\0\1",'nt'=>1,'e'=>1,'v'=>1,'b'=>1],
888
		'picture'=>['c'=>"\45",'ac'=>"\0\200\10",'nt'=>1],
889
		'pre'=>['c'=>"\3",'ac'=>"\4",'pre'=>1,'b'=>1,'cp'=>['p']],
890
		'progress'=>['c'=>"\7\0\1\1",'ac'=>"\4",'dd'=>"\0\0\0\1"],
891
		'q'=>['c'=>"\7",'ac'=>"\4"],
892
		'rb'=>['c'=>"\0\10",'ac'=>"\4",'b'=>1],
893
		'rp'=>['c'=>"\0\10\100",'ac'=>"\4",'b'=>1,'cp'=>['rp','rt']],
894
		'rt'=>['c'=>"\0\10\100",'ac'=>"\4",'b'=>1,'cp'=>['rp','rt']],
895
		'rtc'=>['c'=>"\0\10",'ac'=>"\4\0\100",'b'=>1],
896
		'ruby'=>['c'=>"\7",'ac'=>"\4\10"],
897
		's'=>['c'=>"\7",'ac'=>"\4",'fe'=>1],
898
		'samp'=>['c'=>"\7",'ac'=>"\4"],
899
		'script'=>['c'=>"\25\200",'to'=>1],
900
		'section'=>['c'=>"\3\4",'ac'=>"\1",'b'=>1,'cp'=>['p']],
901
		'select'=>['c'=>"\117",'ac'=>"\0\200\2",'nt'=>1],
902
		'small'=>['c'=>"\7",'ac'=>"\4",'fe'=>1],
903
		'source'=>['c'=>"\0\0\10\4",'nt'=>1,'e'=>1,'v'=>1,'b'=>1],
904
		'span'=>['c'=>"\7",'ac'=>"\4"],
905
		'strong'=>['c'=>"\7",'ac'=>"\4",'fe'=>1],
906
		'style'=>['c'=>"\20",'to'=>1,'b'=>1],
907
		'sub'=>['c'=>"\7",'ac'=>"\4"],
908
		'summary'=>['c'=>"\0\0\0\2",'ac'=>"\4\1",'b'=>1],
909
		'sup'=>['c'=>"\7",'ac'=>"\4"],
910
		'table'=>['c'=>"\3\0\0\200",'ac'=>"\0\202",'nt'=>1,'b'=>1,'cp'=>['p']],
911
		'tbody'=>['c'=>"\0\2",'ac'=>"\0\200\0\0\100",'nt'=>1,'b'=>1,'cp'=>['tbody','td','tfoot','th','thead','tr']],
912
		'td'=>['c'=>"\200\0\40",'ac'=>"\1",'b'=>1,'cp'=>['td','th']],
913
		'template'=>['c'=>"\25\200\20",'nt'=>1],
914
		'textarea'=>['c'=>"\117",'pre'=>1,'to'=>1],
915
		'tfoot'=>['c'=>"\0\2",'ac'=>"\0\200\0\0\100",'nt'=>1,'b'=>1,'cp'=>['tbody','td','th','thead','tr']],
916
		'th'=>['c'=>"\0\0\40",'ac'=>"\1",'dd'=>"\0\5\0\40",'b'=>1,'cp'=>['td','th']],
917
		'thead'=>['c'=>"\0\2",'ac'=>"\0\200\0\0\100",'nt'=>1,'b'=>1],
918
		'time'=>['c'=>"\7",'ac'=>"\4",'ac2'=>'@datetime'],
919
		'title'=>['c'=>"\20",'to'=>1,'b'=>1],
920
		'tr'=>['c'=>"\0\2\0\0\100",'ac'=>"\0\200\40",'nt'=>1,'b'=>1,'cp'=>['td','th','tr']],
921
		'track'=>['c'=>"\0\0\0\100",'nt'=>1,'e'=>1,'v'=>1,'b'=>1],
922
		'u'=>['c'=>"\7",'ac'=>"\4",'fe'=>1],
923
		'ul'=>['c'=>"\3",'c1'=>'li','ac'=>"\0\200\0\0\200",'nt'=>1,'b'=>1,'cp'=>['p']],
924
		'var'=>['c'=>"\7",'ac'=>"\4"],
925
		'video'=>['c'=>"\57",'c3'=>'@controls','ac'=>"\0\0\0\104",'ac26'=>'not(@src)','dd'=>"\0\0\0\0\0\2",'dd41'=>'@src','t'=>1],
926
		'wbr'=>['c'=>"\5",'nt'=>1,'e'=>1,'v'=>1]
927
	];
928
929
	/**
930
	* Get the bitfield value for a given element name in a given context
931
	*
932
	* @param  string     $elName Name of the HTML element
933
	* @param  string     $k      Bitfield name: either 'c', 'ac' or 'dd'
934
	* @param  DOMElement $node   Context node (not necessarily the same as $elName)
935
	* @return string
936
	*/
937
	protected function getBitfield($elName, $k, DOMElement $node)
938
	{
939
		if (!isset(self::$htmlElements[$elName][$k]))
940
		{
941
			return "\0";
942
		}
943
944
		$bitfield = self::$htmlElements[$elName][$k];
945
		foreach (str_split($bitfield, 1) as $byteNumber => $char)
946
		{
947
			$byteValue = ord($char);
948
			for ($bitNumber = 0; $bitNumber < 8; ++$bitNumber)
949
			{
950
				$bitValue = 1 << $bitNumber;
951
				if (!($byteValue & $bitValue))
952
				{
953
					// The bit is not set
954
					continue;
955
				}
956
957
				$n = $byteNumber * 8 + $bitNumber;
958
959
				// Test for an XPath condition for that category
960
				if (isset(self::$htmlElements[$elName][$k . $n]))
961
				{
962
					$xpath = 'boolean(' . self::$htmlElements[$elName][$k . $n] . ')';
963
964
					// If the XPath condition is not fulfilled...
965
					if (!$this->evaluate($xpath, $node))
966
					{
967
						// ...turn off the corresponding bit
968
						$byteValue ^= $bitValue;
969
970
						// Update the original bitfield
971
						$bitfield[$byteNumber] = chr($byteValue);
972
					}
973
				}
974
			}
975
		}
976
977
		return $bitfield;
978
	}
979
980
	/**
981
	* Test whether given element has given property in context
982
	*
983
	* @param  string     $elName   Element name
984
	* @param  string     $propName Property name, see self::$htmlElements
985
	* @param  DOMElement $node     Context node
986
	* @return bool
987
	*/
988
	protected function hasProperty($elName, $propName, DOMElement $node)
989
	{
990
		if (!empty(self::$htmlElements[$elName][$propName]))
991
		{
992
			// Test the XPath condition
993
			if (!isset(self::$htmlElements[$elName][$propName . '0'])
994
			 || $this->evaluate(self::$htmlElements[$elName][$propName . '0'], $node))
995
			{
996
				return true;
997
			}
998
		}
999
1000
		return false;
1001
	}
1002
1003
	/**
1004
	* Test whether two bitfields have any bits in common
1005
	*
1006
	* @param  string $bitfield1
1007
	* @param  string $bitfield2
1008
	* @return bool
1009
	*/
1010
	protected static function match($bitfield1, $bitfield2)
1011
	{
1012
		return (trim($bitfield1 & $bitfield2, "\0") !== '');
1013
	}
1014
}