Passed
Push — main ( b205bb...1d3d49 )
by Mark
12:04
created

HtmlElement::getAttributes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 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 League\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, bool|string> */
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, bool|string>            $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 69
    public function __construct(string $tagName, array $attributes = [], $contents = '', bool $selfClosing = false)
48
    {
49 69
        $this->tagName     = $tagName;
50 69
        $this->selfClosing = $selfClosing;
51
52
        /**
53
         * @var bool|string $value
54
         */
55 69
        foreach ($attributes as $name => $value) {
56 48
            $this->setAttribute($name, $value);
57
        }
58
59 69
        $this->setContents($contents ?? '');
60 69
    }
61
62
    /**
63
     * Prepares a string for use as a CSS identifier (element, class, or ID name).
64
     *
65
     * Link below shows the syntax for valid CSS identifiers (including element
66
     * names, classes, and IDs in selectors).
67
     *
68
     * @see http://www.w3.org/TR/CSS21/syndata.html#characters
69
     *
70
     * @param string   $identifier
71
     *   The identifier to clean.
72
     * @param string[] $filter
73
     *   An array of string replacements to use on the identifier.
74
     *
75
     * @return string
76
     *   The cleaned identifier.
77
     *
78
     * Note: shamelessly copied from
79
     * https://github.com/drupal/core-utility/blob/6807795c25836ccdb3f50d4396c4427705b7b6ad/Html.php
80
     */
81 36
    public static function cleanCssIdentifier(string $identifier, array $filter = self::CSS_IDENTIFIER_FILTERS): string
82
    {
83
        // We could also use strtr() here but its much slower than str_replace(). In
84
        // order to keep '__' to stay '__' we first replace it with a different
85
        // placeholder after checking that it is not defined as a filter.
86 36
        $doubleUnderscoreReplacements = 0;
87 36
        if (! isset($filter['__'])) {
88 36
            $identifier = \str_replace('__', '##', $identifier, $doubleUnderscoreReplacements);
89
        }
90
91 36
        $identifier = \str_replace(\array_keys($filter), \array_values($filter), $identifier);
92
        // Replace temporary placeholder '##' with '__' only if the original
93
        // $identifier contained '__'.
94 36
        if ($doubleUnderscoreReplacements > 0) {
95 3
            $identifier = \str_replace('##', '__', $identifier);
96
        }
97
98
        // Valid characters in a CSS identifier are:
99
        // - the hyphen (U+002D)
100
        // - a-z (U+0030 - U+0039)
101
        // - A-Z (U+0041 - U+005A)
102
        // - the underscore (U+005F)
103
        // - 0-9 (U+0061 - U+007A)
104
        // - ISO 10646 characters U+00A1 and higher
105
        // We strip out any character not in the above list.
106 36
        $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);
107
108
        // Identifiers cannot start with a digit, two hyphens, or a hyphen followed by a digit.
109 36
        $identifier = (string) \preg_replace(['/^[0-9]/', '/^(-[0-9])|^(--)/'], ['_', '__'], $identifier);
110
111 36
        return $identifier;
112
    }
113
114 36
    public function addClass(string ...$classes): self
115
    {
116
        // Merge classes into existing classes.
117 36
        $existing = \explode(' ', (string) ($this->attributes['class'] ?? ''));
118 36
        $classes  = \array_merge($existing, $classes);
119
120
        // Split any strings that may have multiple classes in them.
121
        $classes = \array_map(static function (string $class) {
122 36
            return \explode(' ', $class);
123 36
        }, $classes);
124
125
        // Flatten classes back into a single level array.
126 36
        $classes = \array_reduce(
127 36
            $classes,
128
            /**
129
             * @param string[] $a
130
             * @param string[] $v
131
             *
132
             * @return string[]
133
             */
134
            static function (array $a, array $v): array {
135 36
                return \array_merge($a, $v);
136
            },
137 36
            []
138
        );
139
140
        // Filter out empty items and ensure classes are unique.
141 36
        $classes = \array_filter(\array_unique($classes));
142
143
        // Remove trailing spaces and normalize the class.
144
        $classes = \array_map(static function (string $class): string {
145 36
            return self::cleanCssIdentifier(\trim($class));
146 36
        }, $classes);
147
148
        // Convert the array of classes back into a string.
149 36
        $this->attributes['class'] = \trim(\implode(' ', $classes));
150
151 36
        return $this;
152
    }
153
154 9
    public function getTagName(): string
155
    {
156 9
        return $this->tagName;
157
    }
158
159
    /**
160
     * @return array<string, bool|string>
161
     */
162 21
    public function getAttributes(): array
163
    {
164 21
        return $this->attributes;
165
    }
166
167 36
    public function getAttributesAsString(): string
168
    {
169 36
        $attributes = '';
170 36
        foreach ($this->attributes as $key => $value) {
171 27
            if ($value === true) {
172 3
                $attributes .= ' ' . $key;
173 27
            } elseif ($value === false) {
174 3
                continue;
175
            } else {
176 27
                $attributes .= ' ' . $key . '="' . Xml::escape($value) . '"';
177
            }
178
        }
179
180 36
        return $attributes;
181
    }
182
183
    /**
184
     * @return bool|string|null
185
     */
186 24
    public function getAttribute(string $key)
187
    {
188 24
        if (! isset($this->attributes[$key])) {
189 3
            return null;
190
        }
191
192 24
        return $this->attributes[$key];
193
    }
194
195
    /**
196
     * @param string|string[]|bool $value
197
     */
198 48
    public function setAttribute(string $key, $value): self
199
    {
200 48
        if ($key === 'class') {
201 36
            $this->attributes['class'] = '';
202 36
            if (! \is_array($value)) {
203 36
                $value = [$value];
204
            }
205
206
            /** @var string[] $value */
207 36
            return $this->addClass(...$value);
208
        }
209
210 39
        if (\is_array($value)) {
211 3
            $this->attributes[$key] = \implode(' ', \array_unique($value));
212
        } else {
213 39
            $this->attributes[$key] = $value;
214
        }
215
216 39
        return $this;
217
    }
218
219
    /**
220
     * @return HtmlElement|HtmlElement[]|string
221
     */
222 21
    public function getContents(bool $asString = true)
223
    {
224 21
        if (! $asString) {
225 6
            return $this->contents;
226
        }
227
228 15
        return $this->getContentsAsString();
229
    }
230
231
    /**
232
     * Sets the inner contents of the tag (must be pre-escaped if needed)
233
     *
234
     * @param HtmlElement|HtmlElement[]|string|null $contents
235
     */
236 69
    public function setContents($contents): self
237
    {
238 69
        $this->contents = $contents ?? '';
239
240 69
        return $this;
241
    }
242
243 36
    public function __toString(): string
244
    {
245 36
        $result = '<' . $this->tagName . $this->getAttributesAsString();
246
247 36
        if ($this->contents !== '') {
248 6
            $result .= '>' . $this->getContentsAsString() . '</' . $this->tagName . '>';
249 33
        } elseif ($this->selfClosing && $this->tagName === 'input') {
250 3
            $result .= '>';
251 30
        } elseif ($this->selfClosing) {
252 24
            $result .= ' />';
253
        } else {
254 9
            $result .= '></' . $this->tagName . '>';
255
        }
256
257 36
        return $result;
258
    }
259
260 18
    private function getContentsAsString(): string
261
    {
262 18
        if (\is_string($this->contents)) {
263 15
            return $this->contents;
264
        }
265
266 3
        if (\is_array($this->contents)) {
267 3
            return \implode('', $this->contents);
268
        }
269
270 3
        return (string) $this->contents;
271
    }
272
}
273