[ Index ]

PHP Cross Reference of phpBB-3.3.14-deutsch

title

Body

[close]

/vendor/s9e/text-formatter/src/Configurator/Helpers/ -> TemplateInspector.php (source)

   1  <?php
   2  
   3  /**
   4  * @package   s9e\TextFormatter
   5  * @copyright Copyright (c) 2010-2022 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 DOMElement;
  11  use DOMXPath;
  12  
  13  /**
  14  * This class helps the RulesGenerator by analyzing a given template in order to answer questions
  15  * such as "can this tag be a child/descendant of that other tag?" and others related to the HTML5
  16  * content model.
  17  *
  18  * We use the HTML5 specs to determine which children or descendants should be allowed or denied
  19  * based on HTML5 content models. While it does not exactly match HTML5 content models, it gets
  20  * pretty close. We also use HTML5 "optional end tag" rules to create closeParent rules.
  21  *
  22  * Currently, this method does not evaluate elements created with <xsl:element> correctly, or
  23  * attributes created with <xsl:attribute> and may never will due to the increased complexity it
  24  * would entail. Additionally, it does not evaluate the scope of <xsl:apply-templates/>. For
  25  * instance, it will treat <xsl:apply-templates select="LI"/> as if it was <xsl:apply-templates/>
  26  *
  27  * @link http://dev.w3.org/html5/spec/content-models.html#content-models
  28  * @link http://dev.w3.org/html5/spec/syntax.html#optional-tags
  29  */
  30  class TemplateInspector
  31  {
  32      /**
  33      * XSL namespace
  34      */
  35      const XMLNS_XSL = 'http://www.w3.org/1999/XSL/Transform';
  36  
  37      /**
  38      * @var string[] allowChild bitfield for each branch
  39      */
  40      protected $allowChildBitfields = [];
  41  
  42      /**
  43      * @var bool Whether elements are allowed as children
  44      */
  45      protected $allowsChildElements;
  46  
  47      /**
  48      * @var bool Whether text nodes are allowed as children
  49      */
  50      protected $allowsText;
  51  
  52      /**
  53      * @var array[] Array of array of DOMElement instances
  54      */
  55      protected $branches;
  56  
  57      /**
  58      * @var string OR-ed bitfield representing all of the categories used by this template
  59      */
  60      protected $contentBitfield = "\0";
  61  
  62      /**
  63      * @var string Default bitfield used at the root of a branch
  64      */
  65      protected $defaultBranchBitfield;
  66  
  67      /**
  68      * @var string denyDescendant bitfield
  69      */
  70      protected $denyDescendantBitfield = "\0";
  71  
  72      /**
  73      * @var \DOMDocument Document containing the template
  74      */
  75      protected $dom;
  76  
  77      /**
  78      * @var bool Whether this template contains any HTML elements
  79      */
  80      protected $hasElements = false;
  81  
  82      /**
  83      * @var bool Whether this template renders non-whitespace text nodes at its root
  84      */
  85      protected $hasRootText;
  86  
  87      /**
  88      * @var bool Whether this template should be considered a block-level element
  89      */
  90      protected $isBlock = false;
  91  
  92      /**
  93      * @var bool Whether the template uses the "empty" content model
  94      */
  95      protected $isEmpty;
  96  
  97      /**
  98      * @var bool Whether this template adds to the list of active formatting elements
  99      */
 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  }


Generated: Mon Nov 25 19:05:08 2024 Cross-referenced by PHPXref 0.7.1