xref: /plugin/combo/ComboStrap/TagAttributes.php (revision 70bbd7f1f72440223cc13f3495efdcb2b0a11514)
137748cd8SNickeau<?php
237748cd8SNickeau/**
337748cd8SNickeau * Copyright (c) 2021. ComboStrap, Inc. and its affiliates. All Rights Reserved.
437748cd8SNickeau *
537748cd8SNickeau * This source code is licensed under the GPL license found in the
637748cd8SNickeau * COPYING  file in the root directory of this source tree.
737748cd8SNickeau *
837748cd8SNickeau * @license  GPL 3 (https://www.gnu.org/licenses/gpl-3.0.en.html)
937748cd8SNickeau * @author   ComboStrap <support@combostrap.com>
1037748cd8SNickeau *
1137748cd8SNickeau */
1237748cd8SNickeau
1337748cd8SNickeaunamespace ComboStrap;
1437748cd8SNickeau
1504fd306cSNickeauuse ComboStrap\Tag\BoxTag;
1604fd306cSNickeauuse ComboStrap\TagAttribute\Align;
1704fd306cSNickeauuse ComboStrap\TagAttribute\Animation;
1804fd306cSNickeauuse ComboStrap\TagAttribute\BackgroundAttribute;
1904fd306cSNickeauuse ComboStrap\TagAttribute\Boldness;
2004fd306cSNickeauuse ComboStrap\TagAttribute\Hero;
2104fd306cSNickeauuse ComboStrap\TagAttribute\Shadow;
2204fd306cSNickeauuse ComboStrap\TagAttribute\StyleAttribute;
2304fd306cSNickeauuse ComboStrap\TagAttribute\TextAlign;
2404fd306cSNickeauuse ComboStrap\TagAttribute\Toggle;
2504fd306cSNickeauuse ComboStrap\TagAttribute\Underline;
2604fd306cSNickeauuse ComboStrap\TagAttribute\Vertical;
2704fd306cSNickeauuse ComboStrap\Web\Url;
2804fd306cSNickeauuse ComboStrap\Xml\XmlDocument;
2904fd306cSNickeauuse ComboStrap\Xml\XmlElement;
3037748cd8SNickeauuse dokuwiki\Extension\SyntaxPlugin;
3137748cd8SNickeau
3237748cd8SNickeau/**
3304fd306cSNickeau * An utility:
3404fd306cSNickeau * * to parse the jsx/component markup match
3504fd306cSNickeau * * to enforce user security (ie `style` is not allowed)
3604fd306cSNickeau * * to get from component attributes to html attributes
3704fd306cSNickeau *
3804fd306cSNickeau *
3904fd306cSNickeau * This is the equivalent of an {@link XmlElement}
4004fd306cSNickeau * but does not need any {@link XmlDocument} to be created
4137748cd8SNickeau *
4237748cd8SNickeau * You can:
4337748cd8SNickeau *   * declare component attribute after parsing
4437748cd8SNickeau *   * declare Html attribute during parsing
4537748cd8SNickeau *   * output the final HTML attributes at the end of the process with the function {@link TagAttributes::toHTMLAttributeString()}
4637748cd8SNickeau *
4737748cd8SNickeau * Component attributes have precedence on HTML attributes.
4837748cd8SNickeau *
4937748cd8SNickeau * @package ComboStrap
5037748cd8SNickeau */
5137748cd8SNickeauclass TagAttributes
5237748cd8SNickeau{
5337748cd8SNickeau    /**
5437748cd8SNickeau     * @var string the alt attribute value (known as the title for dokuwiki)
5537748cd8SNickeau     */
5637748cd8SNickeau    const TITLE_KEY = 'title';
5737748cd8SNickeau
5837748cd8SNickeau
5937748cd8SNickeau    const TYPE_KEY = "type";
6037748cd8SNickeau    const ID_KEY = "id";
6137748cd8SNickeau
6237748cd8SNickeau    /**
634cadd4f8SNickeau     * If not strict, no error is reported
644cadd4f8SNickeau     */
654cadd4f8SNickeau    const STRICT = "strict";
664cadd4f8SNickeau
674cadd4f8SNickeau    /**
6804fd306cSNickeau     * The logical attributes that are not becoming HTML attributes
6937748cd8SNickeau     * (ie internal reserved words)
704cadd4f8SNickeau     *
7104fd306cSNickeau     * TODO: They should be advertised by the syntax component
7237748cd8SNickeau     */
7337748cd8SNickeau    const RESERVED_ATTRIBUTES = [
7437748cd8SNickeau        self::SCRIPT_KEY, // no script attribute for security reason
7537748cd8SNickeau        TagAttributes::TYPE_KEY, // type is the component class
7604fd306cSNickeau        MediaMarkup::LINKING_KEY, // internal to image
7704fd306cSNickeau        IFetcherAbs::CACHE_KEY, // internal also
7804fd306cSNickeau        Tag\WebCodeTag::RENDERING_MODE_ATTRIBUTE,
7904fd306cSNickeau        Vertical::VERTICAL_ATTRIBUTE,
8037748cd8SNickeau        self::OPEN_TAG,
8137748cd8SNickeau        self::HTML_BEFORE,
8282a60d03SNickeau        self::HTML_AFTER,
834cadd4f8SNickeau        Dimension::RATIO_ATTRIBUTE,
844cadd4f8SNickeau        self::STRICT,
8504fd306cSNickeau        FetcherSvg::REQUESTED_PRESERVE_ATTRIBUTE,
864cadd4f8SNickeau        \syntax_plugin_combo_link::CLICKABLE_ATTRIBUTE,
8704fd306cSNickeau        LinkMarkup::PREVIEW_ATTRIBUTE,
884cadd4f8SNickeau        Skin::SKIN_ATTRIBUTE,
894cadd4f8SNickeau        ColorRgb::PRIMARY_VALUE,
904cadd4f8SNickeau        ColorRgb::SECONDARY_VALUE,
9104fd306cSNickeau        Dimension::ZOOM_ATTRIBUTE,
9204fd306cSNickeau        Tag\FollowTag::HANDLE_ATTRIBUTE,
9304fd306cSNickeau        \syntax_plugin_combo_menubar::BREAKPOINT_ATTRIBUTE,
9404fd306cSNickeau        ContainerTag::CONTAINER_ATTRIBUTE,
9504fd306cSNickeau        HeadingTag::HEADING_TEXT_ATTRIBUTE,
9604fd306cSNickeau        self::GENERATED_ID_KEY
9737748cd8SNickeau    ];
9837748cd8SNickeau
9937748cd8SNickeau    /**
10037748cd8SNickeau     * The inline element
10137748cd8SNickeau     * We could pass the plugin object into tag attribute in place of the logical tag
10237748cd8SNickeau     * and check if the {@link SyntaxPlugin::getPType()} is normal
10337748cd8SNickeau     */
10437748cd8SNickeau    const INLINE_LOGICAL_ELEMENTS = [
10504fd306cSNickeau        FetcherSvg::CANONICAL,
10604fd306cSNickeau        FetcherRaster::CANONICAL,
10704fd306cSNickeau        \syntax_plugin_combo_media::TAG,
10837748cd8SNickeau        \syntax_plugin_combo_link::TAG, // link button for instance
10904fd306cSNickeau        ButtonTag::MARKUP_LONG
11037748cd8SNickeau    ];
11104fd306cSNickeau
11204fd306cSNickeau    /**
11304fd306cSNickeau     * Container
11404fd306cSNickeau     * Heading is a block but not a container
11504fd306cSNickeau     */
11604fd306cSNickeau    const CONTAINER_LOGICAL_ELEMENTS = [
11704fd306cSNickeau        BoxTag::TAG,
11804fd306cSNickeau        CardTag::CARD_TAG,
11904fd306cSNickeau        BlockquoteTag::TAG,
12004fd306cSNickeau    ];
12104fd306cSNickeau
12237748cd8SNickeau    const SCRIPT_KEY = "script";
12337748cd8SNickeau    const TRANSFORM = "transform";
12437748cd8SNickeau
12537748cd8SNickeau    const CANONICAL = "tag";
1264cadd4f8SNickeau
12737748cd8SNickeau    const CLASS_KEY = "class";
12837748cd8SNickeau    const WIKI_ID = "wiki-id";
12937748cd8SNickeau
13037748cd8SNickeau    /**
13137748cd8SNickeau     * The open tag attributes
13237748cd8SNickeau     * permit to not close the tag in {@link TagAttributes::toHtmlEnterTag()}
13337748cd8SNickeau     *
13437748cd8SNickeau     * It's used for instance by the {@link \syntax_plugin_combo_tooltip}
13537748cd8SNickeau     * to advertise that it will add attribute and close it
13637748cd8SNickeau     */
13737748cd8SNickeau    const OPEN_TAG = "open-tag";
13837748cd8SNickeau
13937748cd8SNickeau    /**
14037748cd8SNickeau     * If an attribute has this value,
14137748cd8SNickeau     * it will not be added to the output (ie {@link TagAttributes::toHtmlEnterTag()})
14237748cd8SNickeau     * Child element can unset attribute this way
14337748cd8SNickeau     * in order to write their own
14437748cd8SNickeau     *
14537748cd8SNickeau     * This is used by the {@link \syntax_plugin_combo_tooltip}
14637748cd8SNickeau     * to advertise that the title attribute should not be set
14737748cd8SNickeau     */
14837748cd8SNickeau    const UN_SET = "unset";
14937748cd8SNickeau
15037748cd8SNickeau    /**
15137748cd8SNickeau     * When wrapping an element
15237748cd8SNickeau     * A tag may get HTML before and after
15337748cd8SNickeau     * Uses for instance to wrap a svg in span
15437748cd8SNickeau     * when adding a {@link \syntax_plugin_combo_tooltip}
15537748cd8SNickeau     */
15637748cd8SNickeau    const HTML_BEFORE = "htmlBefore";
15737748cd8SNickeau    const HTML_AFTER = "htmlAfter";
15837748cd8SNickeau
15937748cd8SNickeau    /**
16082a60d03SNickeau     * Attribute with multiple values
16182a60d03SNickeau     */
16204fd306cSNickeau    const MULTIPLE_VALUES_ATTRIBUTES = [self::CLASS_KEY, self::REL, Align::ALIGN_ATTRIBUTE];
1634cadd4f8SNickeau
1644cadd4f8SNickeau    /**
1654cadd4f8SNickeau     * Link relation attributes
1664cadd4f8SNickeau     * https://html.spec.whatwg.org/multipage/links.html#linkTypes
1674cadd4f8SNickeau     */
1684cadd4f8SNickeau    const REL = "rel";
1694cadd4f8SNickeau
17004fd306cSNickeau    /**
17104fd306cSNickeau     * The default id if no one is specified
17204fd306cSNickeau     */
17304fd306cSNickeau    const GENERATED_ID_KEY = "generated_id";
17404fd306cSNickeau
17504fd306cSNickeau    /**
17604fd306cSNickeau     * The attributes that may flow into an HTML output
17704fd306cSNickeau     * TODO: href comes from {@link \syntax_plugin_combo_brand}, it should be corrected to use {@link LinkMarkup}
17804fd306cSNickeau     */
17904fd306cSNickeau    const HTML_ATTRIBUTES = [
18004fd306cSNickeau        TagAttributes::CLASS_KEY,
18104fd306cSNickeau        StyleAttribute::STYLE_ATTRIBUTE,
18204fd306cSNickeau        TagAttributes::ID_KEY,
18304fd306cSNickeau        TagAttributes::TITLE_KEY,
18404fd306cSNickeau        "href",
18504fd306cSNickeau        "rel", // anchor
18604fd306cSNickeau        "name", // iframe
18704fd306cSNickeau        "frameborder", // iframe
18804fd306cSNickeau        "target" // a
18904fd306cSNickeau    ];
19004fd306cSNickeau
19104fd306cSNickeau    /**
19204fd306cSNickeau     * Attribute that cannot be deleted
19304fd306cSNickeau     * TODO: This is because the request object and the response object are the same. We should add the request attribute in the {@link \TagAttributes}
19404fd306cSNickeau     */
19504fd306cSNickeau    const PROTECTED_ATTRIBUTES = [
19604fd306cSNickeau        TagAttributes::TYPE_KEY
19704fd306cSNickeau    ];
19804fd306cSNickeau    const NAME_ATTRIBUTE = "name";
19904fd306cSNickeau
20004fd306cSNickeau    /**
20104fd306cSNickeau     * The dokuwiki name attribute to store
20204fd306cSNickeau     * text node data
20304fd306cSNickeau     */
20404fd306cSNickeau    public const DOKUWIKI_TEXT_NODE_ATTRIBUTE = "_data";
20582a60d03SNickeau
20682a60d03SNickeau    /**
20737748cd8SNickeau     * A global static counter
20837748cd8SNickeau     * to {@link TagAttributes::generateAndSetId()}
20937748cd8SNickeau     */
21037748cd8SNickeau    private static $counter = 0;
21137748cd8SNickeau
21237748cd8SNickeau
21337748cd8SNickeau    /**
21404fd306cSNickeau     * @var ArrayCaseInsensitive attribute that were set on a component
21537748cd8SNickeau     */
21604fd306cSNickeau    private ArrayCaseInsensitive $componentAttributesCaseInsensitive;
21737748cd8SNickeau
21837748cd8SNickeau    /**
21937748cd8SNickeau     * @var array the style declaration array
22037748cd8SNickeau     */
22104fd306cSNickeau    private array $styleDeclaration = array();
22237748cd8SNickeau
22337748cd8SNickeau    /**
22437748cd8SNickeau     * @var bool - set when the transformation from component attribute to html attribute
22537748cd8SNickeau     * was done to avoid circular problem
22637748cd8SNickeau     */
22737748cd8SNickeau    private $componentToHtmlAttributeProcessingWasDone = false;
22837748cd8SNickeau
22937748cd8SNickeau    /**
2304cadd4f8SNickeau     * @var array - output attribute are not the parsed attributes known as componentAttribute)
2314cadd4f8SNickeau     * They are created by the {@link TagAttributes::toHtmlArray()} processing mainly
23237748cd8SNickeau     */
2334cadd4f8SNickeau    private $outputAttributes = array();
23437748cd8SNickeau
23537748cd8SNickeau    /**
23637748cd8SNickeau     * @var array - the final html array
23737748cd8SNickeau     */
23837748cd8SNickeau    private $finalHtmlArray = array();
23937748cd8SNickeau
24037748cd8SNickeau    /**
24137748cd8SNickeau     * @var string the functional tag to which the attributes applies
24237748cd8SNickeau     * It's not an HTML tag (a div can have a flex display or a block and they don't carry this information)
24337748cd8SNickeau     * The tag gives also context for the attributes (ie an div has no natural width while an img has)
24437748cd8SNickeau     */
24537748cd8SNickeau    private $logicalTag;
24637748cd8SNickeau
24737748cd8SNickeau    /**
24837748cd8SNickeau     * An html that should be added after the enter tag
24937748cd8SNickeau     * (used for instance to add metadata  such as backgrounds, illustration image for cards ,...
25037748cd8SNickeau     * @var string
25137748cd8SNickeau     */
25237748cd8SNickeau    private $htmlAfterEnterTag;
25337748cd8SNickeau
25437748cd8SNickeau    /**
25537748cd8SNickeau     * Use to make the difference between
25637748cd8SNickeau     * an HTTP call for a media (ie SVG) vs an HTTP call for a page (HTML)
25737748cd8SNickeau     */
25837748cd8SNickeau    const TEXT_HTML_MIME = "text/html";
25937748cd8SNickeau    private $mime = TagAttributes::TEXT_HTML_MIME;
26037748cd8SNickeau
2614cadd4f8SNickeau    /**
2624cadd4f8SNickeau     * @var bool - adding  the default class for the logical tag
2634cadd4f8SNickeau     */
2644cadd4f8SNickeau    private $defaultStyleClassShouldBeAdded = true;
26504fd306cSNickeau    private $knownTypes;
26604fd306cSNickeau
26704fd306cSNickeau    /**
26804fd306cSNickeau     * @var string - the inner Text (used for script or style tag mostly)
26904fd306cSNickeau     */
27004fd306cSNickeau    private string $innerText;
2714cadd4f8SNickeau
27237748cd8SNickeau
27337748cd8SNickeau    /**
27437748cd8SNickeau     * ComponentAttributes constructor.
27537748cd8SNickeau     * Use the static create function to instantiate this object
27637748cd8SNickeau     * @param $tag - tag (the tag gives context for the attributes
27737748cd8SNickeau     *     * an div has no natural width while an img has
27837748cd8SNickeau     *     * this is not always the component name / syntax name (for instance the {@link \syntax_plugin_combo_codemarkdown} is another syntax
27937748cd8SNickeau     * for a {@link \syntax_plugin_combo_code} and have therefore the same logical name)
28037748cd8SNickeau     * @param array $componentAttributes
28137748cd8SNickeau     */
2824cadd4f8SNickeau    private function __construct(array $componentAttributes = array(), $tag = null)
28337748cd8SNickeau    {
28437748cd8SNickeau        $this->logicalTag = $tag;
28537748cd8SNickeau        $this->componentAttributesCaseInsensitive = new ArrayCaseInsensitive($componentAttributes);
28637748cd8SNickeau
28737748cd8SNickeau        /**
28837748cd8SNickeau         * Delete null values
28937748cd8SNickeau         * Empty string, 0 may exist
29037748cd8SNickeau         */
29137748cd8SNickeau        foreach ($componentAttributes as $key => $value) {
29237748cd8SNickeau            if (is_null($value)) {
29337748cd8SNickeau                unset($this->componentAttributesCaseInsensitive[$key]);
2944cadd4f8SNickeau                continue;
2954cadd4f8SNickeau            }
29604fd306cSNickeau            if ($key === StyleAttribute::STYLE_ATTRIBUTE) {
2974cadd4f8SNickeau                unset($this->componentAttributesCaseInsensitive[$key]);
29804fd306cSNickeau                LogUtility::warning("The style attribute cannot be set or used due to security. Uses the combostrap style attribute or set a class attibute instead.");
29937748cd8SNickeau            }
30037748cd8SNickeau        }
30137748cd8SNickeau
30237748cd8SNickeau    }
30337748cd8SNickeau
30437748cd8SNickeau    /**
30537748cd8SNickeau     * @param $match - the {@link SyntaxPlugin::handle()} match
30604fd306cSNickeau     * @param array $defaultAttributes - the default attributes values
30704fd306cSNickeau     * @param array $knownTypes - the known types
30804fd306cSNickeau     * @param bool $allowFirstBooleanAttributesAsType - if the first attribute is a boolean, make it a type
30937748cd8SNickeau     * @return TagAttributes
31037748cd8SNickeau     */
31104fd306cSNickeau    public static function createFromTagMatch($match, array $defaultAttributes = [], array $knownTypes = [], bool $allowFirstBooleanAttributesAsType = false): TagAttributes
31237748cd8SNickeau    {
31304fd306cSNickeau        $inlineHtmlAttributes = PluginUtility::getTagAttributes($match, $knownTypes, $allowFirstBooleanAttributesAsType);
31404fd306cSNickeau        $tag = PluginUtility::getMarkupTag($match);
31537748cd8SNickeau        $mergedAttributes = PluginUtility::mergeAttributes($inlineHtmlAttributes, $defaultAttributes);
31604fd306cSNickeau        return (new TagAttributes($mergedAttributes, $tag))
31704fd306cSNickeau            ->setKnownTypes($knownTypes);
31837748cd8SNickeau    }
31937748cd8SNickeau
32037748cd8SNickeau
32104fd306cSNickeau    public static function createEmpty($logicalTag = ""): TagAttributes
32237748cd8SNickeau    {
32337748cd8SNickeau        if ($logicalTag !== "") {
32437748cd8SNickeau            return new TagAttributes([], $logicalTag);
32537748cd8SNickeau        } else {
32637748cd8SNickeau            return new TagAttributes();
32737748cd8SNickeau        }
32837748cd8SNickeau    }
32937748cd8SNickeau
33037748cd8SNickeau    /**
3314cadd4f8SNickeau     * @param array|null $callStackArray - an array of key value pair
3324cadd4f8SNickeau     * @param string|null $logicalTag - the logical tag for which this attribute will apply
33337748cd8SNickeau     * @return TagAttributes
33437748cd8SNickeau     */
3354cadd4f8SNickeau    public static function createFromCallStackArray(?array $callStackArray, string $logicalTag = null): TagAttributes
33637748cd8SNickeau    {
3374cadd4f8SNickeau        if ($callStackArray === null) {
3384cadd4f8SNickeau            $callStackArray = [];
33937748cd8SNickeau        }
3404cadd4f8SNickeau        if (!is_array($callStackArray)) {
3414cadd4f8SNickeau            LogUtility::msg("The renderArray variable passed is not an array ($callStackArray)");
3424cadd4f8SNickeau            $callStackArray = [];
3434cadd4f8SNickeau        }
34404fd306cSNickeau        /**
34504fd306cSNickeau         * Style is not allowed in a TagAttributes
34604fd306cSNickeau         *
34704fd306cSNickeau         * Because callstack is safe,
34804fd306cSNickeau         * style have been added by plugin
34904fd306cSNickeau         * For instance, the card had a `max-width style of 100%` to the image
35004fd306cSNickeau         *
35104fd306cSNickeau         * We capture it and add them afterwards
35204fd306cSNickeau         */
35304fd306cSNickeau        if (isset($callStackArray[StyleAttribute::STYLE_ATTRIBUTE])) {
35404fd306cSNickeau            $style = $callStackArray[StyleAttribute::STYLE_ATTRIBUTE];
35504fd306cSNickeau            unset($callStackArray[StyleAttribute::STYLE_ATTRIBUTE]);
35604fd306cSNickeau        }
35704fd306cSNickeau
35804fd306cSNickeau        $tagAttributes = new TagAttributes($callStackArray, $logicalTag);
35904fd306cSNickeau
36004fd306cSNickeau        /**
36104fd306cSNickeau         * Add the styles
36204fd306cSNickeau         */
36304fd306cSNickeau        if (isset($style)) {
36404fd306cSNickeau            $stylingProperties = StyleAttribute::HtmlStyleValueToArray($style);
36504fd306cSNickeau            foreach ($stylingProperties as $styleKey => $styleValue) {
36604fd306cSNickeau                $tagAttributes->addStyleDeclarationIfNotSet($styleKey, $styleValue);
36704fd306cSNickeau            }
36804fd306cSNickeau        }
36904fd306cSNickeau
37004fd306cSNickeau        return $tagAttributes;
37137748cd8SNickeau    }
37237748cd8SNickeau
37337748cd8SNickeau
37437748cd8SNickeau    /**
37537748cd8SNickeau     * For CSS a unit is mandatory (not for HTML or SVG attributes)
37637748cd8SNickeau     * @param $value
37737748cd8SNickeau     * @return string return a CSS property with pixel as unit if the unit is not specified
37804fd306cSNickeau     * @throws ExceptionBadArgument
37937748cd8SNickeau     */
3804cadd4f8SNickeau    public static function toQualifiedCssValue($value): string
38137748cd8SNickeau    {
38204fd306cSNickeau        return ConditionalLength::createFromString($value)->toCssLength();
38337748cd8SNickeau
38437748cd8SNickeau    }
38537748cd8SNickeau
38637748cd8SNickeau    /**
38737748cd8SNickeau     * Function used to normalize the attribute name to the combostrap attribute name
38837748cd8SNickeau     * @param $name
38937748cd8SNickeau     * @return mixed|string
39037748cd8SNickeau     */
39137748cd8SNickeau    public static function AttributeNameFromDokuwikiToCombo($name)
39237748cd8SNickeau    {
39337748cd8SNickeau        switch ($name) {
39437748cd8SNickeau            case "w":
39537748cd8SNickeau                return Dimension::WIDTH_KEY;
39637748cd8SNickeau            case "h":
39737748cd8SNickeau                return Dimension::HEIGHT_KEY;
39837748cd8SNickeau            default:
39937748cd8SNickeau                return $name;
40037748cd8SNickeau        }
40137748cd8SNickeau    }
40237748cd8SNickeau
403c3437056SNickeau    /**
404c3437056SNickeau     * Clone a tag attributes
405c3437056SNickeau     * Tag Attributes are used for request and for response
406c3437056SNickeau     * To avoid conflict, a function should clone it before
407c3437056SNickeau     * calling the final method {@link TagAttributes::toHtmlArray()}
408c3437056SNickeau     * or {@link TagAttributes::toHtmlEnterTag()}
409c3437056SNickeau     * @param TagAttributes $tagAttributes
410c3437056SNickeau     * @return TagAttributes
411c3437056SNickeau     */
41204fd306cSNickeau    public static function createFromTagAttributeString(TagAttributes $tagAttributes): TagAttributes
413c3437056SNickeau    {
41482a60d03SNickeau        $newTagAttributes = new TagAttributes($tagAttributes->getComponentAttributes(), $tagAttributes->getLogicalTag());
41582a60d03SNickeau        foreach ($tagAttributes->getStyleDeclarations() as $property => $value) {
41682a60d03SNickeau            $newTagAttributes->addStyleDeclarationIfNotSet($property, $value);
41782a60d03SNickeau        }
41882a60d03SNickeau        return $newTagAttributes;
419c3437056SNickeau    }
420c3437056SNickeau
42104fd306cSNickeau    public static function isEmptyValue($attributeValue): bool
42282a60d03SNickeau    {
42304fd306cSNickeau        return empty($attributeValue) && !is_bool($attributeValue);
42482a60d03SNickeau    }
42582a60d03SNickeau
42682a60d03SNickeau    public function addClassName($className): TagAttributes
42737748cd8SNickeau    {
42837748cd8SNickeau
42937748cd8SNickeau        $this->addComponentAttributeValue(self::CLASS_KEY, $className);
43037748cd8SNickeau        return $this;
43137748cd8SNickeau
43237748cd8SNickeau    }
43337748cd8SNickeau
43404fd306cSNickeau    /**
43504fd306cSNickeau     * @throws ExceptionNull
43604fd306cSNickeau     */
43704fd306cSNickeau    public function getClass($default = null)
43837748cd8SNickeau    {
43904fd306cSNickeau        $value = $this->getValue(self::CLASS_KEY, $default);
44004fd306cSNickeau        if ($value !== null) {
44104fd306cSNickeau            return $value;
44204fd306cSNickeau        }
44304fd306cSNickeau        throw new ExceptionNull("No class was found");
44437748cd8SNickeau    }
44537748cd8SNickeau
44637748cd8SNickeau    /**
44704fd306cSNickeau     * @return string
44804fd306cSNickeau     * @throws ExceptionNotFound
44937748cd8SNickeau     */
45004fd306cSNickeau    public function getStyle(): string
45104fd306cSNickeau    {
45204fd306cSNickeau
45304fd306cSNickeau        if (sizeof($this->styleDeclaration) === 0) {
45404fd306cSNickeau            throw new ExceptionNotFound("No style");
45537748cd8SNickeau        }
45604fd306cSNickeau        return Html::array2InlineStyle($this->styleDeclaration);
45737748cd8SNickeau
45837748cd8SNickeau    }
45937748cd8SNickeau
46082a60d03SNickeau    public function getStyleDeclarations(): array
46182a60d03SNickeau    {
46282a60d03SNickeau        return $this->styleDeclaration;
46382a60d03SNickeau
46482a60d03SNickeau    }
46582a60d03SNickeau
46637748cd8SNickeau    /**
46737748cd8SNickeau     * Add an attribute with its value if the value is not empty
46837748cd8SNickeau     * @param $attributeName
46937748cd8SNickeau     * @param $attributeValue
4704cadd4f8SNickeau     * @return TagAttributes
47137748cd8SNickeau     */
4724cadd4f8SNickeau    public function addComponentAttributeValue($attributeName, $attributeValue): TagAttributes
47337748cd8SNickeau    {
47437748cd8SNickeau
47504fd306cSNickeau        if (TagAttributes::isEmptyValue($attributeValue)) {
4764cadd4f8SNickeau            LogUtility::msg("The value of the attribute ($attributeName) is empty. Use the nonEmpty function instead if it's the wanted behavior", LogUtility::LVL_MSG_WARNING, "support");
47737748cd8SNickeau        }
47837748cd8SNickeau
47937748cd8SNickeau        $attLower = strtolower($attributeName);
48082a60d03SNickeau        $actual = null;
48137748cd8SNickeau        if ($this->hasComponentAttribute($attLower)) {
48237748cd8SNickeau            $actual = $this->componentAttributesCaseInsensitive[$attLower];
48337748cd8SNickeau        }
48437748cd8SNickeau
48537748cd8SNickeau        /**
48637748cd8SNickeau         * Type of data: list (class) or atomic (id)
48737748cd8SNickeau         */
48882a60d03SNickeau        if (in_array($attributeName, self::MULTIPLE_VALUES_ATTRIBUTES)) {
48904fd306cSNickeau            $this->componentAttributesCaseInsensitive[$attLower] = Html::mergeClassNames($attributeValue, $actual);
49037748cd8SNickeau        } else {
49137748cd8SNickeau            if (!empty($actual)) {
49237748cd8SNickeau                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);
49337748cd8SNickeau            }
49437748cd8SNickeau            $this->componentAttributesCaseInsensitive[$attLower] = $attributeValue;
49537748cd8SNickeau        }
49637748cd8SNickeau
49782a60d03SNickeau        return $this;
49837748cd8SNickeau
49937748cd8SNickeau    }
50037748cd8SNickeau
50104fd306cSNickeau    public function setComponentAttributeValue($attributeName, $attributeValue): TagAttributes
50237748cd8SNickeau    {
50337748cd8SNickeau        $attLower = strtolower($attributeName);
50437748cd8SNickeau        $actualValue = $this->getValue($attributeName);
50537748cd8SNickeau        if ($actualValue === null || $actualValue !== TagAttributes::UN_SET) {
50637748cd8SNickeau            $this->componentAttributesCaseInsensitive[$attLower] = $attributeValue;
50737748cd8SNickeau        }
50804fd306cSNickeau        return $this;
50937748cd8SNickeau    }
51037748cd8SNickeau
51137748cd8SNickeau    public function addComponentAttributeValueIfNotEmpty($attributeName, $attributeValue)
51237748cd8SNickeau    {
51337748cd8SNickeau        if (!empty($attributeValue)) {
51437748cd8SNickeau            $this->addComponentAttributeValue($attributeName, $attributeValue);
51537748cd8SNickeau        }
51637748cd8SNickeau    }
51737748cd8SNickeau
51804fd306cSNickeau    public function hasComponentAttribute($attributeName): bool
51937748cd8SNickeau    {
52037748cd8SNickeau        $isset = isset($this->componentAttributesCaseInsensitive[$attributeName]);
52104fd306cSNickeau        if ($isset === false && $this->knownTypes === null) {
52237748cd8SNickeau            /**
52337748cd8SNickeau             * Edge effect
52437748cd8SNickeau             * if this is a boolean value and the first value, it may be stored in the type
52537748cd8SNickeau             */
52637748cd8SNickeau            if (isset($this->componentAttributesCaseInsensitive[TagAttributes::TYPE_KEY])) {
52737748cd8SNickeau                if ($attributeName == $this->componentAttributesCaseInsensitive[TagAttributes::TYPE_KEY]) {
52804fd306cSNickeau                    LogUtility::warning("Internal Error: The tag ({$this->getLogicalTag()}) has a boolean attribute ($attributeName) defined as a type. The possible types should be defined for this tag as it's deprecated.");
52937748cd8SNickeau                    return true;
53037748cd8SNickeau                }
53137748cd8SNickeau            }
53237748cd8SNickeau        }
53337748cd8SNickeau        return $isset;
53437748cd8SNickeau    }
53537748cd8SNickeau
53637748cd8SNickeau    /**
53737748cd8SNickeau     * To an HTML array in the form
53837748cd8SNickeau     *   class => 'value1 value2',
53937748cd8SNickeau     *   att => 'value1 value 2'
54037748cd8SNickeau     * For historic reason, data passed between the handle and the render
54137748cd8SNickeau     * can still be in this format
54237748cd8SNickeau     */
543c3437056SNickeau    public function toHtmlArray(): array
54437748cd8SNickeau    {
545c3437056SNickeau        if ($this->componentToHtmlAttributeProcessingWasDone) {
546c3437056SNickeau            LogUtility::msg("This tag attribute ($this) was already finalized. You cannot finalized it twice", LogUtility::LVL_MSG_ERROR);
547c3437056SNickeau            return $this->finalHtmlArray;
548c3437056SNickeau        }
54937748cd8SNickeau
55037748cd8SNickeau        $this->componentToHtmlAttributeProcessingWasDone = true;
55137748cd8SNickeau
55237748cd8SNickeau        /**
55337748cd8SNickeau         * Width and height
55437748cd8SNickeau         */
55537748cd8SNickeau        Dimension::processWidthAndHeight($this);
55637748cd8SNickeau
55737748cd8SNickeau        /**
55837748cd8SNickeau         * Process animation (onHover, onView)
55937748cd8SNickeau         */
56037748cd8SNickeau        Hover::processOnHover($this);
56137748cd8SNickeau        Animation::processOnView($this);
56237748cd8SNickeau
56337748cd8SNickeau
56437748cd8SNickeau        /**
56537748cd8SNickeau         * Position and Stickiness
56637748cd8SNickeau         */
56737748cd8SNickeau        Position::processStickiness($this);
56837748cd8SNickeau        Position::processPosition($this);
5694cadd4f8SNickeau        Display::processDisplay($this);
57004fd306cSNickeau        Vertical::processVertical($this);
57104fd306cSNickeau        Horizontal::processHorizontal($this);
57237748cd8SNickeau
57337748cd8SNickeau        /**
57437748cd8SNickeau         * Block processing
57537748cd8SNickeau         *
57637748cd8SNickeau         * Float, align, spacing
57737748cd8SNickeau         */
57837748cd8SNickeau        FloatAttribute::processFloat($this);
57937748cd8SNickeau        Align::processAlignAttributes($this);
58037748cd8SNickeau        Spacing::processSpacingAttributes($this);
58104fd306cSNickeau        Hero::processHero($this);
58237748cd8SNickeau        Opacity::processOpacityAttribute($this);
58304fd306cSNickeau        BackgroundAttribute::processBackgroundAttributes($this);
58437748cd8SNickeau        Shadow::process($this);
58537748cd8SNickeau
58637748cd8SNickeau        /**
58737748cd8SNickeau         * Process text attributes
58837748cd8SNickeau         */
58937748cd8SNickeau        LineSpacing::processLineSpacingAttributes($this);
59037748cd8SNickeau        TextAlign::processTextAlign($this);
59137748cd8SNickeau        Boldness::processBoldnessAttribute($this);
59237748cd8SNickeau        FontSize::processFontSizeAttribute($this);
59337748cd8SNickeau        TextColor::processTextColorAttribute($this);
59437748cd8SNickeau        Underline::processUnderlineAttribute($this);
59537748cd8SNickeau
59637748cd8SNickeau        /**
59737748cd8SNickeau         * Process the style attributes if any
59837748cd8SNickeau         */
59937748cd8SNickeau        PluginUtility::processStyle($this);
60037748cd8SNickeau        Toggle::processToggle($this);
60137748cd8SNickeau
60237748cd8SNickeau
60337748cd8SNickeau        /**
60437748cd8SNickeau         * Skin Attribute
60537748cd8SNickeau         */
60637748cd8SNickeau        Skin::processSkinAttribute($this);
60737748cd8SNickeau
60837748cd8SNickeau        /**
60937748cd8SNickeau         * Lang
61037748cd8SNickeau         */
61137748cd8SNickeau        Lang::processLangAttribute($this);
61237748cd8SNickeau
61337748cd8SNickeau        /**
61437748cd8SNickeau         * Transform
61537748cd8SNickeau         */
61637748cd8SNickeau        if ($this->hasComponentAttribute(self::TRANSFORM)) {
61737748cd8SNickeau            $transformValue = $this->getValueAndRemove(self::TRANSFORM);
61882a60d03SNickeau            $this->addStyleDeclarationIfNotSet("transform", $transformValue);
61937748cd8SNickeau        }
62037748cd8SNickeau
62104fd306cSNickeau
62237748cd8SNickeau        /**
6234cadd4f8SNickeau         * Tooltip
6244cadd4f8SNickeau         */
6254cadd4f8SNickeau        Tooltip::processTooltip($this);
6264cadd4f8SNickeau
6274cadd4f8SNickeau        /**
62837748cd8SNickeau         * Add the type class used for CSS styling
62937748cd8SNickeau         */
63004fd306cSNickeau        StyleAttribute::addStylingClass($this);
63137748cd8SNickeau
63237748cd8SNickeau        /**
63337748cd8SNickeau         * Add the style has html attribute
63437748cd8SNickeau         * before processing
63537748cd8SNickeau         */
63604fd306cSNickeau        try {
6374cadd4f8SNickeau            $this->addOutputAttributeValueIfNotEmpty("style", $this->getStyle());
63804fd306cSNickeau        } catch (ExceptionNotFound $e) {
63904fd306cSNickeau            // no style
64004fd306cSNickeau        }
64137748cd8SNickeau
64237748cd8SNickeau        /**
64337748cd8SNickeau         * Create a non-sorted temporary html attributes array
64437748cd8SNickeau         */
6454cadd4f8SNickeau        $tempHtmlArray = $this->outputAttributes;
64637748cd8SNickeau
64737748cd8SNickeau        /**
64837748cd8SNickeau         * copy the unknown component attributes
64937748cd8SNickeau         */
65037748cd8SNickeau        $originalArray = $this->componentAttributesCaseInsensitive->getOriginalArray();
65137748cd8SNickeau        foreach ($originalArray as $key => $value) {
65237748cd8SNickeau
65337748cd8SNickeau            // Null Value, not needed
65437748cd8SNickeau            if (is_null($value)) {
65537748cd8SNickeau                continue;
65637748cd8SNickeau            }
65737748cd8SNickeau
65837748cd8SNickeau            // No overwrite
65937748cd8SNickeau            if (isset($tempHtmlArray[$key])) {
66037748cd8SNickeau                continue;
66137748cd8SNickeau            }
66237748cd8SNickeau
66304fd306cSNickeau            // We only add the common HTML attribute
66404fd306cSNickeau            if (in_array($key, self::HTML_ATTRIBUTES) || strpos($key, 'data-') === 0) {
66537748cd8SNickeau                $tempHtmlArray[$key] = $value;
66604fd306cSNickeau            } else {
66704fd306cSNickeau
66804fd306cSNickeau                if (!in_array($key, [
66904fd306cSNickeau                    TagAttributes::TYPE_KEY,
67004fd306cSNickeau                    TagAttributes::GENERATED_ID_KEY,
67104fd306cSNickeau                    TagAttributes::OPEN_TAG
67204fd306cSNickeau                ])) {
67304fd306cSNickeau
67404fd306cSNickeau                    /**
67504fd306cSNickeau                     * Note for developers:
67604fd306cSNickeau                     *    * If it must be in the HTML output, you should add it via the output attribute methods during processing.
67704fd306cSNickeau                     *    * Otherwise you need for now to get and delete it
67804fd306cSNickeau                     */
67904fd306cSNickeau                    $message = "The component attribute ($key) is unknown or does not apply ";
68004fd306cSNickeau                    if (isset($this->logicalTag)) {
68104fd306cSNickeau                        $message = "$message for the component ({$this->logicalTag}).";
68204fd306cSNickeau                    }
68304fd306cSNickeau                    LogUtility::warning($message);
68404fd306cSNickeau
68504fd306cSNickeau                }
68637748cd8SNickeau            }
68737748cd8SNickeau
68837748cd8SNickeau        }
68937748cd8SNickeau
69037748cd8SNickeau
69137748cd8SNickeau        /**
69237748cd8SNickeau         * Sort by attribute
69337748cd8SNickeau         * https://datacadamia.com/web/html/attribute#order
69404fd306cSNickeau         * https://codeguide.co/#html-attribute-order
69537748cd8SNickeau         */
69637748cd8SNickeau        $sortedArray = array();
69737748cd8SNickeau        $once = "once";
69837748cd8SNickeau        $multiple = "multiple";
69937748cd8SNickeau        $orderPatterns = [
70037748cd8SNickeau            "class" => $once,
70137748cd8SNickeau            "id" => $once,
70237748cd8SNickeau            "name" => $once,
70337748cd8SNickeau            "data-.*" => $multiple,
70437748cd8SNickeau            "src.*" => $multiple,
70537748cd8SNickeau            "for" => $once,
70637748cd8SNickeau            "type" => $once,
70737748cd8SNickeau            "href" => $once,
70837748cd8SNickeau            "value" => $once,
70937748cd8SNickeau            "title" => $once,
71037748cd8SNickeau            "alt" => $once,
71137748cd8SNickeau            "role" => $once,
71237748cd8SNickeau            "aria-*" => $multiple];
71337748cd8SNickeau        foreach ($orderPatterns as $pattern => $type) {
71437748cd8SNickeau            foreach ($tempHtmlArray as $name => $value) {
71537748cd8SNickeau                $searchPattern = "^$pattern$";
71637748cd8SNickeau                if (preg_match("/$searchPattern/", $name)) {
71737748cd8SNickeau                    unset($tempHtmlArray[$name]);
7184cadd4f8SNickeau                    if ($type === $once) {
7194cadd4f8SNickeau                        $sortedArray[$name] = $value;
7204cadd4f8SNickeau                        continue 2;
7214cadd4f8SNickeau                    } else {
7224cadd4f8SNickeau                        $multipleValues[$name] = $value;
72337748cd8SNickeau                    }
72437748cd8SNickeau                }
72537748cd8SNickeau            }
7264cadd4f8SNickeau            if (!empty($multipleValues)) {
7274cadd4f8SNickeau                ksort($multipleValues);
7284cadd4f8SNickeau                $sortedArray = array_merge($sortedArray, $multipleValues);
7294cadd4f8SNickeau                $multipleValues = [];
7304cadd4f8SNickeau            }
73137748cd8SNickeau        }
73237748cd8SNickeau        foreach ($tempHtmlArray as $name => $value) {
73337748cd8SNickeau
73437748cd8SNickeau            if (!is_null($value)) {
73537748cd8SNickeau                /**
73637748cd8SNickeau                 *
73737748cd8SNickeau                 * Don't add a filter on the empty values
73837748cd8SNickeau                 *
73937748cd8SNickeau                 * The value of an HTML attribute may be empty
74037748cd8SNickeau                 * Example the wiki id of the root namespace
74137748cd8SNickeau                 *
7424cadd4f8SNickeau                 * By default, {@link TagAttributes::addOutputAttributeValue()}
74337748cd8SNickeau                 * will not accept any value, it must be implicitly said with the
7444cadd4f8SNickeau                 * {@link TagAttributes::addOutputAttributeValue()}
74537748cd8SNickeau                 *
74637748cd8SNickeau                 */
74737748cd8SNickeau                $sortedArray[$name] = $value;
74837748cd8SNickeau            }
74937748cd8SNickeau
75037748cd8SNickeau        }
75137748cd8SNickeau        $this->finalHtmlArray = $sortedArray;
75237748cd8SNickeau
7534cadd4f8SNickeau        /**
7544cadd4f8SNickeau         * To Html attribute encoding
7554cadd4f8SNickeau         */
7564cadd4f8SNickeau        $this->finalHtmlArray = $this->encodeToHtmlValue($this->finalHtmlArray);
7574cadd4f8SNickeau
75837748cd8SNickeau        return $this->finalHtmlArray;
75937748cd8SNickeau
76037748cd8SNickeau    }
76137748cd8SNickeau
76237748cd8SNickeau    /**
7634cadd4f8SNickeau     *
7644cadd4f8SNickeau     *
76537748cd8SNickeau     * @param $key
76637748cd8SNickeau     * @param $value
76737748cd8SNickeau     * @return TagAttributes
76837748cd8SNickeau     */
7694cadd4f8SNickeau    public function addOutputAttributeValue($key, $value): TagAttributes
77037748cd8SNickeau    {
77104fd306cSNickeau
77237748cd8SNickeau        if (blank($value)) {
77304fd306cSNickeau            LogUtility::error("The value of the output attribute is blank for the key ($key) - Tag ($this->logicalTag). Use the empty / boolean function if the value can be empty");
77437748cd8SNickeau        }
77504fd306cSNickeau
776*70bbd7f1Sgerardnico        $actualValue = $this->outputAttributes[$key] ?? null;
77704fd306cSNickeau        if ($actualValue === null) {
7784cadd4f8SNickeau            $this->outputAttributes[$key] = $value;
77937748cd8SNickeau            return $this;
78037748cd8SNickeau        }
78137748cd8SNickeau
78204fd306cSNickeau        if (!in_array($key, self::MULTIPLE_VALUES_ATTRIBUTES)) {
78304fd306cSNickeau            LogUtility::internalError("The output attribute ($key) was already set with the value ($actualValue), we have added the value ($value)");
78404fd306cSNickeau        }
78504fd306cSNickeau
78604fd306cSNickeau        $this->outputAttributes[$key] = "$value $actualValue";
78704fd306cSNickeau        return $this;
78804fd306cSNickeau
78904fd306cSNickeau    }
79004fd306cSNickeau
79137748cd8SNickeau
7924cadd4f8SNickeau    public function addOutputAttributeValueIfNotEmpty($key, $value)
79337748cd8SNickeau    {
79437748cd8SNickeau        if (!empty($value)) {
7954cadd4f8SNickeau            $this->addOutputAttributeValue($key, $value);
79637748cd8SNickeau        }
79737748cd8SNickeau    }
79837748cd8SNickeau
79937748cd8SNickeau    /**
80037748cd8SNickeau     * @param $attributeName
80137748cd8SNickeau     * @param null $default
80237748cd8SNickeau     * @return string|array|null a HTML value in the form 'value1 value2...'
80337748cd8SNickeau     */
80437748cd8SNickeau    public function getValue($attributeName, $default = null)
80537748cd8SNickeau    {
80637748cd8SNickeau        $attributeName = strtolower($attributeName);
80737748cd8SNickeau        if ($this->hasComponentAttribute($attributeName)) {
80837748cd8SNickeau            return $this->componentAttributesCaseInsensitive[$attributeName];
80937748cd8SNickeau        } else {
81037748cd8SNickeau            return $default;
81137748cd8SNickeau        }
81237748cd8SNickeau    }
81337748cd8SNickeau
81437748cd8SNickeau
81537748cd8SNickeau    /**
81637748cd8SNickeau     * Get the value and remove it from the attributes
81737748cd8SNickeau     * @param $attributeName
81837748cd8SNickeau     * @param $default
81937748cd8SNickeau     * @return string|array|null
82004fd306cSNickeau     *
82104fd306cSNickeau     * TODO: we should create a new response object and not deleting data from the request
82237748cd8SNickeau     */
82337748cd8SNickeau    public function getValueAndRemove($attributeName, $default = null)
82437748cd8SNickeau    {
82537748cd8SNickeau        $attributeName = strtolower($attributeName);
82637748cd8SNickeau        $value = $default;
82737748cd8SNickeau        if ($this->hasComponentAttribute($attributeName)) {
82837748cd8SNickeau            $value = $this->getValue($attributeName);
82937748cd8SNickeau
83004fd306cSNickeau            if (!in_array($attributeName, self::PROTECTED_ATTRIBUTES)) {
83137748cd8SNickeau                /**
83237748cd8SNickeau                 * Don't remove for instance the `type`
83337748cd8SNickeau                 * because it may be used elsewhere
83437748cd8SNickeau                 */
83537748cd8SNickeau                unset($this->componentAttributesCaseInsensitive[$attributeName]);
83637748cd8SNickeau            }
83737748cd8SNickeau
83837748cd8SNickeau        }
83937748cd8SNickeau        return $value;
84037748cd8SNickeau    }
84137748cd8SNickeau
84237748cd8SNickeau
84337748cd8SNickeau    /**
84437748cd8SNickeau     * @return array - an array of key string and value of the component attributes
84537748cd8SNickeau     * This array is saved on the disk
84637748cd8SNickeau     */
8474cadd4f8SNickeau    public function toCallStackArray(): array
84837748cd8SNickeau    {
84904fd306cSNickeau
85004fd306cSNickeau        $generatedId = $this->getValue(TagAttributes::GENERATED_ID_KEY);
85104fd306cSNickeau        if ($generatedId === null) {
85204fd306cSNickeau
85304fd306cSNickeau            $componentName = $this->logicalTag;
85404fd306cSNickeau            if ($componentName === null) {
85504fd306cSNickeau                $componentName = "unknown-component";
85604fd306cSNickeau            }
85704fd306cSNickeau            $id = ExecutionContext::getActualOrCreateFromEnv()
85804fd306cSNickeau                ->getIdManager()
85904fd306cSNickeau                ->generateNewHtmlIdForComponent($componentName);
86004fd306cSNickeau            $this->addComponentAttributeValue(TagAttributes::GENERATED_ID_KEY, $id);
86104fd306cSNickeau
86204fd306cSNickeau        }
86304fd306cSNickeau
86437748cd8SNickeau        $array = array();
86537748cd8SNickeau        $originalArray = $this->componentAttributesCaseInsensitive->getOriginalArray();
86637748cd8SNickeau        foreach ($originalArray as $key => $value) {
86737748cd8SNickeau            /**
86837748cd8SNickeau             * Only null value are not passed
86937748cd8SNickeau             * width can be zero, wiki-id can be the empty string (ie root namespace)
87004fd306cSNickeau             *
87104fd306cSNickeau             * Value can be array, number, string
87237748cd8SNickeau             */
87337748cd8SNickeau            if (!is_null($value)) {
87404fd306cSNickeau                $array[$key] = $value;
87537748cd8SNickeau            }
87637748cd8SNickeau        }
8774cadd4f8SNickeau        /**
8784cadd4f8SNickeau         * html attribute may also be in the callstack
8794cadd4f8SNickeau         */
8804cadd4f8SNickeau        foreach ($this->outputAttributes as $key => $value) {
88104fd306cSNickeau            $array[$key] = $value;
8824cadd4f8SNickeau        }
88304fd306cSNickeau        try {
88404fd306cSNickeau            $array["style"] = $this->getStyle();
88504fd306cSNickeau        } catch (ExceptionNotFound $e) {
88604fd306cSNickeau            // no style
88704fd306cSNickeau        }
88804fd306cSNickeau
88904fd306cSNickeau        if (isset($this->innerText)) {
89004fd306cSNickeau            $array[self::DOKUWIKI_TEXT_NODE_ATTRIBUTE] = $this->innerText;
89137748cd8SNickeau        }
89237748cd8SNickeau        return $array;
89337748cd8SNickeau    }
89437748cd8SNickeau
89537748cd8SNickeau    public
89637748cd8SNickeau    function getComponentAttributeValue($attributeName, $default = null)
89737748cd8SNickeau    {
89837748cd8SNickeau        $lowerAttribute = strtolower($attributeName);
89937748cd8SNickeau        $value = $default;
90037748cd8SNickeau        if ($this->hasComponentAttribute($lowerAttribute)) {
90137748cd8SNickeau            $value = $this->getValue($lowerAttribute);
90237748cd8SNickeau        }
90337748cd8SNickeau        return $value;
90437748cd8SNickeau    }
90537748cd8SNickeau
90637748cd8SNickeau    public
90782a60d03SNickeau    function addStyleDeclarationIfNotSet($property, $value)
90837748cd8SNickeau    {
90937748cd8SNickeau        ArrayUtility::addIfNotSet($this->styleDeclaration, $property, $value);
91037748cd8SNickeau    }
91137748cd8SNickeau
91204fd306cSNickeau    public
91304fd306cSNickeau    function setStyleDeclaration($property, $value): TagAttributes
91404fd306cSNickeau    {
91504fd306cSNickeau        $this->styleDeclaration[$property] = $value;
91604fd306cSNickeau        return $this;
91704fd306cSNickeau    }
91804fd306cSNickeau
91937748cd8SNickeau
92037748cd8SNickeau    public
9214cadd4f8SNickeau    function hasStyleDeclaration($styleDeclaration): bool
92237748cd8SNickeau    {
92337748cd8SNickeau        return isset($this->styleDeclaration[$styleDeclaration]);
92437748cd8SNickeau    }
92537748cd8SNickeau
92604fd306cSNickeau    public function getAndRemoveStyleDeclaration($styleDeclaration)
92737748cd8SNickeau    {
92837748cd8SNickeau        $styleValue = $this->styleDeclaration[$styleDeclaration];
92937748cd8SNickeau        unset($this->styleDeclaration[$styleDeclaration]);
93037748cd8SNickeau        return $styleValue;
93137748cd8SNickeau    }
93237748cd8SNickeau
93337748cd8SNickeau
93437748cd8SNickeau    public
9354cadd4f8SNickeau    function toHTMLAttributeString(): string
93637748cd8SNickeau    {
93737748cd8SNickeau
93837748cd8SNickeau        $tagAttributeString = "";
93937748cd8SNickeau
94037748cd8SNickeau        $htmlArray = $this->toHtmlArray();
94137748cd8SNickeau        foreach ($htmlArray as $name => $value) {
94237748cd8SNickeau
94337748cd8SNickeau            /**
94437748cd8SNickeau             * Empty value are authorized
94537748cd8SNickeau             * null are just not set
94637748cd8SNickeau             */
94737748cd8SNickeau            if (!is_null($value)) {
94837748cd8SNickeau
94937748cd8SNickeau                /**
95037748cd8SNickeau                 * Unset attribute should not be added
95137748cd8SNickeau                 */
95237748cd8SNickeau                if ($value === TagAttributes::UN_SET) {
95337748cd8SNickeau                    continue;
95437748cd8SNickeau                }
95537748cd8SNickeau
95637748cd8SNickeau                /**
95737748cd8SNickeau                 * The condition is important
95837748cd8SNickeau                 * because we may pass the javascript character `\n` in a `srcdoc` for javascript
95937748cd8SNickeau                 * and the {@link StringUtility::toString()} will transform it as `\\n`
96037748cd8SNickeau                 * making it unusable
96137748cd8SNickeau                 */
96237748cd8SNickeau                if (!is_string($value)) {
96337748cd8SNickeau                    $stringValue = StringUtility::toString($value);
96437748cd8SNickeau                } else {
96537748cd8SNickeau                    $stringValue = $value;
96637748cd8SNickeau                }
96737748cd8SNickeau
96837748cd8SNickeau
96937748cd8SNickeau                $tagAttributeString .= $name . '="' . $stringValue . '" ';
97037748cd8SNickeau            }
97137748cd8SNickeau
97237748cd8SNickeau        }
97337748cd8SNickeau        return trim($tagAttributeString);
97437748cd8SNickeau
97537748cd8SNickeau
97637748cd8SNickeau    }
97737748cd8SNickeau
97837748cd8SNickeau    public
9794cadd4f8SNickeau    function getComponentAttributes(): array
98037748cd8SNickeau    {
98137748cd8SNickeau        return $this->toCallStackArray();
98237748cd8SNickeau    }
98337748cd8SNickeau
98437748cd8SNickeau    public
98537748cd8SNickeau    function removeComponentAttributeIfPresent($attributeName)
98637748cd8SNickeau    {
98737748cd8SNickeau        if ($this->hasComponentAttribute($attributeName)) {
98837748cd8SNickeau            unset($this->componentAttributesCaseInsensitive[$attributeName]);
98937748cd8SNickeau        }
99037748cd8SNickeau
99137748cd8SNickeau    }
99237748cd8SNickeau
99337748cd8SNickeau    public
9944cadd4f8SNickeau    function toHtmlEnterTag($htmlTag): string
99537748cd8SNickeau    {
99637748cd8SNickeau
99704fd306cSNickeau        $enterTag = "<" . trim($htmlTag);
99837748cd8SNickeau        $attributeString = $this->toHTMLAttributeString();
99937748cd8SNickeau        if (!empty($attributeString)) {
100037748cd8SNickeau            $enterTag .= " " . $attributeString;
100137748cd8SNickeau        }
100237748cd8SNickeau        /**
100337748cd8SNickeau         * Is it an open tag ?
100437748cd8SNickeau         */
100537748cd8SNickeau        if (!$this->getValue(self::OPEN_TAG, false)) {
100637748cd8SNickeau
100737748cd8SNickeau            $enterTag .= ">";
100837748cd8SNickeau
100937748cd8SNickeau            /**
101037748cd8SNickeau             * Do we have html after the tag is closed
101137748cd8SNickeau             */
101237748cd8SNickeau            if (!empty($this->htmlAfterEnterTag)) {
101337748cd8SNickeau                $enterTag .= DOKU_LF . $this->htmlAfterEnterTag;
101437748cd8SNickeau            }
101537748cd8SNickeau
101637748cd8SNickeau        }
101737748cd8SNickeau
101837748cd8SNickeau
101937748cd8SNickeau        return $enterTag;
102037748cd8SNickeau
102137748cd8SNickeau    }
102237748cd8SNickeau
102337748cd8SNickeau    public
10244cadd4f8SNickeau    function toHtmlEmptyTag($htmlTag): string
10254cadd4f8SNickeau    {
10264cadd4f8SNickeau
10274cadd4f8SNickeau        $enterTag = "<" . $htmlTag;
10284cadd4f8SNickeau        $attributeString = $this->toHTMLAttributeString();
10294cadd4f8SNickeau        if (!empty($attributeString)) {
10304cadd4f8SNickeau            $enterTag .= " " . $attributeString;
10314cadd4f8SNickeau        }
10324cadd4f8SNickeau        return $enterTag . "/>";
10334cadd4f8SNickeau
10344cadd4f8SNickeau    }
10354cadd4f8SNickeau
103604fd306cSNickeau    public function getLogicalTag()
103737748cd8SNickeau    {
103837748cd8SNickeau        return $this->logicalTag;
103937748cd8SNickeau    }
104037748cd8SNickeau
104137748cd8SNickeau    public
10424cadd4f8SNickeau    function setLogicalTag($tag): TagAttributes
104337748cd8SNickeau    {
104437748cd8SNickeau        $this->logicalTag = $tag;
10454cadd4f8SNickeau        return $this;
104637748cd8SNickeau    }
104737748cd8SNickeau
104804fd306cSNickeau    /**
104904fd306cSNickeau     * @param $attribute
105004fd306cSNickeau     * @return mixed|null - the value deleted / null if it does not exist
105104fd306cSNickeau     */
105204fd306cSNickeau    public function removeComponentAttribute($attribute)
105337748cd8SNickeau    {
105437748cd8SNickeau        $lowerAtt = strtolower($attribute);
105537748cd8SNickeau        if (isset($this->componentAttributesCaseInsensitive[$lowerAtt])) {
105637748cd8SNickeau            $value = $this->componentAttributesCaseInsensitive[$lowerAtt];
105737748cd8SNickeau            unset($this->componentAttributesCaseInsensitive[$lowerAtt]);
105837748cd8SNickeau            return $value;
105937748cd8SNickeau        } else {
106037748cd8SNickeau            /**
106137748cd8SNickeau             * Edge case, this is the first boolean attribute
106237748cd8SNickeau             * and may has been categorized as the type
106337748cd8SNickeau             */
106437748cd8SNickeau            if (!$this->getType() == $lowerAtt) {
106504fd306cSNickeau                LogUtility::msg("Internal Error: The component attribute ($attribute) is not present. Use the ifPresent function, if you don't want this message");
106637748cd8SNickeau            }
106704fd306cSNickeau            return null;
106804fd306cSNickeau
106937748cd8SNickeau
107037748cd8SNickeau        }
107137748cd8SNickeau
107237748cd8SNickeau    }
107337748cd8SNickeau
107437748cd8SNickeau    /**
107537748cd8SNickeau     * @param $html - an html that should be closed and added after the enter tag
107637748cd8SNickeau     */
107737748cd8SNickeau    public
107837748cd8SNickeau    function addHtmlAfterEnterTag($html)
107937748cd8SNickeau    {
108037748cd8SNickeau        $this->htmlAfterEnterTag = $html . $this->htmlAfterEnterTag;
108137748cd8SNickeau    }
108237748cd8SNickeau
108337748cd8SNickeau    /**
108437748cd8SNickeau     * The mime of the HTTP request
108537748cd8SNickeau     * This is not the good place but yeah,
108637748cd8SNickeau     * this class has become the context class
108737748cd8SNickeau     *
108837748cd8SNickeau     * Mime make the difference for a svg to know if it's required as external resource (ie SVG)
108937748cd8SNickeau     * or as included in HTML page
109037748cd8SNickeau     * @param $mime
109137748cd8SNickeau     */
109237748cd8SNickeau    public
109337748cd8SNickeau    function setMime($mime)
109437748cd8SNickeau    {
109537748cd8SNickeau        $this->mime = $mime;
109637748cd8SNickeau    }
109737748cd8SNickeau
109837748cd8SNickeau    /**
109937748cd8SNickeau     * @return string - the mime of the request
110037748cd8SNickeau     */
110137748cd8SNickeau    public
110237748cd8SNickeau    function getMime()
110337748cd8SNickeau    {
110437748cd8SNickeau        return $this->mime;
110537748cd8SNickeau    }
110637748cd8SNickeau
110737748cd8SNickeau    public
110837748cd8SNickeau    function getType()
110937748cd8SNickeau    {
111037748cd8SNickeau        return $this->getValue(self::TYPE_KEY);
111137748cd8SNickeau    }
111237748cd8SNickeau
111337748cd8SNickeau    /**
111437748cd8SNickeau     * @param $attributeName
111537748cd8SNickeau     * @return ConditionalValue
111637748cd8SNickeau     */
111737748cd8SNickeau    public
111837748cd8SNickeau    function getConditionalValueAndRemove($attributeName)
111937748cd8SNickeau    {
112037748cd8SNickeau        $value = $this->getConditionalValueAndRemove($attributeName);
112137748cd8SNickeau        return new ConditionalValue($value);
112237748cd8SNickeau
112337748cd8SNickeau    }
112437748cd8SNickeau
112537748cd8SNickeau    /**
112637748cd8SNickeau     * @param $attributeName
112704fd306cSNickeau     * @param null $default
112804fd306cSNickeau     * @return null|string[] - an array of values
112904fd306cSNickeau     * @throws ExceptionBadArgument
113037748cd8SNickeau     */
113137748cd8SNickeau    public
113204fd306cSNickeau    function getValuesAndRemove($attributeName, $default = null): array
113337748cd8SNickeau    {
113437748cd8SNickeau
113504fd306cSNickeau        $trim = $this->getValues($attributeName, $default);
113604fd306cSNickeau        $this->removeAttributeIfPresent($attributeName);
113704fd306cSNickeau        return $trim;
113837748cd8SNickeau
113937748cd8SNickeau
114037748cd8SNickeau    }
114137748cd8SNickeau
114237748cd8SNickeau    public
11434cadd4f8SNickeau    function setType($type): TagAttributes
114437748cd8SNickeau    {
114537748cd8SNickeau        $this->setComponentAttributeValue(TagAttributes::TYPE_KEY, $type);
11464cadd4f8SNickeau        return $this;
114737748cd8SNickeau    }
114837748cd8SNickeau
114937748cd8SNickeau    /**
115037748cd8SNickeau     * Merging will add the values, no replace or overwrite
115137748cd8SNickeau     * @param $callStackArray
115237748cd8SNickeau     */
115337748cd8SNickeau    public
115437748cd8SNickeau    function mergeWithCallStackArray($callStackArray)
115537748cd8SNickeau    {
115637748cd8SNickeau        foreach ($callStackArray as $key => $value) {
11574cadd4f8SNickeau
115837748cd8SNickeau            if ($this->hasComponentAttribute($key)) {
11594cadd4f8SNickeau                $isMultipleAttributeValue = in_array($key, self::MULTIPLE_VALUES_ATTRIBUTES);
11604cadd4f8SNickeau                if ($isMultipleAttributeValue) {
116137748cd8SNickeau                    $this->addComponentAttributeValue($key, $value);
11624cadd4f8SNickeau                }
116337748cd8SNickeau            } else {
116437748cd8SNickeau                $this->setComponentAttributeValue($key, $value);
116537748cd8SNickeau            }
116637748cd8SNickeau        }
116737748cd8SNickeau
116837748cd8SNickeau    }
116937748cd8SNickeau
117037748cd8SNickeau    /**
117137748cd8SNickeau     * @param $string
11724cadd4f8SNickeau     * @return TagAttributes
117337748cd8SNickeau     */
117437748cd8SNickeau    public
11754cadd4f8SNickeau    function removeAttributeIfPresent($string): TagAttributes
117637748cd8SNickeau    {
117737748cd8SNickeau        $this->removeComponentAttributeIfPresent($string);
117804fd306cSNickeau        $this->removeOutputAttributeIfPresent($string);
11794cadd4f8SNickeau        return $this;
118037748cd8SNickeau
118137748cd8SNickeau    }
118237748cd8SNickeau
118304fd306cSNickeau    public function removeOutputAttributeIfPresent($string)
118437748cd8SNickeau    {
118537748cd8SNickeau        $lowerAtt = strtolower($string);
11864cadd4f8SNickeau        if (isset($this->outputAttributes[$lowerAtt])) {
11874cadd4f8SNickeau            unset($this->outputAttributes[$lowerAtt]);
118837748cd8SNickeau        }
118937748cd8SNickeau    }
119037748cd8SNickeau
119137748cd8SNickeau    public
119237748cd8SNickeau    function getValueAndRemoveIfPresent($attribute, $default = null)
119337748cd8SNickeau    {
119437748cd8SNickeau        $value = $this->getValue($attribute, $default);
119537748cd8SNickeau        $this->removeAttributeIfPresent($attribute);
119637748cd8SNickeau        return $value;
119737748cd8SNickeau    }
119837748cd8SNickeau
119937748cd8SNickeau    public
120037748cd8SNickeau    function generateAndSetId()
120137748cd8SNickeau    {
120237748cd8SNickeau        self::$counter += 1;
120337748cd8SNickeau        $id = self::$counter;
120437748cd8SNickeau        $logicalTag = $this->getLogicalTag();
120537748cd8SNickeau        if (!empty($logicalTag)) {
120637748cd8SNickeau            $id = $this->logicalTag . $id;
120737748cd8SNickeau        }
120837748cd8SNickeau        $this->setComponentAttributeValue("id", $id);
120937748cd8SNickeau        return $id;
121037748cd8SNickeau    }
121137748cd8SNickeau
121237748cd8SNickeau    /**
121337748cd8SNickeau     *
121437748cd8SNickeau     * @param $markiTag
121537748cd8SNickeau     * @return string - the marki tag made of logical attribute
121637748cd8SNickeau     * There is no processing to transform it to an HTML tag
121737748cd8SNickeau     */
121837748cd8SNickeau    public
121937748cd8SNickeau    function toMarkiEnterTag($markiTag)
122037748cd8SNickeau    {
122137748cd8SNickeau        $enterTag = "<" . $markiTag;
122237748cd8SNickeau
122337748cd8SNickeau        $attributeString = "";
122437748cd8SNickeau        foreach ($this->getComponentAttributes() as $key => $value) {
122537748cd8SNickeau            $attributeString .= "$key=\"$value\" ";
122637748cd8SNickeau        }
122737748cd8SNickeau        $attributeString = trim($attributeString);
122837748cd8SNickeau
122937748cd8SNickeau        if (!empty($attributeString)) {
123037748cd8SNickeau            $enterTag .= " " . $attributeString;
123137748cd8SNickeau        }
123237748cd8SNickeau        $enterTag .= ">";
123337748cd8SNickeau        return $enterTag;
123437748cd8SNickeau
123537748cd8SNickeau    }
123637748cd8SNickeau
123737748cd8SNickeau    /**
123837748cd8SNickeau     * @param string $key add an html attribute with the empty string
123937748cd8SNickeau     */
124037748cd8SNickeau    public
124104fd306cSNickeau    function addBooleanOutputAttributeValue(string $key): TagAttributes
124237748cd8SNickeau    {
124337748cd8SNickeau
124404fd306cSNickeau        $this->outputAttributes[$key] = null;
124537748cd8SNickeau        return $this;
124637748cd8SNickeau
124737748cd8SNickeau    }
124837748cd8SNickeau
124937748cd8SNickeau    public
125037748cd8SNickeau    function addEmptyComponentAttributeValue($attribute)
125137748cd8SNickeau    {
125237748cd8SNickeau        $this->componentAttributesCaseInsensitive[$attribute] = "";
125337748cd8SNickeau    }
125437748cd8SNickeau
125537748cd8SNickeau    /**
125637748cd8SNickeau     * @param $attribute
125737748cd8SNickeau     * @param null $default
125837748cd8SNickeau     * @return mixed
125937748cd8SNickeau     */
126037748cd8SNickeau    public
12614cadd4f8SNickeau    function getBooleanValueAndRemoveIfPresent($attribute, $default = null)
126237748cd8SNickeau    {
126304fd306cSNickeau        $value = $this->getBooleanValue($attribute, $default);
126404fd306cSNickeau        $this->removeAttributeIfPresent($attribute);
126504fd306cSNickeau        return $value;
126637748cd8SNickeau    }
126737748cd8SNickeau
126837748cd8SNickeau    public
126904fd306cSNickeau    function getBooleanValue($attribute, $default = null)
127004fd306cSNickeau    {
127104fd306cSNickeau        $value = $this->getValue($attribute);
127204fd306cSNickeau        if ($value !== null) {
127304fd306cSNickeau            return DataType::toBoolean($value);
127404fd306cSNickeau        }
127504fd306cSNickeau        return $default;
127604fd306cSNickeau    }
127704fd306cSNickeau
127804fd306cSNickeau    public function hasAttribute($attribute): bool
127937748cd8SNickeau    {
128037748cd8SNickeau        $hasAttribute = $this->hasComponentAttribute($attribute);
128137748cd8SNickeau        if ($hasAttribute === true) {
128237748cd8SNickeau            return true;
128337748cd8SNickeau        } else {
128437748cd8SNickeau            return $this->hasHtmlAttribute($attribute);
128537748cd8SNickeau        }
128637748cd8SNickeau    }
128737748cd8SNickeau
12884cadd4f8SNickeau    function hasHtmlAttribute($attribute): bool
128937748cd8SNickeau    {
12904cadd4f8SNickeau        return isset($this->outputAttributes[$attribute]);
129137748cd8SNickeau    }
129237748cd8SNickeau
129337748cd8SNickeau    /**
129404fd306cSNickeau     * @throws ExceptionNotFound
129504fd306cSNickeau     */
129604fd306cSNickeau    function getOutputAttribute($attribute)
129704fd306cSNickeau    {
1298*70bbd7f1Sgerardnico        $value = $this->outputAttributes[$attribute] ?? null;
129904fd306cSNickeau        if ($value === null) {
130004fd306cSNickeau            throw new ExceptionNotFound("No output attribute with the key ($attribute)");
130104fd306cSNickeau        }
130204fd306cSNickeau        return $value;
130304fd306cSNickeau    }
130404fd306cSNickeau
130504fd306cSNickeau    /**
13064cadd4f8SNickeau     * Encoding should happen always to the target format output.
13074cadd4f8SNickeau     * ie HTML
13084cadd4f8SNickeau     *
13094cadd4f8SNickeau     * If it's user or not data.
13104cadd4f8SNickeau     *
13114cadd4f8SNickeau     * Sanitizing is completely useless. We follow the same principal than SQL parameters
13124cadd4f8SNickeau     *
13134cadd4f8SNickeau     * We  follows the rule 2 to encode the unknown value
13144cadd4f8SNickeau     * We encode the component attribute to the target output (ie HTML)
13154cadd4f8SNickeau     *
131637748cd8SNickeau     * @param array $arrayToEscape
131737748cd8SNickeau     * @param null $subKey
13184cadd4f8SNickeau     *
13194cadd4f8SNickeau     *
13204cadd4f8SNickeau     *
13214cadd4f8SNickeau     *
13224cadd4f8SNickeau     * https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#rule-2-attribute-encode-before-inserting-untrusted-data-into-html-common-attributes
13234cadd4f8SNickeau     *
13244cadd4f8SNickeau     * @return array
132537748cd8SNickeau     */
132637748cd8SNickeau    private
13274cadd4f8SNickeau    function encodeToHtmlValue(array $arrayToEscape, $subKey = null): array
132837748cd8SNickeau    {
132937748cd8SNickeau
13304cadd4f8SNickeau        $returnedArray = [];
133137748cd8SNickeau        foreach ($arrayToEscape as $name => $value) {
133237748cd8SNickeau
133304fd306cSNickeau            $encodedName = Html::encode($name);
133437748cd8SNickeau
133537748cd8SNickeau            /**
133637748cd8SNickeau             * Boolean does not need to be encoded
133737748cd8SNickeau             */
133837748cd8SNickeau            if (is_bool($value)) {
133937748cd8SNickeau                if ($subKey == null) {
13404cadd4f8SNickeau                    $returnedArray[$encodedName] = $value;
134137748cd8SNickeau                } else {
13424cadd4f8SNickeau                    $returnedArray[$subKey][$encodedName] = $value;
134337748cd8SNickeau                }
134437748cd8SNickeau                continue;
134537748cd8SNickeau            }
134637748cd8SNickeau
13474cadd4f8SNickeau            /**
13484cadd4f8SNickeau             *
13494cadd4f8SNickeau             * Browser bug in a srcset
13504cadd4f8SNickeau             *
13514cadd4f8SNickeau             * In the HTML attribute srcset (not in the img src), if we set,
13524cadd4f8SNickeau             * ```
13534cadd4f8SNickeau             * http://nico.lan/_media/docs/metadata/metadata_manager.png?w=355&amp;h=176&amp;tseed=1636624852&amp;tok=af396a 355w
13544cadd4f8SNickeau             * ```
13554cadd4f8SNickeau             * the request is encoded ***by the browser**** one more time and the server gets:
13564cadd4f8SNickeau             *   * `&amp;&amp;h  =   176`
13574cadd4f8SNickeau             *   * php create therefore the property
13584cadd4f8SNickeau             *      * `&amp;h  =   176`
13594cadd4f8SNickeau             *      * and note `h = 176`
13604cadd4f8SNickeau             */
13614cadd4f8SNickeau            $encodeValue = true;
13624cadd4f8SNickeau            if ($encodedName === "srcset" && !PluginUtility::isTest()) {
13634cadd4f8SNickeau                /**
13644cadd4f8SNickeau                 * Our test xhtml processor does not support non ampersand encoded character
13654cadd4f8SNickeau                 */
13664cadd4f8SNickeau                $encodeValue = false;
13674cadd4f8SNickeau            }
13684cadd4f8SNickeau            if ($encodeValue) {
136904fd306cSNickeau                $value = Html::encode($value);
13704cadd4f8SNickeau            }
137137748cd8SNickeau            if ($subKey == null) {
13724cadd4f8SNickeau                $returnedArray[$encodedName] = $value;
137337748cd8SNickeau            } else {
13744cadd4f8SNickeau                $returnedArray[$subKey][$encodedName] = $value;
137537748cd8SNickeau            }
13764cadd4f8SNickeau
137737748cd8SNickeau        }
13784cadd4f8SNickeau        return $returnedArray;
13794cadd4f8SNickeau
138037748cd8SNickeau    }
138137748cd8SNickeau
13821fa8c418SNickeau    public function __toString()
13831fa8c418SNickeau    {
13841fa8c418SNickeau        return "TagAttributes";
13851fa8c418SNickeau    }
13861fa8c418SNickeau
13874cadd4f8SNickeau    /**
138804fd306cSNickeau     * @throws ExceptionCompile
13894cadd4f8SNickeau     */
13904cadd4f8SNickeau    public function getValueAsInteger(string $WIDTH_KEY, ?int $default = null): ?int
13914cadd4f8SNickeau    {
13924cadd4f8SNickeau        $value = $this->getValue($WIDTH_KEY, $default);
13934cadd4f8SNickeau        if ($value === null) {
13944cadd4f8SNickeau            return null;
13954cadd4f8SNickeau        }
13964cadd4f8SNickeau        return DataType::toInteger($value);
13974cadd4f8SNickeau    }
13984cadd4f8SNickeau
13994cadd4f8SNickeau    public function hasClass(string $string): bool
14004cadd4f8SNickeau    {
14014cadd4f8SNickeau        return strpos($this->getClass(), $string) !== false;
14024cadd4f8SNickeau    }
14034cadd4f8SNickeau
14044cadd4f8SNickeau    public function getDefaultStyleClassShouldBeAdded(): bool
14054cadd4f8SNickeau    {
14064cadd4f8SNickeau        return $this->defaultStyleClassShouldBeAdded;
14074cadd4f8SNickeau    }
14084cadd4f8SNickeau
14094cadd4f8SNickeau    public function setDefaultStyleClassShouldBeAdded(bool $bool): TagAttributes
14104cadd4f8SNickeau    {
14114cadd4f8SNickeau        $this->defaultStyleClassShouldBeAdded = $bool;
14124cadd4f8SNickeau        return $this;
14134cadd4f8SNickeau    }
14144cadd4f8SNickeau
141504fd306cSNickeau    public function getDefaultGeneratedId()
141604fd306cSNickeau    {
141704fd306cSNickeau        return $this->getValue(TagAttributes::GENERATED_ID_KEY);
141804fd306cSNickeau    }
141904fd306cSNickeau
142004fd306cSNickeau    public function setKnownTypes(?array $knownTypes): TagAttributes
142104fd306cSNickeau    {
142204fd306cSNickeau        $this->knownTypes = $knownTypes;
142304fd306cSNickeau        return $this;
142404fd306cSNickeau    }
142504fd306cSNickeau
142604fd306cSNickeau    public function removeType(): TagAttributes
142704fd306cSNickeau    {
142804fd306cSNickeau        $this->removeAttributeIfPresent(self::TYPE_KEY);
142904fd306cSNickeau        return $this;
143004fd306cSNickeau    }
143104fd306cSNickeau
143204fd306cSNickeau    /**
143304fd306cSNickeau     * @param $attributeName
143404fd306cSNickeau     * @param array|null $default
143504fd306cSNickeau     * @return string[]
143604fd306cSNickeau     * @throws ExceptionBadArgument
143704fd306cSNickeau     */
143804fd306cSNickeau    public function getValues($attributeName, ?array $default = null): ?array
143904fd306cSNickeau    {
144004fd306cSNickeau        /**
144104fd306cSNickeau         * Replace all suite of space that have more than 2 characters
144204fd306cSNickeau         */
144304fd306cSNickeau        $value = $this->getValue($attributeName);
144404fd306cSNickeau        if ($value === null) {
144504fd306cSNickeau            return $default;
144604fd306cSNickeau        }
144704fd306cSNickeau        if (!is_string($value)) {
144804fd306cSNickeau            throw new ExceptionBadArgument("The attribute ($attributeName) does not contain a string, we can't return multiple values");
144904fd306cSNickeau        }
145004fd306cSNickeau        $value = preg_replace("/\s{2,}/", " ", trim($value));
145104fd306cSNickeau        return explode(" ", $value);
145204fd306cSNickeau
145304fd306cSNickeau    }
145404fd306cSNickeau
145504fd306cSNickeau    public function getComponentAttributeValueAndRemoveIfPresent(string $attribute, $default = null)
145604fd306cSNickeau    {
145704fd306cSNickeau        $value = $this->getComponentAttributeValue($attribute, $default);
145804fd306cSNickeau        $this->removeComponentAttributeIfPresent($attribute);
145904fd306cSNickeau        return $value;
146004fd306cSNickeau    }
146104fd306cSNickeau
146204fd306cSNickeau    public function toUrl(): Url
146304fd306cSNickeau    {
146404fd306cSNickeau        $url = Url::createEmpty();
146504fd306cSNickeau        foreach ($this->componentAttributesCaseInsensitive as $key => $value) {
146604fd306cSNickeau            $url->addQueryParameter($key, $value);
146704fd306cSNickeau        }
146804fd306cSNickeau        return $url;
146904fd306cSNickeau    }
147004fd306cSNickeau
147104fd306cSNickeau    public function hasComponentAttributeAndRemove(string $key): bool
147204fd306cSNickeau    {
147304fd306cSNickeau        $hasAttribute = $this->hasComponentAttribute($key);
147404fd306cSNickeau        if ($hasAttribute) {
147504fd306cSNickeau            $this->removeComponentAttribute($key);
147604fd306cSNickeau        }
147704fd306cSNickeau        return $hasAttribute;
147804fd306cSNickeau    }
147904fd306cSNickeau
148004fd306cSNickeau    /**
148104fd306cSNickeau     * @param string $text - the text node content
148204fd306cSNickeau     * @return $this
148304fd306cSNickeau     */
148404fd306cSNickeau    public function setInnerText(string $text): TagAttributes
148504fd306cSNickeau    {
148604fd306cSNickeau        $this->innerText = $text;
148704fd306cSNickeau        return $this;
148804fd306cSNickeau    }
148904fd306cSNickeau
149004fd306cSNickeau    /**
149104fd306cSNickeau     * @throws ExceptionNotFound
149204fd306cSNickeau     */
149304fd306cSNickeau    public function getInnerText(): string
149404fd306cSNickeau    {
149504fd306cSNickeau        if (!isset($this->innerText)) {
149604fd306cSNickeau            throw new ExceptionNotFound("No inner text is set");
149704fd306cSNickeau        }
149804fd306cSNickeau        return $this->innerText;
149904fd306cSNickeau    }
150004fd306cSNickeau
150104fd306cSNickeau
150204fd306cSNickeau    public function setId(string $id): TagAttributes
150304fd306cSNickeau    {
150404fd306cSNickeau        return $this->setComponentAttributeValue("id", $id);
150504fd306cSNickeau    }
150604fd306cSNickeau
150704fd306cSNickeau    /**
150804fd306cSNickeau     * @throws ExceptionNotFound
150904fd306cSNickeau     */
151004fd306cSNickeau    public function getId()
151104fd306cSNickeau    {
151204fd306cSNickeau        $id = $this->getValue(TagAttributes::ID_KEY);
151304fd306cSNickeau        if ($id === null) {
151404fd306cSNickeau            throw new ExceptionNotFound("no id");
151504fd306cSNickeau        }
151604fd306cSNickeau        return $id;
151704fd306cSNickeau
151804fd306cSNickeau    }
151904fd306cSNickeau
152037748cd8SNickeau
152137748cd8SNickeau}
1522