Test Setup Failed
Pull Request — latest (#3)
by Mark
31:31
created

HtmlElement::__toString()   B

Complexity

Conditions 8
Paths 16

Size

Total Lines 25
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 17
c 1
b 0
f 1
dl 0
loc 25
rs 8.4444
cc 8
nc 16
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file was originally part of the league/commonmark package.
7
 *
8
 * (c) Colin O'Dell <[email protected]>
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
17
namespace UnicornFail\Emoji\Util;
18
19
class HtmlElement implements \Stringable
20
{
21
    public const CSS_IDENTIFIER_FILTERS = [
22
        ' ' => '-',
23
        '_' => '-',
24
        '/' => '-',
25
        '[' => '-',
26
        ']' => '',
27
    ];
28
29
    /** @var string */
30
    protected $tagName;
31
32
    /** @var array<string, string|bool> */
33
    protected $attributes = [];
34
35
    /** @var HtmlElement|HtmlElement[]|string */
36
    protected $contents;
37
38
    /** @var bool */
39
    protected $selfClosing = false;
40
41
    /**
42
     * @param string                                $tagName     Name of the HTML tag
43
     * @param array<string, mixed>                  $attributes  Array of attributes (values should be unescaped)
44
     * @param HtmlElement|HtmlElement[]|string|null $contents    Inner contents, pre-escaped if needed
45
     * @param bool                                  $selfClosing Whether the tag is self-closing
46
     */
47
    public function __construct(string $tagName, array $attributes = [], $contents = '', bool $selfClosing = false)
48
    {
49
        $this->tagName     = $tagName;
50
        $this->selfClosing = $selfClosing;
51
52
        /**
53
         * @var string $name
54
         * @var string|bool $value
55
         */
56
        foreach ($attributes as $name => $value) {
57
            $this->setAttribute($name, $value);
58
        }
59
60
        $this->setContents($contents ?? '');
61
    }
62
63
    /**
64
     * Prepares a string for use as a CSS identifier (element, class, or ID name).
65
     *
66
     * Link below shows the syntax for valid CSS identifiers (including element
67
     * names, classes, and IDs in selectors).
68
     *
69
     * @see http://www.w3.org/TR/CSS21/syndata.html#characters
70
     *
71
     * @param string   $identifier
72
     *   The identifier to clean.
73
     * @param string[] $filter
74
     *   An array of string replacements to use on the identifier.
75
     *
76
     * @return string
77
     *   The cleaned identifier.
78
     *
79
     * Note: shamelessly copied from
80
     * https://github.com/drupal/core-utility/blob/6807795c25836ccdb3f50d4396c4427705b7b6ad/Html.php
81
     */
82
    public static function cleanCssIdentifier(string $identifier, array $filter = self::CSS_IDENTIFIER_FILTERS): string
83
    {
84
        // We could also use strtr() here but its much slower than str_replace(). In
85
        // order to keep '__' to stay '__' we first replace it with a different
86
        // placeholder after checking that it is not defined as a filter.
87
        $doubleUnderscoreReplacements = 0;
88
        if (! isset($filter['__'])) {
89
            $identifier = \str_replace('__', '##', $identifier, $doubleUnderscoreReplacements);
90
        }
91
92
        $identifier = \str_replace(\array_keys($filter), \array_values($filter), $identifier);
93
        // Replace temporary placeholder '##' with '__' only if the original
94
        // $identifier contained '__'.
95
        if ($doubleUnderscoreReplacements > 0) {
96
            $identifier = \str_replace('##', '__', $identifier);
97
        }
98
99
        // Valid characters in a CSS identifier are:
100
        // - the hyphen (U+002D)
101
        // - a-z (U+0030 - U+0039)
102
        // - A-Z (U+0041 - U+005A)
103
        // - the underscore (U+005F)
104
        // - 0-9 (U+0061 - U+007A)
105
        // - ISO 10646 characters U+00A1 and higher
106
        // We strip out any character not in the above list.
107
        $identifier = (string) \preg_replace('/[^\x{002D}\x{0030}-\x{0039}\x{0041}-\x{005A}\x{005F}\x{0061}-\x{007A}\x{00A1}-\x{FFFF}]/u', '', $identifier);
108
109
        // Identifiers cannot start with a digit, two hyphens, or a hyphen followed by a digit.
110
        $identifier = (string) \preg_replace(['/^[0-9]/', '/^(-[0-9])|^(--)/'], ['_', '__'], $identifier);
111
112
        return $identifier;
113
    }
114
115
    public function addClass(string ...$classes): self
116
    {
117
        $classes = \array_merge([(string) ($this->attributes['class'] ?? '')], $classes);
118
119
        /** @var string[] $classes */
120
        $classes = \array_filter(\iterator_to_array(new \RecursiveIteratorIterator(new \RecursiveArrayIterator(\array_map(static function (string $class) {
121
            return \explode(' ', $class);
122
        }, $classes)))));
123
124
        $classes = \array_map('\UnicornFail\Emoji\Util\HtmlElement::cleanCssIdentifier', \array_map('trim', $classes));
125
126
        $classes = \trim(\implode(' ', $classes));
127
128
        $this->attributes['class'] = $classes;
129
130
        return $this;
131
    }
132
133
    public function getTagName(): string
134
    {
135
        return $this->tagName;
136
    }
137
138
    /**
139
     * @return array<string, string|bool>
140
     */
141
    public function getAllAttributes(): array
142
    {
143
        return $this->attributes;
144
    }
145
146
    /**
147
     * @return string|bool|null
148
     */
149
    public function getAttribute(string $key)
150
    {
151
        if (! isset($this->attributes[$key])) {
152
            return null;
153
        }
154
155
        return $this->attributes[$key];
156
    }
157
158
    /**
159
     * @param string|string[]|bool $value
160
     */
161
    public function setAttribute(string $key, $value): self
162
    {
163
        if ($key === 'class') {
164
            if (! \is_array($value)) {
165
                $value = [$value];
166
            }
167
168
            /** @var string[] $value */
169
            return $this->addClass(...$value);
170
        }
171
172
        if (\is_array($value)) {
173
            $this->attributes[$key] = \implode(' ', \array_unique($value));
174
        } else {
175
            $this->attributes[$key] = $value;
176
        }
177
178
        return $this;
179
    }
180
181
    /**
182
     * @return HtmlElement|HtmlElement[]|string
183
     */
184
    public function getContents(bool $asString = true)
185
    {
186
        if (! $asString) {
187
            return $this->contents;
188
        }
189
190
        return $this->getContentsAsString();
191
    }
192
193
    /**
194
     * Sets the inner contents of the tag (must be pre-escaped if needed)
195
     *
196
     * @param HtmlElement|HtmlElement[]|string $contents
197
     *
198
     * @return $this
199
     */
200
    public function setContents($contents): self
201
    {
202
        $this->contents = $contents ?: '';
203
204
        return $this;
205
    }
206
207
    public function __toString(): string
208
    {
209
        $result = '<' . $this->tagName;
210
211
        foreach ($this->attributes as $key => $value) {
212
            if ($value === true) {
213
                $result .= ' ' . $key;
214
            } elseif ($value === false) {
215
                continue;
216
            } else {
217
                $result .= ' ' . $key . '="' . Xml::escape($value) . '"';
218
            }
219
        }
220
221
        if ($this->contents !== '') {
222
            $result .= '>' . $this->getContentsAsString() . '</' . $this->tagName . '>';
223
        } elseif ($this->selfClosing && $this->tagName === 'input') {
224
            $result .= '>';
225
        } elseif ($this->selfClosing) {
226
            $result .= ' />';
227
        } else {
228
            $result .= '></' . $this->tagName . '>';
229
        }
230
231
        return $result;
232
    }
233
234
    private function getContentsAsString(): string
235
    {
236
        if (\is_string($this->contents)) {
237
            return $this->contents;
238
        }
239
240
        if (\is_array($this->contents)) {
241
            return \implode('', $this->contents);
242
        }
243
244
        return (string) $this->contents;
245
    }
246
}
247