1<?php 2/** 3 * Copyright (c) 2021. ComboStrap, Inc. and its affiliates. All Rights Reserved. 4 * 5 * This source code is licensed under the GPL license found in the 6 * COPYING file in the root directory of this source tree. 7 * 8 * @license GPL 3 (https://www.gnu.org/licenses/gpl-3.0.en.html) 9 * @author ComboStrap <support@combostrap.com> 10 * 11 */ 12 13namespace ComboStrap; 14 15use dokuwiki\Extension\SyntaxPlugin; 16use syntax_plugin_combo_cell; 17 18/** 19 * An helper to create manipulate component and html attributes 20 * 21 * You can: 22 * * declare component attribute after parsing 23 * * declare Html attribute during parsing 24 * * output the final HTML attributes at the end of the process with the function {@link TagAttributes::toHTMLAttributeString()} 25 * 26 * Component attributes have precedence on HTML attributes. 27 * 28 * @package ComboStrap 29 */ 30class TagAttributes 31{ 32 /** 33 * @var string the alt attribute value (known as the title for dokuwiki) 34 */ 35 const TITLE_KEY = 'title'; 36 37 38 const TYPE_KEY = "type"; 39 const ID_KEY = "id"; 40 41 /** 42 * The logical attributes that: 43 * * are not becoming HTML attributes 44 * * are never deleted 45 * (ie internal reserved words) 46 */ 47 const RESERVED_ATTRIBUTES = [ 48 self::SCRIPT_KEY, // no script attribute for security reason 49 TagAttributes::TYPE_KEY, // type is the component class 50 MediaLink::LINKING_KEY, // internal to image 51 CacheMedia::CACHE_KEY, // internal also 52 \syntax_plugin_combo_webcode::RENDERING_MODE_ATTRIBUTE, 53 syntax_plugin_combo_cell::VERTICAL_ATTRIBUTE, 54 self::OPEN_TAG, 55 self::HTML_BEFORE, 56 self::HTML_AFTER 57 ]; 58 59 /** 60 * The inline element 61 * We could pass the plugin object into tag attribute in place of the logical tag 62 * and check if the {@link SyntaxPlugin::getPType()} is normal 63 */ 64 const INLINE_LOGICAL_ELEMENTS = [ 65 ImageSvg::CANONICAL, 66 ImageRaster::CANONICAL, 67 Image::CANONICAL, 68 \syntax_plugin_combo_link::TAG, // link button for instance 69 \syntax_plugin_combo_button::TAG, 70 \syntax_plugin_combo_heading::TAG 71 ]; 72 const SCRIPT_KEY = "script"; 73 const TRANSFORM = "transform"; 74 75 const CANONICAL = "tag"; 76 const DISPLAY = "display"; 77 const CLASS_KEY = "class"; 78 const WIKI_ID = "wiki-id"; 79 80 /** 81 * The open tag attributes 82 * permit to not close the tag in {@link TagAttributes::toHtmlEnterTag()} 83 * 84 * It's used for instance by the {@link \syntax_plugin_combo_tooltip} 85 * to advertise that it will add attribute and close it 86 */ 87 const OPEN_TAG = "open-tag"; 88 89 /** 90 * If an attribute has this value, 91 * it will not be added to the output (ie {@link TagAttributes::toHtmlEnterTag()}) 92 * Child element can unset attribute this way 93 * in order to write their own 94 * 95 * This is used by the {@link \syntax_plugin_combo_tooltip} 96 * to advertise that the title attribute should not be set 97 */ 98 const UN_SET = "unset"; 99 100 /** 101 * When wrapping an element 102 * A tag may get HTML before and after 103 * Uses for instance to wrap a svg in span 104 * when adding a {@link \syntax_plugin_combo_tooltip} 105 */ 106 const HTML_BEFORE = "htmlBefore"; 107 const HTML_AFTER = "htmlAfter"; 108 109 /** 110 * A global static counter 111 * to {@link TagAttributes::generateAndSetId()} 112 */ 113 private static $counter = 0; 114 115 116 /** 117 * @var array attribute that were set on a component 118 */ 119 private $componentAttributesCaseInsensitive; 120 121 /** 122 * @var array the style declaration array 123 */ 124 private $styleDeclaration = array(); 125 126 /** 127 * @var bool - set when the transformation from component attribute to html attribute 128 * was done to avoid circular problem 129 */ 130 private $componentToHtmlAttributeProcessingWasDone = false; 131 132 /** 133 * @var array - html attributes set in the code. This is needed to make a difference 134 * on attribute name that are the same such as the component attribute `width` that is 135 * transformed as a style `max-width` but exists also as attribute of an image for instance 136 */ 137 private $htmlAttributes = array(); 138 139 /** 140 * @var array - the final html array 141 */ 142 private $finalHtmlArray = array(); 143 144 /** 145 * @var string the functional tag to which the attributes applies 146 * It's not an HTML tag (a div can have a flex display or a block and they don't carry this information) 147 * The tag gives also context for the attributes (ie an div has no natural width while an img has) 148 */ 149 private $logicalTag; 150 151 /** 152 * An html that should be added after the enter tag 153 * (used for instance to add metadata such as backgrounds, illustration image for cards ,... 154 * @var string 155 */ 156 private $htmlAfterEnterTag; 157 158 /** 159 * Use to make the difference between 160 * an HTTP call for a media (ie SVG) vs an HTTP call for a page (HTML) 161 */ 162 const TEXT_HTML_MIME = "text/html"; 163 private $mime = TagAttributes::TEXT_HTML_MIME; 164 165 166 /** 167 * ComponentAttributes constructor. 168 * Use the static create function to instantiate this object 169 * @param $tag - tag (the tag gives context for the attributes 170 * * an div has no natural width while an img has 171 * * this is not always the component name / syntax name (for instance the {@link \syntax_plugin_combo_codemarkdown} is another syntax 172 * for a {@link \syntax_plugin_combo_code} and have therefore the same logical name) 173 * @param array $componentAttributes 174 */ 175 private function __construct($componentAttributes = array(), $tag = null) 176 { 177 $this->logicalTag = $tag; 178 $this->componentAttributesCaseInsensitive = new ArrayCaseInsensitive($componentAttributes); 179 180 /** 181 * Delete null values 182 * Empty string, 0 may exist 183 */ 184 foreach ($componentAttributes as $key => $value) { 185 if (is_null($value)) { 186 unset($this->componentAttributesCaseInsensitive[$key]); 187 } 188 } 189 190 } 191 192 /** 193 * @param $match - the {@link SyntaxPlugin::handle()} match 194 * @param array $defaultAttributes 195 * @return TagAttributes 196 */ 197 public static function createFromTagMatch($match, $defaultAttributes = []) 198 { 199 $inlineHtmlAttributes = PluginUtility::getTagAttributes($match); 200 $tag = PluginUtility::getTag($match); 201 $mergedAttributes = PluginUtility::mergeAttributes($inlineHtmlAttributes, $defaultAttributes); 202 return self::createFromCallStackArray($mergedAttributes, $tag); 203 } 204 205 206 public static function createEmpty($logicalTag = "") 207 { 208 if ($logicalTag !== "") { 209 return new TagAttributes([], $logicalTag); 210 } else { 211 return new TagAttributes(); 212 } 213 } 214 215 /** 216 * @param array $renderArray - an array of key value pair 217 * @param string $logicalTag - the logical tag for which this attribute will apply 218 * @return TagAttributes 219 */ 220 public static function createFromCallStackArray($renderArray, $logicalTag = null) 221 { 222 if (!is_array($renderArray)) { 223 LogUtility::msg("The renderArray variable passed is not an array ($renderArray)", LogUtility::LVL_MSG_ERROR); 224 $renderArray = TagAttributes::createEmpty($logicalTag); 225 } 226 return new TagAttributes($renderArray, $logicalTag); 227 } 228 229 230 /** 231 * For CSS a unit is mandatory (not for HTML or SVG attributes) 232 * @param $value 233 * @return string return a CSS property with pixel as unit if the unit is not specified 234 */ 235 public static function toQualifiedCssValue($value) 236 { 237 /** 238 * A length value may be also `fit-content` 239 * we just check that if there is only number, 240 * we add the pixel 241 * Same as {@link is_numeric()} ? 242 */ 243 if (is_numeric($value)) { 244 return $value . "px"; 245 } else { 246 return $value; 247 } 248 249 } 250 251 /** 252 * Function used to normalize the attribute name to the combostrap attribute name 253 * @param $name 254 * @return mixed|string 255 */ 256 public static function AttributeNameFromDokuwikiToCombo($name) 257 { 258 switch ($name) { 259 case "w": 260 return Dimension::WIDTH_KEY; 261 case "h": 262 return Dimension::HEIGHT_KEY; 263 default: 264 return $name; 265 } 266 } 267 268 /** 269 * Clone a tag attributes 270 * Tag Attributes are used for request and for response 271 * To avoid conflict, a function should clone it before 272 * calling the final method {@link TagAttributes::toHtmlArray()} 273 * or {@link TagAttributes::toHtmlEnterTag()} 274 * @param TagAttributes $tagAttributes 275 * @return TagAttributes 276 */ 277 public static function createFromTagAttributes(TagAttributes $tagAttributes) 278 { 279 return new TagAttributes($tagAttributes->getComponentAttributes(), $tagAttributes->getLogicalTag()); 280 } 281 282 public function addClassName($className) 283 { 284 285 $this->addComponentAttributeValue(self::CLASS_KEY, $className); 286 return $this; 287 288 } 289 290 public function getClass() 291 { 292 return $this->getValue(self::CLASS_KEY); 293 } 294 295 public function getStyle() 296 { 297 if (sizeof($this->styleDeclaration) != 0) { 298 return PluginUtility::array2InlineStyle($this->styleDeclaration); 299 } else { 300 /** 301 * null is needed to see if the attribute was set or not 302 * because an attribute may have the empty string 303 * Example: the wiki id of the root namespace 304 */ 305 return null; 306 } 307 308 } 309 310 /** 311 * Add an attribute with its value if the value is not empty 312 * @param $attributeName 313 * @param $attributeValue 314 */ 315 public function addComponentAttributeValue($attributeName, $attributeValue) 316 { 317 318 if (empty($attributeValue) && !is_bool($attributeValue)) { 319 LogUtility::msg("The value of the attribute ($attributeName) is empty. Use the nonEmpty function instead", LogUtility::LVL_MSG_WARNING, "support"); 320 } 321 322 $attLower = strtolower($attributeName); 323 if ($this->hasComponentAttribute($attLower)) { 324 $actual = $this->componentAttributesCaseInsensitive[$attLower]; 325 } 326 327 /** 328 * Type of data: list (class) or atomic (id) 329 */ 330 if ($attributeName === "class") { 331 if (!is_string($attributeValue)) { 332 LogUtility::msg("The value ($attributeValue) for the `class` attribute is not a string", LogUtility::LVL_MSG_ERROR, self::CANONICAL); 333 } 334 /** 335 * It may be in the form "value1 value2" 336 */ 337 $newValues = StringUtility::explodeAndTrim($attributeValue, " "); 338 if (!empty($actual)) { 339 $actualValues = StringUtility::explodeAndTrim($actual, " "); 340 } else { 341 $actualValues = []; 342 } 343 $newValues = PluginUtility::mergeAttributes($newValues, $actualValues); 344 $this->componentAttributesCaseInsensitive[$attLower] = implode(" ", $newValues); 345 } else { 346 if (!empty($actual)) { 347 LogUtility::msg("The attribute ($attLower) stores an unique value and has already a value ($actual). to set another value ($attributeValue), use the `set` operation instead", LogUtility::LVL_MSG_ERROR, self::CANONICAL); 348 } 349 $this->componentAttributesCaseInsensitive[$attLower] = $attributeValue; 350 } 351 352 353 } 354 355 public function setComponentAttributeValue($attributeName, $attributeValue) 356 { 357 $attLower = strtolower($attributeName); 358 $actualValue = $this->getValue($attributeName); 359 if ($actualValue === null || $actualValue !== TagAttributes::UN_SET) { 360 $this->componentAttributesCaseInsensitive[$attLower] = $attributeValue; 361 } 362 } 363 364 public function addComponentAttributeValueIfNotEmpty($attributeName, $attributeValue) 365 { 366 if (!empty($attributeValue)) { 367 $this->addComponentAttributeValue($attributeName, $attributeValue); 368 } 369 } 370 371 public function hasComponentAttribute($attributeName) 372 { 373 $isset = isset($this->componentAttributesCaseInsensitive[$attributeName]); 374 if ($isset === false) { 375 /** 376 * Edge effect 377 * if this is a boolean value and the first value, it may be stored in the type 378 */ 379 if (isset($this->componentAttributesCaseInsensitive[TagAttributes::TYPE_KEY])) { 380 if ($attributeName == $this->componentAttributesCaseInsensitive[TagAttributes::TYPE_KEY]) { 381 return true; 382 } 383 } 384 } 385 return $isset; 386 } 387 388 /** 389 * To an HTML array in the form 390 * class => 'value1 value2', 391 * att => 'value1 value 2' 392 * For historic reason, data passed between the handle and the render 393 * can still be in this format 394 */ 395 public function toHtmlArray(): array 396 { 397 if ($this->componentToHtmlAttributeProcessingWasDone) { 398 LogUtility::msg("This tag attribute ($this) was already finalized. You cannot finalized it twice", LogUtility::LVL_MSG_ERROR); 399 return $this->finalHtmlArray; 400 } 401 402 $this->componentToHtmlAttributeProcessingWasDone = true; 403 404 /** 405 * Following the rule 2 to encode the unknown value 406 * We encode the component attribute (ie not the HTML attribute because 407 * they may have already encoded value) 408 * https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#rule-2-attribute-encode-before-inserting-untrusted-data-into-html-common-attributes 409 */ 410 411 $originalArray = $this->componentAttributesCaseInsensitive->getOriginalArray(); 412 $this->escapeComponentAttribute($originalArray); 413 414 415 /** 416 * Width and height 417 */ 418 Dimension::processWidthAndHeight($this); 419 420 /** 421 * Process animation (onHover, onView) 422 */ 423 Hover::processOnHover($this); 424 Animation::processOnView($this); 425 426 427 /** 428 * Position and Stickiness 429 */ 430 Position::processStickiness($this); 431 Position::processPosition($this); 432 433 /** 434 * Block processing 435 * 436 * Float, align, spacing 437 */ 438 FloatAttribute::processFloat($this); 439 Align::processAlignAttributes($this); 440 Spacing::processSpacingAttributes($this); 441 Opacity::processOpacityAttribute($this); 442 Background::processBackgroundAttributes($this); 443 Shadow::process($this); 444 445 /** 446 * Process text attributes 447 */ 448 LineSpacing::processLineSpacingAttributes($this); 449 TextAlign::processTextAlign($this); 450 Boldness::processBoldnessAttribute($this); 451 FontSize::processFontSizeAttribute($this); 452 TextColor::processTextColorAttribute($this); 453 Underline::processUnderlineAttribute($this); 454 455 /** 456 * Process the style attributes if any 457 */ 458 PluginUtility::processStyle($this); 459 Toggle::processToggle($this); 460 461 462 /** 463 * Skin Attribute 464 */ 465 Skin::processSkinAttribute($this); 466 467 /** 468 * Lang 469 */ 470 Lang::processLangAttribute($this); 471 472 /** 473 * Transform 474 */ 475 if ($this->hasComponentAttribute(self::TRANSFORM)) { 476 $transformValue = $this->getValueAndRemove(self::TRANSFORM); 477 $this->addStyleDeclaration("transform", $transformValue); 478 } 479 480 /** 481 * Add the type class used for CSS styling 482 */ 483 StyleUtility::addStylingClass($this); 484 485 /** 486 * Add the style has html attribute 487 * before processing 488 */ 489 $this->addHtmlAttributeValueIfNotEmpty("style", $this->getStyle()); 490 491 /** 492 * Create a non-sorted temporary html attributes array 493 */ 494 $tempHtmlArray = $this->htmlAttributes; 495 496 /** 497 * copy the unknown component attributes 498 */ 499 $originalArray = $this->componentAttributesCaseInsensitive->getOriginalArray(); 500 foreach ($originalArray as $key => $value) { 501 502 // Null Value, not needed 503 if (is_null($value)) { 504 continue; 505 } 506 507 // No overwrite 508 if (isset($tempHtmlArray[$key])) { 509 continue; 510 } 511 512 // Reserved attribute 513 if (!in_array($key, self::RESERVED_ATTRIBUTES)) { 514 $tempHtmlArray[$key] = $value; 515 } 516 517 } 518 519 520 /** 521 * Sort by attribute 522 * https://datacadamia.com/web/html/attribute#order 523 */ 524 $sortedArray = array(); 525 $once = "once"; 526 $multiple = "multiple"; 527 $orderPatterns = [ 528 "class" => $once, 529 "id" => $once, 530 "name" => $once, 531 "data-.*" => $multiple, 532 "src.*" => $multiple, 533 "for" => $once, 534 "type" => $once, 535 "href" => $once, 536 "value" => $once, 537 "title" => $once, 538 "alt" => $once, 539 "role" => $once, 540 "aria-*" => $multiple]; 541 foreach ($orderPatterns as $pattern => $type) { 542 foreach ($tempHtmlArray as $name => $value) { 543 $searchPattern = "^$pattern$"; 544 if (preg_match("/$searchPattern/", $name)) { 545 $sortedArray[$name] = $value; 546 unset($tempHtmlArray[$name]); 547 if ($type == $once) { 548 break; 549 } 550 } 551 } 552 } 553 foreach ($tempHtmlArray as $name => $value) { 554 555 if (!is_null($value)) { 556 /** 557 * 558 * Don't add a filter on the empty values 559 * 560 * The value of an HTML attribute may be empty 561 * Example the wiki id of the root namespace 562 * 563 * By default, {@link TagAttributes::addHtmlAttributeValue()} 564 * will not accept any value, it must be implicitly said with the 565 * {@link TagAttributes::addHtmlAttributeValue()} 566 * 567 */ 568 $sortedArray[$name] = $value; 569 } 570 571 } 572 $this->finalHtmlArray = $sortedArray; 573 574 return $this->finalHtmlArray; 575 576 } 577 578 /** 579 * HTML attribute are attributes 580 * that are not transformed to HTML 581 * (We make a difference between a high level attribute 582 * that we have in the written document set on a component 583 * @param $key 584 * @param $value 585 * @return TagAttributes 586 */ 587 public function addHtmlAttributeValue($key, $value) 588 { 589 if (blank($value)) { 590 LogUtility::msg("The value of the HTML attribute is blank for the key ($key) - Tag ($this->logicalTag). Use the empty function if the value can be empty", LogUtility::LVL_MSG_ERROR); 591 } 592 /** 593 * We encode all HTML attribute 594 * because `Unescaped '<' not allowed in attributes values` 595 * 596 * except for url that have another encoding 597 * (ie only the query parameters value should be encoded) 598 */ 599 $urlEncoding = ["href", "src", "data-src", "data-srcset"]; 600 if (!in_array($key, $urlEncoding)) { 601 /** 602 * htmlencode the value `true` as `1`, 603 * We transform it first as string, then 604 */ 605 $value = PluginUtility::htmlEncode(StringUtility::toString($value)); 606 } 607 $this->htmlAttributes[$key] = $value; 608 return $this; 609 } 610 611 612 public function addHtmlAttributeValueIfNotEmpty($key, $value) 613 { 614 if (!empty($value)) { 615 $this->addHtmlAttributeValue($key, $value); 616 } 617 } 618 619 /** 620 * @param $attributeName 621 * @param null $default 622 * @return string|array|null a HTML value in the form 'value1 value2...' 623 */ 624 public function getValue($attributeName, $default = null) 625 { 626 $attributeName = strtolower($attributeName); 627 if ($this->hasComponentAttribute($attributeName)) { 628 return $this->componentAttributesCaseInsensitive[$attributeName]; 629 } else { 630 return $default; 631 } 632 } 633 634 /** 635 * @return array - the storage format returned from the {@link SyntaxPlugin::handle()} method 636 */ 637 public function toInternalArray() 638 { 639 return $this->componentAttributesCaseInsensitive; 640 } 641 642 /** 643 * Get the value and remove it from the attributes 644 * @param $attributeName 645 * @param $default 646 * @return string|array|null 647 */ 648 public function getValueAndRemove($attributeName, $default = null) 649 { 650 $attributeName = strtolower($attributeName); 651 $value = $default; 652 if ($this->hasComponentAttribute($attributeName)) { 653 $value = $this->getValue($attributeName); 654 655 if (!in_array($attributeName, self::RESERVED_ATTRIBUTES)) { 656 /** 657 * Don't remove for instance the `type` 658 * because it may be used elsewhere 659 */ 660 unset($this->componentAttributesCaseInsensitive[$attributeName]); 661 } else { 662 LogUtility::msg("Internal: The attribute $attributeName is a reserved word and cannot be removed. Use the get function instead", LogUtility::LVL_MSG_WARNING, "support"); 663 } 664 665 } 666 return $value; 667 } 668 669 670 /** 671 * @return array - an array of key string and value of the component attributes 672 * This array is saved on the disk 673 */ 674 public function toCallStackArray() 675 { 676 $array = array(); 677 $originalArray = $this->componentAttributesCaseInsensitive->getOriginalArray(); 678 foreach ($originalArray as $key => $value) { 679 /** 680 * Only null value are not passed 681 * width can be zero, wiki-id can be the empty string (ie root namespace) 682 */ 683 if (!is_null($value)) { 684 $array[$key] = StringUtility::toString($value); 685 } 686 } 687 $style = $this->getStyle(); 688 if ($style != null) { 689 $array["style"] = $style; 690 } 691 return $array; 692 } 693 694 public 695 function getComponentAttributeValue($attributeName, $default = null) 696 { 697 $lowerAttribute = strtolower($attributeName); 698 $value = $default; 699 if ($this->hasComponentAttribute($lowerAttribute)) { 700 $value = $this->getValue($lowerAttribute); 701 } 702 return $value; 703 } 704 705 public 706 function addStyleDeclaration($property, $value) 707 { 708 ArrayUtility::addIfNotSet($this->styleDeclaration, $property, $value); 709 } 710 711 712 public 713 function hasStyleDeclaration($styleDeclaration) 714 { 715 return isset($this->styleDeclaration[$styleDeclaration]); 716 } 717 718 public 719 function getAndRemoveStyleDeclaration($styleDeclaration) 720 { 721 $styleValue = $this->styleDeclaration[$styleDeclaration]; 722 unset($this->styleDeclaration[$styleDeclaration]); 723 return $styleValue; 724 } 725 726 727 public 728 function toHTMLAttributeString() 729 { 730 731 $tagAttributeString = ""; 732 733 $htmlArray = $this->toHtmlArray(); 734 foreach ($htmlArray as $name => $value) { 735 736 /** 737 * Empty value are authorized 738 * null are just not set 739 */ 740 if (!is_null($value)) { 741 742 /** 743 * Unset attribute should not be added 744 */ 745 if ($value === TagAttributes::UN_SET) { 746 continue; 747 } 748 749 /** 750 * The condition is important 751 * because we may pass the javascript character `\n` in a `srcdoc` for javascript 752 * and the {@link StringUtility::toString()} will transform it as `\\n` 753 * making it unusable 754 */ 755 if (!is_string($value)) { 756 $stringValue = StringUtility::toString($value); 757 } else { 758 $stringValue = $value; 759 } 760 761 762 $tagAttributeString .= $name . '="' . $stringValue . '" '; 763 } 764 765 } 766 return trim($tagAttributeString); 767 768 769 } 770 771 public 772 function getComponentAttributes() 773 { 774 return $this->toCallStackArray(); 775 } 776 777 public 778 function removeComponentAttributeIfPresent($attributeName) 779 { 780 if ($this->hasComponentAttribute($attributeName)) { 781 unset($this->componentAttributesCaseInsensitive[$attributeName]); 782 } 783 784 } 785 786 public 787 function toHtmlEnterTag($htmlTag) 788 { 789 790 $enterTag = "<" . $htmlTag; 791 $attributeString = $this->toHTMLAttributeString(); 792 if (!empty($attributeString)) { 793 $enterTag .= " " . $attributeString; 794 } 795 /** 796 * Is it an open tag ? 797 */ 798 if (!$this->getValue(self::OPEN_TAG, false)) { 799 800 $enterTag .= ">"; 801 802 /** 803 * Do we have html after the tag is closed 804 */ 805 if (!empty($this->htmlAfterEnterTag)) { 806 $enterTag .= DOKU_LF . $this->htmlAfterEnterTag; 807 } 808 809 } 810 811 812 return $enterTag; 813 814 } 815 816 public 817 function getLogicalTag() 818 { 819 return $this->logicalTag; 820 } 821 822 public 823 function setLogicalTag($tag) 824 { 825 $this->logicalTag = $tag; 826 } 827 828 public 829 function removeComponentAttribute($attribute) 830 { 831 $lowerAtt = strtolower($attribute); 832 if (isset($this->componentAttributesCaseInsensitive[$lowerAtt])) { 833 $value = $this->componentAttributesCaseInsensitive[$lowerAtt]; 834 unset($this->componentAttributesCaseInsensitive[$lowerAtt]); 835 return $value; 836 } else { 837 /** 838 * Edge case, this is the first boolean attribute 839 * and may has been categorized as the type 840 */ 841 if (!$this->getType() == $lowerAtt) { 842 LogUtility::msg("Internal Error: The component attribute ($attribute) is not present. Use the ifPresent function, if you don't want this message", LogUtility::LVL_MSG_ERROR); 843 } 844 845 } 846 847 } 848 849 /** 850 * @param $html - an html that should be closed and added after the enter tag 851 */ 852 public 853 function addHtmlAfterEnterTag($html) 854 { 855 $this->htmlAfterEnterTag = $html . $this->htmlAfterEnterTag; 856 } 857 858 /** 859 * The mime of the HTTP request 860 * This is not the good place but yeah, 861 * this class has become the context class 862 * 863 * Mime make the difference for a svg to know if it's required as external resource (ie SVG) 864 * or as included in HTML page 865 * @param $mime 866 */ 867 public 868 function setMime($mime) 869 { 870 $this->mime = $mime; 871 } 872 873 /** 874 * @return string - the mime of the request 875 */ 876 public 877 function getMime() 878 { 879 return $this->mime; 880 } 881 882 public 883 function getType() 884 { 885 return $this->getValue(self::TYPE_KEY); 886 } 887 888 /** 889 * @param $attributeName 890 * @return ConditionalValue 891 */ 892 public 893 function getConditionalValueAndRemove($attributeName) 894 { 895 $value = $this->getConditionalValueAndRemove($attributeName); 896 return new ConditionalValue($value); 897 898 } 899 900 /** 901 * @param $attributeName 902 * @return false|string[] - an array of values 903 */ 904 public 905 function getValuesAndRemove($attributeName) 906 { 907 908 /** 909 * Trim 910 */ 911 $trim = trim($this->getValueAndRemove($attributeName)); 912 913 /** 914 * Replace all suite of space that have more than 2 characters 915 */ 916 $value = preg_replace("/\s{2,}/", " ", $trim); 917 return explode(" ", $value); 918 919 } 920 921 public 922 function setType($type) 923 { 924 $this->setComponentAttributeValue(TagAttributes::TYPE_KEY, $type); 925 } 926 927 /** 928 * Merging will add the values, no replace or overwrite 929 * @param $callStackArray 930 */ 931 public 932 function mergeWithCallStackArray($callStackArray) 933 { 934 foreach ($callStackArray as $key => $value) { 935 if ($this->hasComponentAttribute($key)) { 936 $this->addComponentAttributeValue($key, $value); 937 } else { 938 $this->setComponentAttributeValue($key, $value); 939 } 940 } 941 942 } 943 944 /** 945 * @param $string 946 */ 947 public 948 function removeAttributeIfPresent($string) 949 { 950 $this->removeComponentAttributeIfPresent($string); 951 $this->removeHTMLAttributeIfPresent($string); 952 953 } 954 955 private 956 function removeHTMLAttributeIfPresent($string) 957 { 958 $lowerAtt = strtolower($string); 959 if (isset($this->htmlAttributes[$lowerAtt])) { 960 unset($this->htmlAttributes[$lowerAtt]); 961 } 962 } 963 964 public 965 function getValueAndRemoveIfPresent($attribute, $default = null) 966 { 967 $value = $this->getValue($attribute, $default); 968 $this->removeAttributeIfPresent($attribute); 969 return $value; 970 } 971 972 public 973 function generateAndSetId() 974 { 975 self::$counter += 1; 976 $id = self::$counter; 977 $logicalTag = $this->getLogicalTag(); 978 if (!empty($logicalTag)) { 979 $id = $this->logicalTag . $id; 980 } 981 $this->setComponentAttributeValue("id", $id); 982 return $id; 983 } 984 985 /** 986 * 987 * @param $markiTag 988 * @return string - the marki tag made of logical attribute 989 * There is no processing to transform it to an HTML tag 990 */ 991 public 992 function toMarkiEnterTag($markiTag) 993 { 994 $enterTag = "<" . $markiTag; 995 996 $attributeString = ""; 997 foreach ($this->getComponentAttributes() as $key => $value) { 998 $attributeString .= "$key=\"$value\" "; 999 } 1000 $attributeString = trim($attributeString); 1001 1002 if (!empty($attributeString)) { 1003 $enterTag .= " " . $attributeString; 1004 } 1005 $enterTag .= ">"; 1006 return $enterTag; 1007 1008 } 1009 1010 /** 1011 * @param string $key add an html attribute with the empty string 1012 */ 1013 public 1014 function addEmptyHtmlAttributeValue($key) 1015 { 1016 1017 $this->htmlAttributes[$key] = ''; 1018 return $this; 1019 1020 } 1021 1022 public 1023 function addEmptyComponentAttributeValue($attribute) 1024 { 1025 $this->componentAttributesCaseInsensitive[$attribute] = ""; 1026 } 1027 1028 /** 1029 * @param $attribute 1030 * @param null $default 1031 * @return mixed 1032 */ 1033 public 1034 function getBooleanValueAndRemove($attribute, $default = null) 1035 { 1036 $value = $this->getValueAndRemove($attribute); 1037 if ($value == null) { 1038 return $default; 1039 } else { 1040 return filter_var($value, FILTER_VALIDATE_BOOLEAN); 1041 } 1042 } 1043 1044 public 1045 function hasAttribute($attribute) 1046 { 1047 $hasAttribute = $this->hasComponentAttribute($attribute); 1048 if ($hasAttribute === true) { 1049 return true; 1050 } else { 1051 return $this->hasHtmlAttribute($attribute); 1052 } 1053 } 1054 1055 private 1056 function hasHtmlAttribute($attribute) 1057 { 1058 return isset($this->htmlAttributes[$attribute]); 1059 } 1060 1061 /** 1062 * Component attribute are entered by the user and should be encoded 1063 * @param array $arrayToEscape 1064 * @param null $subKey 1065 */ 1066 private 1067 function escapeComponentAttribute(array $arrayToEscape, $subKey = null) 1068 { 1069 1070 foreach ($arrayToEscape as $name => $value) { 1071 1072 $encodedName = PluginUtility::htmlEncode($name); 1073 1074 /** 1075 * Boolean does not need to be encoded 1076 */ 1077 if (is_bool($value)) { 1078 if ($subKey == null) { 1079 $this->componentAttributesCaseInsensitive[$encodedName] = $value; 1080 } else { 1081 $this->componentAttributesCaseInsensitive[$subKey][$encodedName] = $value; 1082 } 1083 continue; 1084 } 1085 1086 if (is_array($value)) { 1087 $this->escapeComponentAttribute($value, $encodedName); 1088 } else { 1089 1090 $value = PluginUtility::htmlEncode($value); 1091 if ($subKey == null) { 1092 $this->componentAttributesCaseInsensitive[$encodedName] = $value; 1093 } else { 1094 $this->componentAttributesCaseInsensitive[$subKey][$encodedName] = $value; 1095 } 1096 } 1097 } 1098 } 1099 1100 public function __toString() 1101 { 1102 return "TagAttributes"; 1103 } 1104 1105 1106} 1107