1<?php
2
3declare(strict_types=1);
4
5/*
6 * This file is part of the league/commonmark package.
7 *
8 * (c) Colin O'Dell <colinodell@gmail.com>
9 *
10 * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
11 *  - (c) John MacFarlane
12 *
13 * For the full copyright and license information, please view the LICENSE
14 * file that was distributed with this source code.
15 */
16
17namespace League\CommonMark\Util;
18
19final class HtmlElement implements \Stringable
20{
21    /** @psalm-readonly */
22    private string $tagName;
23
24    /** @var array<string, string|bool> */
25    private array $attributes = [];
26
27    /** @var \Stringable|\Stringable[]|string */
28    private $contents;
29
30    /** @psalm-readonly */
31    private bool $selfClosing;
32
33    /**
34     * @param string                                $tagName     Name of the HTML tag
35     * @param array<string, string|string[]|bool>   $attributes  Array of attributes (values should be unescaped)
36     * @param \Stringable|\Stringable[]|string|null $contents    Inner contents, pre-escaped if needed
37     * @param bool                                  $selfClosing Whether the tag is self-closing
38     */
39    public function __construct(string $tagName, array $attributes = [], $contents = '', bool $selfClosing = false)
40    {
41        $this->tagName     = $tagName;
42        $this->selfClosing = $selfClosing;
43
44        foreach ($attributes as $name => $value) {
45            $this->setAttribute($name, $value);
46        }
47
48        $this->setContents($contents ?? '');
49    }
50
51    /** @psalm-immutable */
52    public function getTagName(): string
53    {
54        return $this->tagName;
55    }
56
57    /**
58     * @return array<string, string|bool>
59     *
60     * @psalm-immutable
61     */
62    public function getAllAttributes(): array
63    {
64        return $this->attributes;
65    }
66
67    /**
68     * @return string|bool|null
69     *
70     * @psalm-immutable
71     */
72    public function getAttribute(string $key)
73    {
74        return $this->attributes[$key] ?? null;
75    }
76
77    /**
78     * @param string|string[]|bool $value
79     */
80    public function setAttribute(string $key, $value = true): self
81    {
82        if (\is_array($value)) {
83            $this->attributes[$key] = \implode(' ', \array_unique($value));
84        } else {
85            $this->attributes[$key] = $value;
86        }
87
88        return $this;
89    }
90
91    /**
92     * @return \Stringable|\Stringable[]|string
93     *
94     * @psalm-immutable
95     */
96    public function getContents(bool $asString = true)
97    {
98        if (! $asString) {
99            return $this->contents;
100        }
101
102        return $this->getContentsAsString();
103    }
104
105    /**
106     * Sets the inner contents of the tag (must be pre-escaped if needed)
107     *
108     * @param \Stringable|\Stringable[]|string $contents
109     *
110     * @return $this
111     */
112    public function setContents($contents): self
113    {
114        $this->contents = $contents ?? ''; // @phpstan-ignore-line
115
116        return $this;
117    }
118
119    /** @psalm-immutable */
120    public function __toString(): string
121    {
122        $result = '<' . $this->tagName;
123
124        foreach ($this->attributes as $key => $value) {
125            if ($value === true) {
126                $result .= ' ' . $key;
127            } elseif ($value === false) {
128                continue;
129            } else {
130                $result .= ' ' . $key . '="' . Xml::escape($value) . '"';
131            }
132        }
133
134        if ($this->contents !== '') {
135            $result .= '>' . $this->getContentsAsString() . '</' . $this->tagName . '>';
136        } elseif ($this->selfClosing && $this->tagName === 'input') {
137            $result .= '>';
138        } elseif ($this->selfClosing) {
139            $result .= ' />';
140        } else {
141            $result .= '></' . $this->tagName . '>';
142        }
143
144        return $result;
145    }
146
147    /** @psalm-immutable */
148    private function getContentsAsString(): string
149    {
150        if (\is_string($this->contents)) {
151            return $this->contents;
152        }
153
154        if (\is_array($this->contents)) {
155            return \implode('', $this->contents);
156        }
157
158        return (string) $this->contents;
159    }
160}
161