Verzeichnisstruktur phpBB-3.3.16


Veröffentlicht
27.04.2026

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: 01.05.2026, 11:28 - 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  }