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