• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dg / texy / 21345922688

26 Jan 2026 04:05AM UTC coverage: 92.426% (+0.04%) from 92.382%
21345922688

push

github

dg
HtmlElement: removed toHtml() & toText()

18 of 19 new or added lines in 5 files covered. (94.74%)

28 existing lines in 7 files now uncovered.

2404 of 2601 relevant lines covered (92.43%)

0.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.48
/src/Texy/HtmlElement.php
1
<?php
2

3
/**
4
 * This file is part of the Texy! (https://texy.nette.org)
5
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6
 */
7

8
declare(strict_types=1);
9

10
namespace Texy;
11

12
use function array_splice, count, http_build_query, implode, is_array, is_object, is_string, str_replace, str_starts_with;
13

14

15
/**
16
 * HTML helper.
17
 *
18
 * usage:
19
 * $anchor = (new HtmlElement('a'))->href($link)->setText('Texy');
20
 * $el->class = 'myclass';
21
 *
22
 * echo $el->startTag(), $el->endTag();
23
 *
24
 * @implements \ArrayAccess<int, HtmlElement|string>
25
 * @implements \IteratorAggregate<int, HtmlElement|string>
26
 */
27
class HtmlElement implements \ArrayAccess, /* Countable, */ \IteratorAggregate
28
{
29
        public const InnerText = '%TEXT';
30
        public const InnerTransparent = '%TRANS';
31

32
        /** @var array<string, string|int|bool|array<string|int|bool>|null>  element's attributes */
33
        public array $attrs = [];
34

35
        /** @var array<string, 1>  void elements */
36
        public static array $emptyElements = [
37
                'area' => 1, 'base' => 1, 'br' => 1, 'col' => 1, 'embed' => 1, 'hr' => 1, 'img' => 1, 'input' => 1,
38
                'link' => 1, 'meta' => 1, 'param' => 1, 'source' => 1, 'track' => 1, 'wbr' => 1,
39
        ];
40

41
        /** @var array<string, int>  phrasing elements; replaced elements + br have value 1 */
42
        public static array $inlineElements = [
43
                'a' => 0, 'abbr' => 0, 'area' => 0, 'audio' => 0, 'b' => 0, 'bdi' => 0, 'bdo' => 0, 'br' => 1, 'button' => 1, 'canvas' => 1,
44
                'cite' => 0, 'code' => 0, 'data' => 0, 'datalist' => 0, 'del' => 0, 'dfn' => 0, 'em' => 0, 'embed' => 1, 'i' => 0, 'iframe' => 1,
45
                'img' => 1, 'input' => 1, 'ins' => 0, 'kbd' => 0, 'label' => 0, 'link' => 0, 'map' => 0, 'mark' => 0, 'math' => 1, 'meta' => 0,
46
                'meter' => 1, 'noscript' => 1, 'object' => 1, 'output' => 1, 'picture' => 1, 'progress' => 1, 'q' => 0, 'ruby' => 0, 's' => 0,
47
                'samp' => 0, 'script' => 1, 'select' => 1, 'slot' => 0, 'small' => 0, 'span' => 0, 'strong' => 0, 'sub' => 0, 'sup' => 0,
48
                'svg' => 1, 'template' => 0, 'textarea' => 1, 'time' => 0, 'u' => 0, 'var' => 0, 'video' => 1, 'wbr' => 0,
49
        ];
50

51
        /** @var array<string, 1>  elements with optional end tag in HTML */
52
        public static array $optionalEnds = [
53
                'body' => 1, 'head' => 1, 'html' => 1, 'colgroup' => 1, 'dd' => 1, 'dt' => 1, 'li' => 1,
54
                'option' => 1, 'p' => 1, 'tbody' => 1, 'td' => 1, 'tfoot' => 1, 'th' => 1, 'thead' => 1, 'tr' => 1,
55
        ];
56

57
        /** @var array<string, list<string>> */
58
        public static array $prohibits = [
59
                'a' => ['a', 'button'],
60
                'button' => ['a', 'button'],
61
                'details' => ['a', 'button'],
62
                'embed' => ['a', 'button'],
63
                'iframe' => ['a', 'button'],
64
                'input' => ['a', 'button'],
65
                'label' => ['a', 'button', 'label'],
66
                'select' => ['a', 'button'],
67
                'textarea' => ['a', 'button'],
68
                'header' => ['address', 'dt', 'footer', 'header', 'th'],
69
                'footer' => ['address', 'dt', 'footer', 'header', 'th'],
70
                'address' => ['address'],
71
                'h1' => ['address', 'dt', 'th'],
72
                'h2' => ['address', 'dt', 'th'],
73
                'h3' => ['address', 'dt', 'th'],
74
                'h4' => ['address', 'dt', 'th'],
75
                'h5' => ['address', 'dt', 'th'],
76
                'h6' => ['address', 'dt', 'th'],
77
                'hgroup' => ['address', 'dt', 'th'],
78
                'article' => ['address', 'dt', 'th'],
79
                'aside' => ['address', 'dt', 'th'],
80
                'nav' => ['address', 'dt', 'th'],
81
                'section' => ['address', 'dt', 'th'],
82
                'table' => ['caption'],
83
                'dfn' => ['dfn'],
84
                'form' => ['form'],
85
                'meter' => ['meter'],
86
                'progress' => ['progress'],
87
        ];
88

89
        /** @var list<HtmlElement|string> nodes */
90
        protected array $children = [];
91
        private ?string $name;
92
        private bool $isEmpty;
93

94

95
        /**
96
         * @param  array<string, mixed>|string|null  $attrs  element's attributes (or textual content)
97
         */
98
        public function __construct(?string $name = null, array|string|null $attrs = null)
1✔
99
        {
100
                $this->setName($name);
1✔
101
                if (is_array($attrs)) {
1✔
102
                        $this->attrs = $attrs;
1✔
103
                } elseif ($attrs !== null) {
1✔
104
                        $this->setText($attrs);
1✔
105
                }
106
        }
1✔
107

108

109
        /** @param  array<string, mixed>|string|null  $attrs */
110
        public static function el(?string $name = null, array|string|null $attrs = null): self
111
        {
112
                return new self($name, $attrs);
×
113
        }
114

115

116
        /**
117
         * Changes element's name.
118
         */
119
        final public function setName(?string $name, ?bool $empty = null): static
1✔
120
        {
121
                $this->name = $name;
1✔
122
                $this->isEmpty = $empty === null
1✔
123
                        ? isset(self::$emptyElements[$name ?? ''])
1✔
124
                        : (bool) $empty;
×
125
                return $this;
1✔
126
        }
127

128

129
        /**
130
         * Returns element's name.
131
         */
132
        final public function getName(): ?string
133
        {
134
                return $this->name;
1✔
135
        }
136

137

138
        /**
139
         * Is element empty?
140
         */
141
        final public function isEmpty(): bool
142
        {
143
                return $this->isEmpty;
1✔
144
        }
145

146

147
        /**
148
         * Overloaded setter for element's attribute.
149
         */
150
        final public function __set(string $name, mixed $value): void
151
        {
152
                $this->attrs[$name] = $value;
×
153
        }
154

155

156
        /**
157
         * Overloaded getter for element's attribute.
158
         */
159
        final public function &__get(string $name): mixed
160
        {
161
                return $this->attrs[$name];
×
162
        }
163

164

165
        /**
166
         * Sets element's attribute.
167
         * @param  string|int|bool|string[]|null  $value
168
         */
169
        final public function setAttribute(string $name, string|int|bool|array|null $value): static
170
        {
171
                $this->attrs[$name] = $value;
×
172
                return $this;
×
173
        }
174

175

176
        /**
177
         * Returns element's attribute.
178
         * @return string|int|bool|array<string|int|bool>|null
179
         */
180
        final public function getAttribute(string $name): string|int|bool|array|null
181
        {
182
                return $this->attrs[$name] ?? null;
×
183
        }
184

185

186
        /**
187
         * Special setter for element's attribute.
188
         * @param  array<string, mixed>|null  $query
189
         */
190
        final public function href(string $path, ?array $query = null): static
191
        {
192
                if ($query) {
×
193
                        $query = http_build_query($query, '', '&');
×
194
                        if ($query !== '') {
×
195
                                $path .= '?' . $query;
×
196
                        }
197
                }
198

199
                $this->attrs['href'] = $path;
×
200
                return $this;
×
201
        }
202

203

204
        /**
205
         * Sets element's textual content.
206
         */
207
        final public function setText(string $text): static
1✔
208
        {
209
                $this->removeChildren();
1✔
210
                $this->children = [$text];
1✔
211
                return $this;
1✔
212
        }
213

214

215
        /**
216
         * Gets element's textual content.
217
         */
218
        final public function getText(): ?string
219
        {
220
                $s = '';
1✔
221
                foreach ($this->children as $child) {
1✔
222
                        if (is_object($child)) {
1✔
223
                                return null;
1✔
224
                        }
225

226
                        $s .= $child;
1✔
227
                }
228

229
                return $s;
1✔
230
        }
231

232

233
        /**
234
         * Adds new element's child.
235
         */
236
        final public function add(self|string $child): static
1✔
237
        {
238
                return $this->insert(null, $child);
1✔
239
        }
240

241

242
        /**
243
         * Creates and adds a new HtmlElement child.
244
         * @param  array<string, mixed>|string|null  $attrs
245
         */
246
        final public function create(string $name, array|string|null $attrs = null): self
1✔
247
        {
248
                $this->insert(null, $child = new self($name, $attrs));
1✔
249
                return $child;
1✔
250
        }
251

252

253
        /**
254
         * Inserts child node.
255
         */
256
        public function insert(?int $index, self|string $child, bool $replace = false): static
1✔
257
        {
258
                if ($index === null) { // append
1✔
259
                        $this->children[] = $child;
1✔
260

261
                } else { // insert or replace
262
                        array_splice($this->children, (int) $index, $replace ? 1 : 0, [$child]);
1✔
263
                }
264

265
                return $this;
1✔
266
        }
267

268

269
        /**
270
         * Inserts (replaces) child node (ArrayAccess implementation).
271
         * @param  int  $index
272
         * @param  HtmlElement  $child
273
         */
274
        final public function offsetSet($index, $child): void
275
        {
276
                $this->insert($index, $child, replace: true);
1✔
277
        }
1✔
278

279

280
        /**
281
         * Returns child node (ArrayAccess implementation).
282
         * @param  int  $index
283
         */
284
        final public function offsetGet($index): self|string
285
        {
286
                return $this->children[$index];
1✔
287
        }
288

289

290
        /**
291
         * Exists child node? (ArrayAccess implementation).
292
         * @param  int  $index
293
         */
294
        final public function offsetExists($index): bool
295
        {
296
                return isset($this->children[$index]);
1✔
297
        }
298

299

300
        /**
301
         * Removes child node (ArrayAccess implementation).
302
         * @param  int  $index
303
         */
304
        public function offsetUnset($index): void
305
        {
306
                if (isset($this->children[$index])) {
×
307
                        array_splice($this->children, (int) $index, 1);
×
308
                }
309
        }
310

311

312
        /**
313
         * Required by the Countable interface.
314
         */
315
        final public function count(): int
316
        {
317
                return count($this->children);
1✔
318
        }
319

320

321
        /**
322
         * Removed all children.
323
         */
324
        public function removeChildren(): void
325
        {
326
                $this->children = [];
1✔
327
        }
1✔
328

329

330
        /**
331
         * Required by the IteratorAggregate interface.
332
         * @return \ArrayIterator<int, HtmlElement|string>
333
         */
334
        final public function getIterator(): \ArrayIterator
335
        {
336
                return new \ArrayIterator($this->children);
×
337
        }
338

339

340
        /**
341
         * Returns all of children.
342
         * @return list<HtmlElement|string>
343
         */
344
        final public function getChildren(): array
345
        {
346
                return $this->children;
1✔
347
        }
348

349

350
        /**
351
         * @param  list<self|string>  $children
352
         */
353
        public function inject(array $children): void
1✔
354
        {
355
                (function (self|string ...$children) {})(...$children);
1✔
356
                $this->children = $children;
1✔
357
        }
1✔
358

359

360
        /**
361
         * Returns element's start tag.
362
         */
363
        public function startTag(): string
364
        {
365
                if (!$this->name) {
1✔
366
                        return '';
1✔
367
                }
368

369
                $s = '<' . $this->name;
1✔
370

371
                foreach ($this->attrs as $key => $value) {
1✔
372
                        if ($value === null || $value === false) {
1✔
373
                                continue; // skip nulls and false boolean attributes
1✔
374

375
                        } elseif ($value === true) {
1✔
UNCOV
376
                                $s .= ' ' . $key; // true boolean attribute
×
UNCOV
377
                                continue;
×
378

379
                        } elseif (is_array($value)) {
1✔
380
                                $tmp = null;
1✔
381
                                foreach ($value as $k => $v) {
1✔
382
                                        if ($v == null) { // skip nulls & empty string; composite 'style' vs. 'others'
1✔
383
                                                continue;
1✔
384
                                        } elseif (is_string($k)) {
1✔
385
                                                $tmp[] = $k . ':' . $v;
1✔
386
                                        } else {
387
                                                $tmp[] = $v;
1✔
388
                                        }
389
                                }
390

391
                                if (!$tmp) {
1✔
392
                                        continue;
1✔
393
                                }
394

395
                                $value = implode($key === 'style' ? ';' : ' ', $tmp);
1✔
396

397
                        } else {
398
                                $value = (string) $value;
1✔
399
                        }
400

401
                        // add new attribute
402
                        $value = str_replace(['&', '"', '<', '>', '@'], ['&amp;', '&quot;', '&lt;', '&gt;', '&#64;'], $value);
1✔
403
                        $s .= ' ' . $key . '="' . Helpers::freezeSpaces($value) . '"';
1✔
404
                }
405

406
                return $s . '>';
1✔
407
        }
408

409

410
        /**
411
         * Returns element's end tag.
412
         */
413
        public function endTag(): string
414
        {
415
                if ($this->name && !$this->isEmpty) {
1✔
416
                        return '</' . $this->name . '>';
1✔
417
                }
418

419
                return '';
1✔
420
        }
421

422

423
        /**
424
         * Clones all children too.
425
         */
426
        public function __clone()
427
        {
428
                foreach ($this->children as $key => $value) {
1✔
UNCOV
429
                        if (is_object($value)) {
×
UNCOV
430
                                $this->children[$key] = clone $value;
×
431
                        }
432
                }
433
        }
1✔
434

435

436
        final public function getContentType(): string
437
        {
438
                $inlineType = self::$inlineElements[$this->name ?? ''] ?? null;
1✔
439
                return $inlineType === null
1✔
440
                        ? Texy::CONTENT_BLOCK
1✔
441
                        : ($inlineType ? Texy::CONTENT_REPLACED : Texy::CONTENT_MARKUP);
1✔
442
        }
443

444

445
        /** @param  array<string, array{array<string, int>, array<string, int>}>  $dtd */
446
        final public function validateAttrs(array $dtd): void
1✔
447
        {
448
                $allowed = $dtd[$this->name ?? ''][0] ?? null;
1✔
449
                if (is_array($allowed)) {
1✔
450
                        foreach ($this->attrs as $attr => $foo) {
1✔
451
                                if (
452
                                        !isset($allowed[$attr])
1✔
453
                                        && (!isset($allowed['data-*']) || !str_starts_with((string) $attr, 'data-'))
1✔
454
                                        && (!isset($allowed['aria-*']) || !str_starts_with((string) $attr, 'aria-'))
1✔
455
                                ) {
456
                                        unset($this->attrs[$attr]);
1✔
457
                                }
458
                        }
459
                }
460
        }
1✔
461

462

463
        /** @param  array<string, array{array<string, int>, array<string, int>}>  $dtd */
464
        public function validateChild(self|string $child, array $dtd): bool
465
        {
UNCOV
466
                if ($this->name !== null && isset($dtd[$this->name])) {
×
UNCOV
467
                        if ($child instanceof self) {
×
UNCOV
468
                                $child = $child->name;
×
469
                        }
470

UNCOV
471
                        return $child !== null && isset($dtd[$this->name][1][$child]);
×
472
                } else {
UNCOV
473
                        return true; // unknown element
×
474
                }
475
        }
476
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc