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