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 public function addClassName($className) 269 { 270 271 $this->addComponentAttributeValue(self::CLASS_KEY, $className); 272 return $this; 273 274 } 275 276 public function getClass() 277 { 278 return $this->getValue(self::CLASS_KEY); 279 } 280 281 public function getStyle() 282 { 283 if (sizeof($this->styleDeclaration) != 0) { 284 return PluginUtility::array2InlineStyle($this->styleDeclaration); 285 } else { 286 /** 287 * null is needed to see if the attribute was set or not 288 * because an attribute may have the empty string 289 * Example: the wiki id of the root namespace 290 */ 291 return null; 292 } 293 294 } 295 296 /** 297 * Add an attribute with its value if the value is not empty 298 * @param $attributeName 299 * @param $attributeValue 300 */ 301 public function addComponentAttributeValue($attributeName, $attributeValue) 302 { 303 304 if (empty($attributeValue) && !is_bool($attributeValue)) { 305 LogUtility::msg("The value of the attribute ($attributeName) is empty. Use the nonEmpty function instead", LogUtility::LVL_MSG_WARNING, "support"); 306 } 307 308 $attLower = strtolower($attributeName); 309 if ($this->hasComponentAttribute($attLower)) { 310 $actual = $this->componentAttributesCaseInsensitive[$attLower]; 311 } 312 313 /** 314 * Type of data: list (class) or atomic (id) 315 */ 316 if ($attributeName === "class") { 317 if (!is_string($attributeValue)) { 318 LogUtility::msg("The value ($attributeValue) for the `class` attribute is not a string", LogUtility::LVL_MSG_ERROR, self::CANONICAL); 319 } 320 /** 321 * It may be in the form "value1 value2" 322 */ 323 $newValues = StringUtility::explodeAndTrim($attributeValue, " "); 324 if (!empty($actual)) { 325 $actualValues = StringUtility::explodeAndTrim($actual, " "); 326 } else { 327 $actualValues = []; 328 } 329 $newValues = PluginUtility::mergeAttributes($newValues, $actualValues); 330 $this->componentAttributesCaseInsensitive[$attLower] = implode(" ", $newValues); 331 } else { 332 if (!empty($actual)) { 333 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); 334 } 335 $this->componentAttributesCaseInsensitive[$attLower] = $attributeValue; 336 } 337 338 339 } 340 341 public function setComponentAttributeValue($attributeName, $attributeValue) 342 { 343 $attLower = strtolower($attributeName); 344 $actualValue = $this->getValue($attributeName); 345 if ($actualValue === null || $actualValue !== TagAttributes::UN_SET) { 346 $this->componentAttributesCaseInsensitive[$attLower] = $attributeValue; 347 } 348 } 349 350 public function addComponentAttributeValueIfNotEmpty($attributeName, $attributeValue) 351 { 352 if (!empty($attributeValue)) { 353 $this->addComponentAttributeValue($attributeName, $attributeValue); 354 } 355 } 356 357 public function hasComponentAttribute($attributeName) 358 { 359 $isset = isset($this->componentAttributesCaseInsensitive[$attributeName]); 360 if ($isset === false) { 361 /** 362 * Edge effect 363 * if this is a boolean value and the first value, it may be stored in the type 364 */ 365 if (isset($this->componentAttributesCaseInsensitive[TagAttributes::TYPE_KEY])) { 366 if ($attributeName == $this->componentAttributesCaseInsensitive[TagAttributes::TYPE_KEY]) { 367 return true; 368 } 369 } 370 } 371 return $isset; 372 } 373 374 /** 375 * To an HTML array in the form 376 * class => 'value1 value2', 377 * att => 'value1 value 2' 378 * For historic reason, data passed between the handle and the render 379 * can still be in this format 380 */ 381 public function toHtmlArray() 382 { 383 if (!$this->componentToHtmlAttributeProcessingWasDone) { 384 385 $this->componentToHtmlAttributeProcessingWasDone = true; 386 387 /** 388 * Following the rule 2 to encode the unknown value 389 * We encode the component attribute (ie not the HTML attribute because 390 * they may have already encoded value) 391 * https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#rule-2-attribute-encode-before-inserting-untrusted-data-into-html-common-attributes 392 */ 393 394 $originalArray = $this->componentAttributesCaseInsensitive->getOriginalArray(); 395 $this->escapeComponentAttribute($originalArray); 396 397 398 /** 399 * Width and height 400 */ 401 Dimension::processWidthAndHeight($this); 402 403 /** 404 * Process animation (onHover, onView) 405 */ 406 Hover::processOnHover($this); 407 Animation::processOnView($this); 408 409 410 /** 411 * Position and Stickiness 412 */ 413 Position::processStickiness($this); 414 Position::processPosition($this); 415 416 /** 417 * Block processing 418 * 419 * Float, align, spacing 420 */ 421 FloatAttribute::processFloat($this); 422 Align::processAlignAttributes($this); 423 Spacing::processSpacingAttributes($this); 424 Opacity::processOpacityAttribute($this); 425 Background::processBackgroundAttributes($this); 426 Shadow::process($this); 427 428 /** 429 * Process text attributes 430 */ 431 LineSpacing::processLineSpacingAttributes($this); 432 TextAlign::processTextAlign($this); 433 Boldness::processBoldnessAttribute($this); 434 FontSize::processFontSizeAttribute($this); 435 TextColor::processTextColorAttribute($this); 436 Underline::processUnderlineAttribute($this); 437 438 /** 439 * Process the style attributes if any 440 */ 441 PluginUtility::processStyle($this); 442 Toggle::processToggle($this); 443 444 445 /** 446 * Skin Attribute 447 */ 448 Skin::processSkinAttribute($this); 449 450 /** 451 * Lang 452 */ 453 Lang::processLangAttribute($this); 454 455 /** 456 * Transform 457 */ 458 if ($this->hasComponentAttribute(self::TRANSFORM)) { 459 $transformValue = $this->getValueAndRemove(self::TRANSFORM); 460 $this->addStyleDeclaration("transform", $transformValue); 461 } 462 463 /** 464 * Add the type class used for CSS styling 465 */ 466 StyleUtility::addStylingClass($this); 467 468 /** 469 * Add the style has html attribute 470 * before processing 471 */ 472 $this->addHtmlAttributeValueIfNotEmpty("style", $this->getStyle()); 473 474 /** 475 * Create a non-sorted temporary html attributes array 476 */ 477 $tempHtmlArray = $this->htmlAttributes; 478 479 /** 480 * copy the unknown component attributes 481 */ 482 $originalArray = $this->componentAttributesCaseInsensitive->getOriginalArray(); 483 foreach ($originalArray as $key => $value) { 484 485 // Null Value, not needed 486 if (is_null($value)) { 487 continue; 488 } 489 490 // No overwrite 491 if (isset($tempHtmlArray[$key])) { 492 continue; 493 } 494 495 // Reserved attribute 496 if (!in_array($key, self::RESERVED_ATTRIBUTES)) { 497 $tempHtmlArray[$key] = $value; 498 } 499 500 } 501 502 503 /** 504 * Sort by attribute 505 * https://datacadamia.com/web/html/attribute#order 506 */ 507 $sortedArray = array(); 508 $once = "once"; 509 $multiple = "multiple"; 510 $orderPatterns = [ 511 "class" => $once, 512 "id" => $once, 513 "name" => $once, 514 "data-.*" => $multiple, 515 "src.*" => $multiple, 516 "for" => $once, 517 "type" => $once, 518 "href" => $once, 519 "value" => $once, 520 "title" => $once, 521 "alt" => $once, 522 "role" => $once, 523 "aria-*" => $multiple]; 524 foreach ($orderPatterns as $pattern => $type) { 525 foreach ($tempHtmlArray as $name => $value) { 526 $searchPattern = "^$pattern$"; 527 if (preg_match("/$searchPattern/", $name)) { 528 $sortedArray[$name] = $value; 529 unset($tempHtmlArray[$name]); 530 if ($type == $once) { 531 break; 532 } 533 } 534 } 535 } 536 foreach ($tempHtmlArray as $name => $value) { 537 538 if (!is_null($value)) { 539 /** 540 * 541 * Don't add a filter on the empty values 542 * 543 * The value of an HTML attribute may be empty 544 * Example the wiki id of the root namespace 545 * 546 * By default, {@link TagAttributes::addHtmlAttributeValue()} 547 * will not accept any value, it must be implicitly said with the 548 * {@link TagAttributes::addHtmlAttributeValue()} 549 * 550 */ 551 $sortedArray[$name] = $value; 552 } 553 554 } 555 $this->finalHtmlArray = $sortedArray; 556 557 } 558 559 560 return $this->finalHtmlArray; 561 562 } 563 564 /** 565 * HTML attribute are attributes 566 * that are not transformed to HTML 567 * (We make a difference between a high level attribute 568 * that we have in the written document set on a component 569 * @param $key 570 * @param $value 571 * @return TagAttributes 572 */ 573 public function addHtmlAttributeValue($key, $value) 574 { 575 if (blank($value)) { 576 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); 577 } 578 /** 579 * We encode all HTML attribute 580 * because `Unescaped '<' not allowed in attributes values` 581 * 582 * except for url that have another encoding 583 * (ie only the query parameters value should be encoded) 584 */ 585 $urlEncoding = ["href", "src", "data-src", "data-srcset"]; 586 if (!in_array($key, $urlEncoding)) { 587 /** 588 * htmlencode the value `true` as `1`, 589 * We transform it first as string, then 590 */ 591 $value = PluginUtility::htmlEncode(StringUtility::toString($value)); 592 } 593 $this->htmlAttributes[$key] = $value; 594 return $this; 595 } 596 597 598 public function addHtmlAttributeValueIfNotEmpty($key, $value) 599 { 600 if (!empty($value)) { 601 $this->addHtmlAttributeValue($key, $value); 602 } 603 } 604 605 /** 606 * @param $attributeName 607 * @param null $default 608 * @return string|array|null a HTML value in the form 'value1 value2...' 609 */ 610 public function getValue($attributeName, $default = null) 611 { 612 $attributeName = strtolower($attributeName); 613 if ($this->hasComponentAttribute($attributeName)) { 614 return $this->componentAttributesCaseInsensitive[$attributeName]; 615 } else { 616 return $default; 617 } 618 } 619 620 /** 621 * @return array - the storage format returned from the {@link SyntaxPlugin::handle()} method 622 */ 623 public function toInternalArray() 624 { 625 return $this->componentAttributesCaseInsensitive; 626 } 627 628 /** 629 * Get the value and remove it from the attributes 630 * @param $attributeName 631 * @param $default 632 * @return string|array|null 633 */ 634 public function getValueAndRemove($attributeName, $default = null) 635 { 636 $attributeName = strtolower($attributeName); 637 $value = $default; 638 if ($this->hasComponentAttribute($attributeName)) { 639 $value = $this->getValue($attributeName); 640 641 if (!in_array($attributeName, self::RESERVED_ATTRIBUTES)) { 642 /** 643 * Don't remove for instance the `type` 644 * because it may be used elsewhere 645 */ 646 unset($this->componentAttributesCaseInsensitive[$attributeName]); 647 } else { 648 LogUtility::msg("Internal: The attribute $attributeName is a reserved word and cannot be removed. Use the get function instead", LogUtility::LVL_MSG_WARNING, "support"); 649 } 650 651 } 652 return $value; 653 } 654 655 656 /** 657 * @return array - an array of key string and value of the component attributes 658 * This array is saved on the disk 659 */ 660 public function toCallStackArray() 661 { 662 $array = array(); 663 $originalArray = $this->componentAttributesCaseInsensitive->getOriginalArray(); 664 foreach ($originalArray as $key => $value) { 665 /** 666 * Only null value are not passed 667 * width can be zero, wiki-id can be the empty string (ie root namespace) 668 */ 669 if (!is_null($value)) { 670 $array[$key] = StringUtility::toString($value); 671 } 672 } 673 $style = $this->getStyle(); 674 if ($style != null) { 675 $array["style"] = $style; 676 } 677 return $array; 678 } 679 680 public 681 function getComponentAttributeValue($attributeName, $default = null) 682 { 683 $lowerAttribute = strtolower($attributeName); 684 $value = $default; 685 if ($this->hasComponentAttribute($lowerAttribute)) { 686 $value = $this->getValue($lowerAttribute); 687 } 688 return $value; 689 } 690 691 public 692 function addStyleDeclaration($property, $value) 693 { 694 ArrayUtility::addIfNotSet($this->styleDeclaration, $property, $value); 695 } 696 697 698 public 699 function hasStyleDeclaration($styleDeclaration) 700 { 701 return isset($this->styleDeclaration[$styleDeclaration]); 702 } 703 704 public 705 function getAndRemoveStyleDeclaration($styleDeclaration) 706 { 707 $styleValue = $this->styleDeclaration[$styleDeclaration]; 708 unset($this->styleDeclaration[$styleDeclaration]); 709 return $styleValue; 710 } 711 712 713 public 714 function toHTMLAttributeString() 715 { 716 717 $tagAttributeString = ""; 718 719 $htmlArray = $this->toHtmlArray(); 720 foreach ($htmlArray as $name => $value) { 721 722 /** 723 * Empty value are authorized 724 * null are just not set 725 */ 726 if (!is_null($value)) { 727 728 /** 729 * Unset attribute should not be added 730 */ 731 if ($value === TagAttributes::UN_SET) { 732 continue; 733 } 734 735 /** 736 * The condition is important 737 * because we may pass the javascript character `\n` in a `srcdoc` for javascript 738 * and the {@link StringUtility::toString()} will transform it as `\\n` 739 * making it unusable 740 */ 741 if (!is_string($value)) { 742 $stringValue = StringUtility::toString($value); 743 } else { 744 $stringValue = $value; 745 } 746 747 748 $tagAttributeString .= $name . '="' . $stringValue . '" '; 749 } 750 751 } 752 return trim($tagAttributeString); 753 754 755 } 756 757 public 758 function getComponentAttributes() 759 { 760 return $this->toCallStackArray(); 761 } 762 763 public 764 function removeComponentAttributeIfPresent($attributeName) 765 { 766 if ($this->hasComponentAttribute($attributeName)) { 767 unset($this->componentAttributesCaseInsensitive[$attributeName]); 768 } 769 770 } 771 772 public 773 function toHtmlEnterTag($htmlTag) 774 { 775 776 $enterTag = "<" . $htmlTag; 777 $attributeString = $this->toHTMLAttributeString(); 778 if (!empty($attributeString)) { 779 $enterTag .= " " . $attributeString; 780 } 781 /** 782 * Is it an open tag ? 783 */ 784 if (!$this->getValue(self::OPEN_TAG, false)) { 785 786 $enterTag .= ">"; 787 788 /** 789 * Do we have html after the tag is closed 790 */ 791 if (!empty($this->htmlAfterEnterTag)) { 792 $enterTag .= DOKU_LF . $this->htmlAfterEnterTag; 793 } 794 795 } 796 797 798 return $enterTag; 799 800 } 801 802 public 803 function getLogicalTag() 804 { 805 return $this->logicalTag; 806 } 807 808 public 809 function setLogicalTag($tag) 810 { 811 $this->logicalTag = $tag; 812 } 813 814 public 815 function removeComponentAttribute($attribute) 816 { 817 $lowerAtt = strtolower($attribute); 818 if (isset($this->componentAttributesCaseInsensitive[$lowerAtt])) { 819 $value = $this->componentAttributesCaseInsensitive[$lowerAtt]; 820 unset($this->componentAttributesCaseInsensitive[$lowerAtt]); 821 return $value; 822 } else { 823 /** 824 * Edge case, this is the first boolean attribute 825 * and may has been categorized as the type 826 */ 827 if (!$this->getType() == $lowerAtt) { 828 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); 829 } 830 831 } 832 833 } 834 835 /** 836 * @param $html - an html that should be closed and added after the enter tag 837 */ 838 public 839 function addHtmlAfterEnterTag($html) 840 { 841 $this->htmlAfterEnterTag = $html . $this->htmlAfterEnterTag; 842 } 843 844 /** 845 * The mime of the HTTP request 846 * This is not the good place but yeah, 847 * this class has become the context class 848 * 849 * Mime make the difference for a svg to know if it's required as external resource (ie SVG) 850 * or as included in HTML page 851 * @param $mime 852 */ 853 public 854 function setMime($mime) 855 { 856 $this->mime = $mime; 857 } 858 859 /** 860 * @return string - the mime of the request 861 */ 862 public 863 function getMime() 864 { 865 return $this->mime; 866 } 867 868 public 869 function getType() 870 { 871 return $this->getValue(self::TYPE_KEY); 872 } 873 874 /** 875 * @param $attributeName 876 * @return ConditionalValue 877 */ 878 public 879 function getConditionalValueAndRemove($attributeName) 880 { 881 $value = $this->getConditionalValueAndRemove($attributeName); 882 return new ConditionalValue($value); 883 884 } 885 886 /** 887 * @param $attributeName 888 * @return false|string[] - an array of values 889 */ 890 public 891 function getValuesAndRemove($attributeName) 892 { 893 894 /** 895 * Trim 896 */ 897 $trim = trim($this->getValueAndRemove($attributeName)); 898 899 /** 900 * Replace all suite of space that have more than 2 characters 901 */ 902 $value = preg_replace("/\s{2,}/", " ", $trim); 903 return explode(" ", $value); 904 905 } 906 907 public 908 function setType($type) 909 { 910 $this->setComponentAttributeValue(TagAttributes::TYPE_KEY, $type); 911 } 912 913 /** 914 * Merging will add the values, no replace or overwrite 915 * @param $callStackArray 916 */ 917 public 918 function mergeWithCallStackArray($callStackArray) 919 { 920 foreach ($callStackArray as $key => $value) { 921 if ($this->hasComponentAttribute($key)) { 922 $this->addComponentAttributeValue($key, $value); 923 } else { 924 $this->setComponentAttributeValue($key, $value); 925 } 926 } 927 928 } 929 930 /** 931 * @param $string 932 */ 933 public 934 function removeAttributeIfPresent($string) 935 { 936 $this->removeComponentAttributeIfPresent($string); 937 $this->removeHTMLAttributeIfPresent($string); 938 939 } 940 941 private 942 function removeHTMLAttributeIfPresent($string) 943 { 944 $lowerAtt = strtolower($string); 945 if (isset($this->htmlAttributes[$lowerAtt])) { 946 unset($this->htmlAttributes[$lowerAtt]); 947 } 948 } 949 950 public 951 function getValueAndRemoveIfPresent($attribute, $default = null) 952 { 953 $value = $this->getValue($attribute, $default); 954 $this->removeAttributeIfPresent($attribute); 955 return $value; 956 } 957 958 public 959 function generateAndSetId() 960 { 961 self::$counter += 1; 962 $id = self::$counter; 963 $logicalTag = $this->getLogicalTag(); 964 if (!empty($logicalTag)) { 965 $id = $this->logicalTag . $id; 966 } 967 $this->setComponentAttributeValue("id", $id); 968 return $id; 969 } 970 971 /** 972 * 973 * @param $markiTag 974 * @return string - the marki tag made of logical attribute 975 * There is no processing to transform it to an HTML tag 976 */ 977 public 978 function toMarkiEnterTag($markiTag) 979 { 980 $enterTag = "<" . $markiTag; 981 982 $attributeString = ""; 983 foreach ($this->getComponentAttributes() as $key => $value) { 984 $attributeString .= "$key=\"$value\" "; 985 } 986 $attributeString = trim($attributeString); 987 988 if (!empty($attributeString)) { 989 $enterTag .= " " . $attributeString; 990 } 991 $enterTag .= ">"; 992 return $enterTag; 993 994 } 995 996 /** 997 * @param string $key add an html attribute with the empty string 998 */ 999 public 1000 function addEmptyHtmlAttributeValue($key) 1001 { 1002 1003 $this->htmlAttributes[$key] = ''; 1004 return $this; 1005 1006 } 1007 1008 public 1009 function addEmptyComponentAttributeValue($attribute) 1010 { 1011 $this->componentAttributesCaseInsensitive[$attribute] = ""; 1012 } 1013 1014 /** 1015 * @param $attribute 1016 * @param null $default 1017 * @return mixed 1018 */ 1019 public 1020 function getBooleanValueAndRemove($attribute, $default = null) 1021 { 1022 $value = $this->getValueAndRemove($attribute); 1023 if ($value == null) { 1024 return $default; 1025 } else { 1026 return filter_var($value, FILTER_VALIDATE_BOOLEAN); 1027 } 1028 } 1029 1030 public 1031 function hasAttribute($attribute) 1032 { 1033 $hasAttribute = $this->hasComponentAttribute($attribute); 1034 if ($hasAttribute === true) { 1035 return true; 1036 } else { 1037 return $this->hasHtmlAttribute($attribute); 1038 } 1039 } 1040 1041 private 1042 function hasHtmlAttribute($attribute) 1043 { 1044 return isset($this->htmlAttributes[$attribute]); 1045 } 1046 1047 /** 1048 * Component attribute are entered by the user and should be encoded 1049 * @param array $arrayToEscape 1050 * @param null $subKey 1051 */ 1052 private 1053 function escapeComponentAttribute(array $arrayToEscape, $subKey = null) 1054 { 1055 1056 foreach ($arrayToEscape as $name => $value) { 1057 1058 $encodedName = PluginUtility::htmlEncode($name); 1059 1060 /** 1061 * Boolean does not need to be encoded 1062 */ 1063 if (is_bool($value)) { 1064 if ($subKey == null) { 1065 $this->componentAttributesCaseInsensitive[$encodedName] = $value; 1066 } else { 1067 $this->componentAttributesCaseInsensitive[$subKey][$encodedName] = $value; 1068 } 1069 continue; 1070 } 1071 1072 if (is_array($value)) { 1073 $this->escapeComponentAttribute($value, $encodedName); 1074 } else { 1075 1076 $value = PluginUtility::htmlEncode($value); 1077 if ($subKey == null) { 1078 $this->componentAttributesCaseInsensitive[$encodedName] = $value; 1079 } else { 1080 $this->componentAttributesCaseInsensitive[$subKey][$encodedName] = $value; 1081 } 1082 } 1083 } 1084 } 1085 1086 public function __toString() 1087 { 1088 return "TagAttributes"; 1089 } 1090 1091 1092} 1093