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

dg / texy / 15479423458

05 Jun 2025 11:37PM UTC coverage: 92.741% (+0.5%) from 92.224%
15479423458

push

github

dg
HtmlElement: removed toHtml() & toText()

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

78 existing lines in 11 files now uncovered.

2389 of 2576 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.info)
5
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6
 */
7

8
declare(strict_types=1);
9

10
namespace Texy;
11

12

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

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

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

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

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

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

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

89

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

103

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

109

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

122

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

131

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

140

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

149

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

158

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

168

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

178

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

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

195

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

206

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

218
                        $s .= $child;
1✔
219
                }
220

221
                return $s;
1✔
222
        }
223

224

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

233

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

243

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

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

256
                return $this;
1✔
257
        }
258

259

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

270

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

280

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

290

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

302

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

311

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

320

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

329

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

338

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

348

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

358
                $s = '<' . $this->name;
1✔
359

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

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

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

380
                                if (!$tmp) {
1✔
381
                                        continue;
1✔
382
                                }
383

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

386
                        } else {
387
                                $value = (string) $value;
1✔
388
                        }
389

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

395
                return $s . '>';
1✔
396
        }
397

398

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

408
                return '';
1✔
409
        }
410

411

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

424

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

433

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

450

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

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