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