Verzeichnisstruktur phpBB-3.3.15


Veröffentlicht
28.08.2024

So funktioniert es


Auf das letzte Element klicken. Dies geht jeweils ein Schritt zurück

Auf das Icon klicken, dies öffnet das Verzeichnis. Nochmal klicken schließt das Verzeichnis.
Auf den Verzeichnisnamen klicken, dies zeigt nur das Verzeichnis mit Inhalt an

(Beispiel Datei-Icons)

Auf das Icon klicken um den Quellcode anzuzeigen

TemplateInspector.php

Zuletzt modifiziert: 02.04.2025, 15:04 - Dateigröße: 16.64 KiB


001  <?php
002   
003  /**
004  * @package   s9e\TextFormatter
005  * @copyright Copyright (c) 2010-2022 The s9e authors
006  * @license   http://www.opensource.org/licenses/mit-license.php The MIT License
007  */
008  namespace s9e\TextFormatter\Configurator\Helpers;
009   
010  use DOMElement;
011  use DOMXPath;
012   
013  /**
014  * This class helps the RulesGenerator by analyzing a given template in order to answer questions
015  * such as "can this tag be a child/descendant of that other tag?" and others related to the HTML5
016  * content model.
017  *
018  * We use the HTML5 specs to determine which children or descendants should be allowed or denied
019  * based on HTML5 content models. While it does not exactly match HTML5 content models, it gets
020  * pretty close. We also use HTML5 "optional end tag" rules to create closeParent rules.
021  *
022  * Currently, this method does not evaluate elements created with <xsl:element> correctly, or
023  * attributes created with <xsl:attribute> and may never will due to the increased complexity it
024  * would entail. Additionally, it does not evaluate the scope of <xsl:apply-templates/>. For
025  * instance, it will treat <xsl:apply-templates select="LI"/> as if it was <xsl:apply-templates/>
026  *
027  * @link http://dev.w3.org/html5/spec/content-models.html#content-models
028  * @link http://dev.w3.org/html5/spec/syntax.html#optional-tags
029  */
030  class TemplateInspector
031  {
032      /**
033      * XSL namespace
034      */
035      const XMLNS_XSL = 'http://www.w3.org/1999/XSL/Transform';
036   
037      /**
038      * @var string[] allowChild bitfield for each branch
039      */
040      protected $allowChildBitfields = [];
041   
042      /**
043      * @var bool Whether elements are allowed as children
044      */
045      protected $allowsChildElements;
046   
047      /**
048      * @var bool Whether text nodes are allowed as children
049      */
050      protected $allowsText;
051   
052      /**
053      * @var array[] Array of array of DOMElement instances
054      */
055      protected $branches;
056   
057      /**
058      * @var string OR-ed bitfield representing all of the categories used by this template
059      */
060      protected $contentBitfield = "\0";
061   
062      /**
063      * @var string Default bitfield used at the root of a branch
064      */
065      protected $defaultBranchBitfield;
066   
067      /**
068      * @var string denyDescendant bitfield
069      */
070      protected $denyDescendantBitfield = "\0";
071   
072      /**
073      * @var \DOMDocument Document containing the template
074      */
075      protected $dom;
076   
077      /**
078      * @var bool Whether this template contains any HTML elements
079      */
080      protected $hasElements = false;
081   
082      /**
083      * @var bool Whether this template renders non-whitespace text nodes at its root
084      */
085      protected $hasRootText;
086   
087      /**
088      * @var bool Whether this template should be considered a block-level element
089      */
090      protected $isBlock = false;
091   
092      /**
093      * @var bool Whether the template uses the "empty" content model
094      */
095      protected $isEmpty;
096   
097      /**
098      * @var bool Whether this template adds to the list of active formatting elements
099      */
100      protected $isFormattingElement;
101   
102      /**
103      * @var bool Whether this template lets content through via an xsl:apply-templates element
104      */
105      protected $isPassthrough = false;
106   
107      /**
108      * @var bool Whether all branches use the transparent content model
109      */
110      protected $isTransparent = false;
111   
112      /**
113      * @var bool Whether all branches have an ancestor that is a void element
114      */
115      protected $isVoid;
116   
117      /**
118      * @var array Last HTML element that precedes an <xsl:apply-templates/> node
119      */
120      protected $leafNodes = [];
121   
122      /**
123      * @var bool Whether any branch has an element that preserves new lines by default (e.g. <pre>)
124      */
125      protected $preservesNewLines = false;
126   
127      /**
128      * @var array Bitfield of the first HTML element of every branch
129      */
130      protected $rootBitfields = [];
131   
132      /**
133      * @var array Every HTML element that has no HTML parent
134      */
135      protected $rootNodes = [];
136   
137      /**
138      * @var DOMXPath XPath engine associated with $this->dom
139      */
140      protected $xpath;
141   
142      /**
143      * Constructor
144      *
145      * @param string $template Template content
146      */
147      public function __construct($template)
148      {
149          $this->dom   = TemplateLoader::load($template);
150          $this->xpath = new DOMXPath($this->dom);
151   
152          $this->defaultBranchBitfield = ElementInspector::getAllowChildBitfield($this->dom->createElement('div'));
153   
154          $this->analyseRootNodes();
155          $this->analyseBranches();
156          $this->analyseContent();
157      }
158   
159      /**
160      * Return whether this template allows a given child
161      *
162      * @param  TemplateInspector $child
163      * @return bool
164      */
165      public function allowsChild(TemplateInspector $child)
166      {
167          // Sometimes, a template can technically be allowed as a child but denied as a descendant
168          if (!$this->allowsDescendant($child))
169          {
170              return false;
171          }
172   
173          foreach ($child->rootBitfields as $rootBitfield)
174          {
175              foreach ($this->allowChildBitfields as $allowChildBitfield)
176              {
177                  if (!self::match($rootBitfield, $allowChildBitfield))
178                  {
179                      return false;
180                  }
181              }
182          }
183   
184          return ($this->allowsText || !$child->hasRootText);
185      }
186   
187      /**
188      * Return whether this template allows a given descendant
189      *
190      * @param  TemplateInspector $descendant
191      * @return bool
192      */
193      public function allowsDescendant(TemplateInspector $descendant)
194      {
195          // Test whether the descendant is explicitly disallowed
196          if (self::match($descendant->contentBitfield, $this->denyDescendantBitfield))
197          {
198              return false;
199          }
200   
201          // Test whether the descendant contains any elements and we disallow elements
202          return ($this->allowsChildElements || !$descendant->hasElements);
203      }
204   
205      /**
206      * Return whether this template allows elements as children
207      *
208      * @return bool
209      */
210      public function allowsChildElements()
211      {
212          return $this->allowsChildElements;
213      }
214   
215      /**
216      * Return whether this template allows text nodes as children
217      *
218      * @return bool
219      */
220      public function allowsText()
221      {
222          return $this->allowsText;
223      }
224   
225      /**
226      * Return whether this template automatically closes given parent template
227      *
228      * @param  TemplateInspector $parent
229      * @return bool
230      */
231      public function closesParent(TemplateInspector $parent)
232      {
233          // Test whether any of this template's root nodes closes any of given template's leaf nodes
234          foreach ($this->rootNodes as $rootNode)
235          {
236              foreach ($parent->leafNodes as $leafNode)
237              {
238                  if (ElementInspector::closesParent($rootNode, $leafNode))
239                  {
240                      return true;
241                  }
242              }
243          }
244   
245          return false;
246      }
247   
248      /**
249      * Evaluate an XPath expression
250      *
251      * @param  string     $expr XPath expression
252      * @param  DOMElement $node Context node
253      * @return mixed
254      */
255      public function evaluate($expr, DOMElement $node = null)
256      {
257          return $this->xpath->evaluate($expr, $node);
258      }
259   
260      /**
261      * Return whether this template should be considered a block-level element
262      *
263      * @return bool
264      */
265      public function isBlock()
266      {
267          return $this->isBlock;
268      }
269   
270      /**
271      * Return whether this template adds to the list of active formatting elements
272      *
273      * @return bool
274      */
275      public function isFormattingElement()
276      {
277          return $this->isFormattingElement;
278      }
279   
280      /**
281      * Return whether this template uses the "empty" content model
282      *
283      * @return bool
284      */
285      public function isEmpty()
286      {
287          return $this->isEmpty;
288      }
289   
290      /**
291      * Return whether this template lets content through via an xsl:apply-templates element
292      *
293      * @return bool
294      */
295      public function isPassthrough()
296      {
297          return $this->isPassthrough;
298      }
299   
300      /**
301      * Return whether this template uses the "transparent" content model
302      *
303      * @return bool
304      */
305      public function isTransparent()
306      {
307          return $this->isTransparent;
308      }
309   
310      /**
311      * Return whether all branches have an ancestor that is a void element
312      *
313      * @return bool
314      */
315      public function isVoid()
316      {
317          return $this->isVoid;
318      }
319   
320      /**
321      * Return whether this template preserves the whitespace in its descendants
322      *
323      * @return bool
324      */
325      public function preservesNewLines()
326      {
327          return $this->preservesNewLines;
328      }
329   
330      /**
331      * Analyses the content of the whole template and set $this->contentBitfield accordingly
332      */
333      protected function analyseContent()
334      {
335          // Get all non-XSL elements
336          $query = '//*[namespace-uri() != "' . self::XMLNS_XSL . '"]';
337          foreach ($this->xpath->query($query) as $node)
338          {
339              $this->contentBitfield |= ElementInspector::getCategoryBitfield($node);
340              $this->hasElements = true;
341          }
342   
343          // Test whether this template is passthrough
344          $this->isPassthrough = (bool) $this->evaluate('count(//xsl:apply-templates)');
345      }
346   
347      /**
348      * Records the HTML elements (and their bitfield) rendered at the root of the template
349      */
350      protected function analyseRootNodes()
351      {
352          // Get every non-XSL element with no non-XSL ancestor. This should return us the first
353          // HTML element of every branch
354          $query = '//*[namespace-uri() != "' . self::XMLNS_XSL . '"]'
355                 . '[not(ancestor::*[namespace-uri() != "' . self::XMLNS_XSL . '"])]';
356          foreach ($this->xpath->query($query) as $node)
357          {
358              // Store the root node of this branch
359              $this->rootNodes[] = $node;
360   
361              // If any root node is a block-level element, we'll mark the template as such
362              if ($this->elementIsBlock($node))
363              {
364                  $this->isBlock = true;
365              }
366   
367              $this->rootBitfields[] = ElementInspector::getCategoryBitfield($node);
368          }
369   
370          // Test for non-whitespace text nodes at the root. For that we need a predicate that filters
371          // out: nodes with a non-XSL ancestor,
372          $predicate = '[not(ancestor::*[namespace-uri() != "' . self::XMLNS_XSL . '"])]';
373   
374          // ..and nodes with an <xsl:attribute/>, <xsl:comment/> or <xsl:variable/> ancestor
375          $predicate .= '[not(ancestor::xsl:attribute | ancestor::xsl:comment | ancestor::xsl:variable)]';
376   
377          $query = '//text()[normalize-space() != ""]' . $predicate
378                 . '|'
379                 . '//xsl:text[normalize-space() != ""]' . $predicate
380                 . '|'
381                 . '//xsl:value-of' . $predicate;
382   
383          $this->hasRootText = (bool) $this->evaluate('count(' . $query . ')');
384      }
385   
386      /**
387      * Analyses each branch that leads to an <xsl:apply-templates/> tag
388      */
389      protected function analyseBranches()
390      {
391          $this->branches = [];
392          foreach ($this->xpath->query('//xsl:apply-templates') as $applyTemplates)
393          {
394              $query            = 'ancestor::*[namespace-uri() != "' . self::XMLNS_XSL . '"]';
395              $this->branches[] = iterator_to_array($this->xpath->query($query, $applyTemplates));
396          }
397   
398          $this->computeAllowsChildElements();
399          $this->computeAllowsText();
400          $this->computeBitfields();
401          $this->computeFormattingElement();
402          $this->computeIsEmpty();
403          $this->computeIsTransparent();
404          $this->computeIsVoid();
405          $this->computePreservesNewLines();
406          $this->storeLeafNodes();
407      }
408   
409      /**
410      * Test whether any branch of this template has an element that has given property
411      *
412      * @param  string $methodName
413      * @return bool
414      */
415      protected function anyBranchHasProperty($methodName)
416      {
417          foreach ($this->branches as $branch)
418          {
419              foreach ($branch as $element)
420              {
421                  if (ElementInspector::$methodName($element))
422                  {
423                      return true;
424                  }
425              }
426          }
427   
428          return false;
429      }
430   
431      /**
432      * Compute the allowChildBitfields and denyDescendantBitfield properties
433      *
434      * @return void
435      */
436      protected function computeBitfields()
437      {
438          if (empty($this->branches))
439          {
440              $this->allowChildBitfields = ["\0"];
441   
442              return;
443          }
444          foreach ($this->branches as $branch)
445          {
446              /**
447              * @var string allowChild bitfield for current branch. Starts with the value associated
448              *             with <div> in order to approximate a value if the whole branch uses the
449              *             transparent content model
450              */
451              $branchBitfield = $this->defaultBranchBitfield;
452   
453              foreach ($branch as $element)
454              {
455                  if (!ElementInspector::isTransparent($element))
456                  {
457                      // If the element isn't transparent, we reset its bitfield
458                      $branchBitfield = "\0";
459                  }
460   
461                  // allowChild rules are cumulative if transparent, and reset above otherwise
462                  $branchBitfield |= ElementInspector::getAllowChildBitfield($element);
463   
464                  // denyDescendant rules are cumulative
465                  $this->denyDescendantBitfield |= ElementInspector::getDenyDescendantBitfield($element);
466              }
467   
468              // Add this branch's bitfield to the list
469              $this->allowChildBitfields[] = $branchBitfield;
470          }
471      }
472   
473      /**
474      * Compute the allowsChildElements property
475      *
476      * A template allows child Elements if it has at least one xsl:apply-templates and none of its
477      * ancestors have the text-only ("to") property
478      *
479      * @return void
480      */
481      protected function computeAllowsChildElements()
482      {
483          $this->allowsChildElements = ($this->anyBranchHasProperty('isTextOnly')) ? false : !empty($this->branches);
484      }
485   
486      /**
487      * Compute the allowsText property
488      *
489      * A template is said to allow text if none of the leaf elements disallow text
490      *
491      * @return void
492      */
493      protected function computeAllowsText()
494      {
495          foreach (array_filter($this->branches) as $branch)
496          {
497              if (ElementInspector::disallowsText(end($branch)))
498              {
499                  $this->allowsText = false;
500   
501                  return;
502              }
503          }
504          $this->allowsText = true;
505      }
506   
507      /**
508      * Compute the isFormattingElement property
509      *
510      * A template is said to be a formatting element if all (non-zero) of its branches are entirely
511      * composed of formatting elements
512      *
513      * @return void
514      */
515      protected function computeFormattingElement()
516      {
517          foreach ($this->branches as $branch)
518          {
519              foreach ($branch as $element)
520              {
521                  if (!ElementInspector::isFormattingElement($element) && !$this->isFormattingSpan($element))
522                  {
523                      $this->isFormattingElement = false;
524   
525                      return;
526                  }
527              }
528          }
529          $this->isFormattingElement = (bool) count(array_filter($this->branches));
530      }
531   
532      /**
533      * Compute the isEmpty property
534      *
535      * A template is said to be empty if it has no xsl:apply-templates elements or any there is a empty
536      * element ancestor to an xsl:apply-templates element
537      *
538      * @return void
539      */
540      protected function computeIsEmpty()
541      {
542          $this->isEmpty = ($this->anyBranchHasProperty('isEmpty')) || empty($this->branches);
543      }
544   
545      /**
546      * Compute the isTransparent property
547      *
548      * A template is said to be transparent if it has at least one branch and no non-transparent
549      * elements in its path
550      *
551      * @return void
552      */
553      protected function computeIsTransparent()
554      {
555          foreach ($this->branches as $branch)
556          {
557              foreach ($branch as $element)
558              {
559                  if (!ElementInspector::isTransparent($element))
560                  {
561                      $this->isTransparent = false;
562   
563                      return;
564                  }
565              }
566          }
567          $this->isTransparent = !empty($this->branches);
568      }
569   
570      /**
571      * Compute the isVoid property
572      *
573      * A template is said to be void if it has no xsl:apply-templates elements or any there is a void
574      * element ancestor to an xsl:apply-templates element
575      *
576      * @return void
577      */
578      protected function computeIsVoid()
579      {
580          $this->isVoid = ($this->anyBranchHasProperty('isVoid')) || empty($this->branches);
581      }
582   
583      /**
584      * Compute the preservesNewLines property
585      *
586      * @return void
587      */
588      protected function computePreservesNewLines()
589      {
590          foreach ($this->branches as $branch)
591          {
592              $style = '';
593              foreach ($branch as $element)
594              {
595                  $style .= $this->getStyle($element, true);
596              }
597   
598              if (preg_match('(.*white-space\\s*:\\s*(no|pre))is', $style, $m) && strtolower($m[1]) === 'pre')
599              {
600                  $this->preservesNewLines = true;
601   
602                  return;
603              }
604          }
605          $this->preservesNewLines = false;
606      }
607   
608      /**
609      * Test whether given element is a block-level element
610      *
611      * @param  DOMElement $element
612      * @return bool
613      */
614      protected function elementIsBlock(DOMElement $element)
615      {
616          $style = $this->getStyle($element);
617          if (preg_match('(\\bdisplay\\s*:\\s*block)i', $style))
618          {
619              return true;
620          }
621          if (preg_match('(\\bdisplay\\s*:\\s*(?:inli|no)ne)i', $style))
622          {
623              return false;
624          }
625   
626          return ElementInspector::isBlock($element);
627      }
628   
629      /**
630      * Retrieve and return the inline style assigned to given element
631      *
632      * @param  DOMElement $node Context node
633      * @param  bool       $deep Whether to retrieve the content of all xsl:attribute descendants
634      * @return string
635      */
636      protected function getStyle(DOMElement $node, $deep = false)
637      {
638          $style = '';
639          if (ElementInspector::preservesWhitespace($node))
640          {
641              $style .= 'white-space:pre;';
642          }
643          $style .= $node->getAttribute('style');
644   
645          // Add the content of any descendant/child xsl:attribute named "style"
646          $query = (($deep) ? './/' : './') . 'xsl:attribute[@name="style"]';
647          foreach ($this->xpath->query($query, $node) as $attribute)
648          {
649              $style .= ';' . $attribute->textContent;
650          }
651   
652          return $style;
653      }
654   
655      /**
656      * Test whether given node is a span element used for formatting
657      *
658      * Will return TRUE if the node is a span element with a class attribute and/or a style attribute
659      * and no other attributes
660      *
661      * @param  DOMElement $node
662      * @return boolean
663      */
664      protected function isFormattingSpan(DOMElement $node)
665      {
666          if ($node->nodeName !== 'span')
667          {
668              return false;
669          }
670   
671          if ($node->getAttribute('class') === '' && $node->getAttribute('style') === '')
672          {
673              return false;
674          }
675   
676          foreach ($node->attributes as $attrName => $attribute)
677          {
678              if ($attrName !== 'class' && $attrName !== 'style')
679              {
680                  return false;
681              }
682          }
683   
684          return true;
685      }
686   
687      /**
688      * Store the names of every leaf node
689      *
690      * A leaf node is defined as the closest non-XSL ancestor to an xsl:apply-templates element
691      *
692      * @return void
693      */
694      protected function storeLeafNodes()
695      {
696          foreach (array_filter($this->branches) as $branch)
697          {
698              $this->leafNodes[] = end($branch);
699          }
700      }
701   
702      /**
703      * Test whether two bitfields have any bits in common
704      *
705      * @param  string $bitfield1
706      * @param  string $bitfield2
707      * @return bool
708      */
709      protected static function match($bitfield1, $bitfield2)
710      {
711          return (trim($bitfield1 & $bitfield2, "\0") !== '');
712      }
713  }