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

dg / texy / 18957288707

30 Oct 2025 10:55PM UTC coverage: 92.744% (-0.002%) from 92.746%
18957288707

push

github

dg
HtmlElement: removed toHtml() & toText()

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

13 existing lines in 3 files now uncovered.

2390 of 2577 relevant lines covered (92.74%)

0.93 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
class HtmlElement implements \ArrayAccess, /* Countable, */ \IteratorAggregate
25
{
26
        public const InnerText = '%TEXT';
27
        public const InnerTransparent = '%TRANS';
28

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

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

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

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

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

86
        /** @var array<int, HtmlElement|string> nodes */
87
        protected array $children = [];
88
        private ?string $name;
89
        private bool $isEmpty;
90

91

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

105

106
        public static function el(?string $name = null, $attrs = null): static
107
        {
108
                return new self($name, $attrs);
×
109
        }
110

111

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

124

125
        /**
126
         * Returns element's name.
127
         */
128
        final public function getName(): ?string
129
        {
130
                return $this->name;
1✔
131
        }
132

133

134
        /**
135
         * Is element empty?
136
         */
137
        final public function isEmpty(): bool
138
        {
139
                return $this->isEmpty;
1✔
140
        }
141

142

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

151

152
        /**
153
         * Overloaded getter for element's attribute.
154
         */
155
        final public function &__get(string $name)
156
        {
157
                return $this->attrs[$name];
×
158
        }
159

160

161
        /**
162
         * Sets element's attribute.
163
         */
164
        final public function setAttribute(string $name, $value): static
165
        {
166
                $this->attrs[$name] = $value;
×
167
                return $this;
×
168
        }
169

170

171
        /**
172
         * Returns element's attribute.
173
         * @return string|int|bool|string[]|null
174
         */
175
        final public function getAttribute(string $name)
176
        {
177
                return $this->attrs[$name] ?? null;
×
178
        }
179

180

181
        /**
182
         * Special setter for element's attribute.
183
         */
184
        final public function href(string $path, ?array $query = null): static
185
        {
186
                if ($query) {
×
187
                        $query = http_build_query($query, '', '&');
×
188
                        if ($query !== '') {
×
189
                                $path .= '?' . $query;
×
190
                        }
191
                }
192

193
                $this->attrs['href'] = $path;
×
194
                return $this;
×
195
        }
196

197

198
        /**
199
         * Sets element's textual content.
200
         */
201
        final public function setText(string $text): static
1✔
202
        {
203
                $this->removeChildren();
1✔
204
                $this->children = [$text];
1✔
205
                return $this;
1✔
206
        }
207

208

209
        /**
210
         * Gets element's textual content.
211
         */
212
        final public function getText(): ?string
213
        {
214
                $s = '';
1✔
215
                foreach ($this->children as $child) {
1✔
216
                        if (is_object($child)) {
1✔
217
                                return null;
1✔
218
                        }
219

220
                        $s .= $child;
1✔
221
                }
222

223
                return $s;
1✔
224
        }
225

226

227
        /**
228
         * Adds new element's child.
229
         */
230
        final public function add(self|string $child): static
1✔
231
        {
232
                return $this->insert(null, $child);
1✔
233
        }
234

235

236
        /**
237
         * Creates and adds a new HtmlElement child.
238
         */
239
        final public function create(string $name, array|string|null $attrs = null): static
1✔
240
        {
241
                $this->insert(null, $child = new self($name, $attrs));
1✔
242
                return $child;
1✔
243
        }
244

245

246
        /**
247
         * Inserts child node.
248
         */
249
        public function insert(?int $index, self|string $child, bool $replace = false): static
1✔
250
        {
251
                if ($index === null) { // append
1✔
252
                        $this->children[] = $child;
1✔
253

254
                } else { // insert or replace
255
                        array_splice($this->children, (int) $index, $replace ? 1 : 0, [$child]);
1✔
256
                }
257

258
                return $this;
1✔
259
        }
260

261

262
        /**
263
         * Inserts (replaces) child node (ArrayAccess implementation).
264
         * @param  int  $index
265
         * @param  HtmlElement  $child
266
         */
267
        final public function offsetSet($index, $child): void
268
        {
269
                $this->insert($index, $child, true);
1✔
270
        }
1✔
271

272

273
        /**
274
         * Returns child node (ArrayAccess implementation).
275
         * @param  int  $index
276
         */
277
        final public function offsetGet($index): mixed
278
        {
279
                return $this->children[$index];
1✔
280
        }
281

282

283
        /**
284
         * Exists child node? (ArrayAccess implementation).
285
         * @param  int  $index
286
         */
287
        final public function offsetExists($index): bool
288
        {
289
                return isset($this->children[$index]);
1✔
290
        }
291

292

293
        /**
294
         * Removes child node (ArrayAccess implementation).
295
         * @param  int  $index
296
         */
297
        public function offsetUnset($index): void
298
        {
299
                if (isset($this->children[$index])) {
×
300
                        array_splice($this->children, (int) $index, 1);
×
301
                }
302
        }
303

304

305
        /**
306
         * Required by the Countable interface.
307
         */
308
        final public function count(): int
309
        {
310
                return count($this->children);
1✔
311
        }
312

313

314
        /**
315
         * Removed all children.
316
         */
317
        public function removeChildren(): void
318
        {
319
                $this->children = [];
1✔
320
        }
1✔
321

322

323
        /**
324
         * Required by the IteratorAggregate interface.
325
         */
326
        final public function getIterator(): \ArrayIterator
327
        {
328
                return new \ArrayIterator($this->children);
×
329
        }
330

331

332
        /**
333
         * Returns all of children.
334
         */
335
        final public function getChildren(): array
336
        {
337
                return $this->children;
1✔
338
        }
339

340

341
        /**
342
         * @param  array<self|string>  $children
343
         */
344
        public function inject(array $children): void
1✔
345
        {
346
                (function (self|string ...$children) {})(...$children);
1✔
347
                $this->children = $children;
1✔
348
        }
1✔
349

350

351
        /**
352
         * Returns element's start tag.
353
         */
354
        public function startTag(): string
355
        {
356
                if (!$this->name) {
1✔
357
                        return '';
1✔
358
                }
359

360
                $s = '<' . $this->name;
1✔
361

362
                foreach ($this->attrs as $key => $value) {
1✔
363
                        if ($value === null || $value === false) {
1✔
364
                                continue; // skip nulls and false boolean attributes
1✔
365

366
                        } elseif ($value === true) {
1✔
UNCOV
367
                                $s .= ' ' . $key; // true boolean attribute
×
UNCOV
368
                                continue;
×
369

370
                        } elseif (is_array($value)) {
1✔
371
                                $tmp = null;
1✔
372
                                foreach ($value as $k => $v) {
1✔
373
                                        if ($v == null) { // skip nulls & empty string; composite 'style' vs. 'others'
1✔
374
                                                continue;
1✔
375
                                        } elseif (is_string($k)) {
1✔
376
                                                $tmp[] = $k . ':' . $v;
1✔
377
                                        } else {
378
                                                $tmp[] = $v;
1✔
379
                                        }
380
                                }
381

382
                                if (!$tmp) {
1✔
383
                                        continue;
1✔
384
                                }
385

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

388
                        } else {
389
                                $value = (string) $value;
1✔
390
                        }
391

392
                        // add new attribute
393
                        $value = str_replace(['&', '"', '<', '>', '@'], ['&amp;', '&quot;', '&lt;', '&gt;', '&#64;'], $value);
1✔
394
                        $s .= ' ' . $key . '="' . Helpers::freezeSpaces($value) . '"';
1✔
395
                }
396

397
                return $s . '>';
1✔
398
        }
399

400

401
        /**
402
         * Returns element's end tag.
403
         */
404
        public function endTag(): string
405
        {
406
                if ($this->name && !$this->isEmpty) {
1✔
407
                        return '</' . $this->name . '>';
1✔
408
                }
409

410
                return '';
1✔
411
        }
412

413

414
        /**
415
         * Clones all children too.
416
         */
417
        public function __clone()
418
        {
419
                foreach ($this->children as $key => $value) {
1✔
UNCOV
420
                        if (is_object($value)) {
×
UNCOV
421
                                $this->children[$key] = clone $value;
×
422
                        }
423
                }
424
        }
1✔
425

426

427
        final public function getContentType(): string
428
        {
429
                $inlineType = self::$inlineElements[$this->name ?? ''] ?? null;
1✔
430
                return $inlineType === null
1✔
431
                        ? Texy::CONTENT_BLOCK
1✔
432
                        : ($inlineType ? Texy::CONTENT_REPLACED : Texy::CONTENT_MARKUP);
1✔
433
        }
434

435

436
        final public function validateAttrs(array $dtd): void
1✔
437
        {
438
                $allowed = $dtd[$this->name][0] ?? null;
1✔
439
                if (is_array($allowed)) {
1✔
440
                        foreach ($this->attrs as $attr => $foo) {
1✔
441
                                if (
442
                                        !isset($allowed[$attr])
1✔
443
                                        && (!isset($allowed['data-*']) || !str_starts_with((string) $attr, 'data-'))
1✔
444
                                        && (!isset($allowed['aria-*']) || !str_starts_with((string) $attr, 'aria-'))
1✔
445
                                ) {
446
                                        unset($this->attrs[$attr]);
1✔
447
                                }
448
                        }
449
                }
450
        }
1✔
451

452

453
        public function validateChild($child, array $dtd): bool
454
        {
UNCOV
455
                if (isset($dtd[$this->name])) {
×
UNCOV
456
                        if ($child instanceof self) {
×
UNCOV
457
                                $child = $child->name;
×
458
                        }
459

UNCOV
460
                        return isset($dtd[$this->name][1][$child]);
×
461
                } else {
UNCOV
462
                        return true; // unknown element
×
463
                }
464
        }
465
}
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