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