[ Index ] |
PHP Cross Reference of phpBB-3.3.14-deutsch |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Mon Nov 25 19:05:08 2024 | Cross-referenced by PHPXref 0.7.1 |