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