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

nette / utils / 21636962765

03 Feb 2026 03:39PM UTC coverage: 93.312% (+0.05%) from 93.264%
21636962765

push

github

dg
added CLAUDE.md

2065 of 2213 relevant lines covered (93.31%)

0.93 hits per line

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

97.22
/src/Utils/Html.php
1
<?php
2

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

8
declare(strict_types=1);
9

10
namespace Nette\Utils;
11

12
use Nette\HtmlStringable;
13
use function array_merge, array_splice, count, explode, func_num_args, html_entity_decode, htmlspecialchars, http_build_query, implode, is_array, is_bool, is_float, is_object, is_string, json_encode, max, number_format, rtrim, str_contains, str_repeat, str_replace, strip_tags, strncmp, strpbrk, substr;
14
use const ENT_HTML5, ENT_NOQUOTES, ENT_QUOTES;
15

16

17
/**
18
 * HTML helper.
19
 *
20
 * @property ?string $accept
21
 * @property ?string $accesskey
22
 * @property ?string $action
23
 * @property ?string $align
24
 * @property ?string $allow
25
 * @property ?string $alt
26
 * @property ?bool   $async
27
 * @property ?string $autocapitalize
28
 * @property ?string $autocomplete
29
 * @property ?bool   $autofocus
30
 * @property ?bool   $autoplay
31
 * @property ?string $charset
32
 * @property ?bool   $checked
33
 * @property ?string $cite
34
 * @property ?string $class
35
 * @property ?int    $cols
36
 * @property ?int    $colspan
37
 * @property ?string $content
38
 * @property ?bool   $contenteditable
39
 * @property ?bool   $controls
40
 * @property ?string $coords
41
 * @property ?string $crossorigin
42
 * @property ?string $data
43
 * @property ?string $datetime
44
 * @property ?string $decoding
45
 * @property ?bool   $default
46
 * @property ?bool   $defer
47
 * @property ?string $dir
48
 * @property ?string $dirname
49
 * @property ?bool   $disabled
50
 * @property ?bool   $download
51
 * @property ?string $draggable
52
 * @property ?string $dropzone
53
 * @property ?string $enctype
54
 * @property ?string $for
55
 * @property ?string $form
56
 * @property ?string $formaction
57
 * @property ?string $formenctype
58
 * @property ?string $formmethod
59
 * @property ?bool   $formnovalidate
60
 * @property ?string $formtarget
61
 * @property ?string $headers
62
 * @property ?int    $height
63
 * @property ?bool   $hidden
64
 * @property ?float  $high
65
 * @property ?string $href
66
 * @property ?string $hreflang
67
 * @property ?string $id
68
 * @property ?string $integrity
69
 * @property ?string $inputmode
70
 * @property ?bool   $ismap
71
 * @property ?string $itemprop
72
 * @property ?string $kind
73
 * @property ?string $label
74
 * @property ?string $lang
75
 * @property ?string $list
76
 * @property ?bool   $loop
77
 * @property ?float  $low
78
 * @property ?float  $max
79
 * @property ?int    $maxlength
80
 * @property ?int    $minlength
81
 * @property ?string $media
82
 * @property ?string $method
83
 * @property ?float  $min
84
 * @property ?bool   $multiple
85
 * @property ?bool   $muted
86
 * @property ?string $name
87
 * @property ?bool   $novalidate
88
 * @property ?bool   $open
89
 * @property ?float  $optimum
90
 * @property ?string $pattern
91
 * @property ?string $ping
92
 * @property ?string $placeholder
93
 * @property ?string $poster
94
 * @property ?string $preload
95
 * @property ?string $radiogroup
96
 * @property ?bool   $readonly
97
 * @property ?string $rel
98
 * @property ?bool   $required
99
 * @property ?bool   $reversed
100
 * @property ?int    $rows
101
 * @property ?int    $rowspan
102
 * @property ?string $sandbox
103
 * @property ?string $scope
104
 * @property ?bool   $selected
105
 * @property ?string $shape
106
 * @property ?int    $size
107
 * @property ?string $sizes
108
 * @property ?string $slot
109
 * @property ?int    $span
110
 * @property ?string $spellcheck
111
 * @property ?string $src
112
 * @property ?string $srcdoc
113
 * @property ?string $srclang
114
 * @property ?string $srcset
115
 * @property ?int    $start
116
 * @property ?float  $step
117
 * @property ?string $style
118
 * @property ?int    $tabindex
119
 * @property ?string $target
120
 * @property ?string $title
121
 * @property ?string $translate
122
 * @property ?string $type
123
 * @property ?string $usemap
124
 * @property ?string $value
125
 * @property ?int    $width
126
 * @property ?string $wrap
127
 *
128
 * @method self accept(?string $val)
129
 * @method self accesskey(?string $val, bool $state = null)
130
 * @method self action(?string $val)
131
 * @method self align(?string $val)
132
 * @method self allow(?string $val, bool $state = null)
133
 * @method self alt(?string $val)
134
 * @method self async(?bool $val)
135
 * @method self autocapitalize(?string $val)
136
 * @method self autocomplete(?string $val)
137
 * @method self autofocus(?bool $val)
138
 * @method self autoplay(?bool $val)
139
 * @method self charset(?string $val)
140
 * @method self checked(?bool $val)
141
 * @method self cite(?string $val)
142
 * @method self class(?string $val, bool $state = null)
143
 * @method self cols(?int $val)
144
 * @method self colspan(?int $val)
145
 * @method self content(?string $val)
146
 * @method self contenteditable(?bool $val)
147
 * @method self controls(?bool $val)
148
 * @method self coords(?string $val)
149
 * @method self crossorigin(?string $val)
150
 * @method self datetime(?string $val)
151
 * @method self decoding(?string $val)
152
 * @method self default(?bool $val)
153
 * @method self defer(?bool $val)
154
 * @method self dir(?string $val)
155
 * @method self dirname(?string $val)
156
 * @method self disabled(?bool $val)
157
 * @method self download(?bool $val)
158
 * @method self draggable(?string $val)
159
 * @method self dropzone(?string $val)
160
 * @method self enctype(?string $val)
161
 * @method self for(?string $val)
162
 * @method self form(?string $val)
163
 * @method self formaction(?string $val)
164
 * @method self formenctype(?string $val)
165
 * @method self formmethod(?string $val)
166
 * @method self formnovalidate(?bool $val)
167
 * @method self formtarget(?string $val)
168
 * @method self headers(?string $val, bool $state = null)
169
 * @method self height(?int $val)
170
 * @method self hidden(?bool $val)
171
 * @method self high(?float $val)
172
 * @method self hreflang(?string $val)
173
 * @method self id(?string $val)
174
 * @method self integrity(?string $val)
175
 * @method self inputmode(?string $val)
176
 * @method self ismap(?bool $val)
177
 * @method self itemprop(?string $val)
178
 * @method self kind(?string $val)
179
 * @method self label(?string $val)
180
 * @method self lang(?string $val)
181
 * @method self list(?string $val)
182
 * @method self loop(?bool $val)
183
 * @method self low(?float $val)
184
 * @method self max(?float $val)
185
 * @method self maxlength(?int $val)
186
 * @method self minlength(?int $val)
187
 * @method self media(?string $val)
188
 * @method self method(?string $val)
189
 * @method self min(?float $val)
190
 * @method self multiple(?bool $val)
191
 * @method self muted(?bool $val)
192
 * @method self name(?string $val)
193
 * @method self novalidate(?bool $val)
194
 * @method self open(?bool $val)
195
 * @method self optimum(?float $val)
196
 * @method self pattern(?string $val)
197
 * @method self ping(?string $val, bool $state = null)
198
 * @method self placeholder(?string $val)
199
 * @method self poster(?string $val)
200
 * @method self preload(?string $val)
201
 * @method self radiogroup(?string $val)
202
 * @method self readonly(?bool $val)
203
 * @method self rel(?string $val)
204
 * @method self required(?bool $val)
205
 * @method self reversed(?bool $val)
206
 * @method self rows(?int $val)
207
 * @method self rowspan(?int $val)
208
 * @method self sandbox(?string $val, bool $state = null)
209
 * @method self scope(?string $val)
210
 * @method self selected(?bool $val)
211
 * @method self shape(?string $val)
212
 * @method self size(?int $val)
213
 * @method self sizes(?string $val)
214
 * @method self slot(?string $val)
215
 * @method self span(?int $val)
216
 * @method self spellcheck(?string $val)
217
 * @method self src(?string $val)
218
 * @method self srcdoc(?string $val)
219
 * @method self srclang(?string $val)
220
 * @method self srcset(?string $val)
221
 * @method self start(?int $val)
222
 * @method self step(?float $val)
223
 * @method self style(?string $property, string $val = null)
224
 * @method self tabindex(?int $val)
225
 * @method self target(?string $val)
226
 * @method self title(?string $val)
227
 * @method self translate(?string $val)
228
 * @method self type(?string $val)
229
 * @method self usemap(?string $val)
230
 * @method self value(?string $val)
231
 * @method self width(?int $val)
232
 * @method self wrap(?string $val)
233
 *
234
 * @implements \IteratorAggregate<int, HtmlStringable|string>
235
 * @implements \ArrayAccess<int, HtmlStringable|string>
236
 */
237
class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringable
238
{
239
        /** @var array<string, mixed>  element's attributes */
240
        public array $attrs = [];
241

242
        /** @var array<string, int>  void elements */
243
        public static array $emptyElements = [
244
                'img' => 1, 'hr' => 1, 'br' => 1, 'input' => 1, 'meta' => 1, 'area' => 1, 'embed' => 1, 'keygen' => 1,
245
                'source' => 1, 'base' => 1, 'col' => 1, 'link' => 1, 'param' => 1, 'basefont' => 1, 'frame' => 1,
246
                'isindex' => 1, 'wbr' => 1, 'command' => 1, 'track' => 1,
247
        ];
248

249
        /** @var array<int, HtmlStringable|string> nodes */
250
        protected array $children = [];
251

252
        /** element's name */
253
        private string $name = '';
254

255
        private bool $isEmpty = false;
256

257

258
        /**
259
         * Constructs new HTML element.
260
         * @param  array<string, mixed>|string|null  $attrs element's attributes or plain text content
261
         */
262
        public static function el(?string $name = null, array|string|null $attrs = null): static
1✔
263
        {
264
                $el = new static;
1✔
265
                $parts = explode(' ', (string) $name, 2);
1✔
266
                $el->setName($parts[0]);
1✔
267

268
                if (is_array($attrs)) {
1✔
269
                        $el->attrs = $attrs;
×
270

271
                } elseif ($attrs !== null) {
1✔
272
                        $el->setText($attrs);
×
273
                }
274

275
                if (isset($parts[1])) {
1✔
276
                        foreach (Strings::matchAll($parts[1] . ' ', '#([a-z0-9:-]+)(?:=(["\'])?(.*?)(?(2)\2|\s))?#i') as $m) {
1✔
277
                                $el->attrs[$m[1]] = $m[3] ?? true;
1✔
278
                        }
279
                }
280

281
                return $el;
1✔
282
        }
283

284

285
        /**
286
         * Returns an object representing HTML text.
287
         */
288
        public static function fromHtml(string $html): static
1✔
289
        {
290
                return (new static)->setHtml($html);
1✔
291
        }
292

293

294
        /**
295
         * Returns an object representing plain text.
296
         */
297
        public static function fromText(string $text): static
1✔
298
        {
299
                return (new static)->setText($text);
1✔
300
        }
301

302

303
        /**
304
         * Converts to HTML.
305
         */
306
        final public function toHtml(): string
307
        {
308
                return $this->render();
1✔
309
        }
310

311

312
        /**
313
         * Converts to plain text.
314
         */
315
        final public function toText(): string
316
        {
317
                return $this->getText();
1✔
318
        }
319

320

321
        /**
322
         * Converts given HTML code to plain text.
323
         */
324
        public static function htmlToText(string $html): string
1✔
325
        {
326
                return html_entity_decode(strip_tags($html), ENT_QUOTES | ENT_HTML5, 'UTF-8');
1✔
327
        }
328

329

330
        /**
331
         * Changes element's name.
332
         */
333
        final public function setName(string $name, ?bool $isEmpty = null): static
1✔
334
        {
335
                $this->name = $name;
1✔
336
                $this->isEmpty = $isEmpty ?? isset(static::$emptyElements[$name]);
1✔
337
                return $this;
1✔
338
        }
339

340

341
        /**
342
         * Returns element's name.
343
         */
344
        final public function getName(): string
345
        {
346
                return $this->name;
1✔
347
        }
348

349

350
        /**
351
         * Is element empty?
352
         */
353
        final public function isEmpty(): bool
354
        {
355
                return $this->isEmpty;
×
356
        }
357

358

359
        /**
360
         * Sets multiple attributes.
361
         * @param  array<string, mixed>  $attrs
362
         */
363
        public function addAttributes(array $attrs): static
1✔
364
        {
365
                $this->attrs = array_merge($this->attrs, $attrs);
1✔
366
                return $this;
1✔
367
        }
368

369

370
        /**
371
         * Appends value to element's attribute.
372
         */
373
        public function appendAttribute(string $name, mixed $value, mixed $option = true): static
1✔
374
        {
375
                if (is_array($value)) {
1✔
376
                        $prev = isset($this->attrs[$name]) ? (array) $this->attrs[$name] : [];
1✔
377
                        $this->attrs[$name] = $value + $prev;
1✔
378

379
                } elseif ((string) $value === '') {
1✔
380
                        $tmp = &$this->attrs[$name]; // appending empty value? -> ignore, but ensure it exists
1✔
381

382
                } elseif (!isset($this->attrs[$name]) || is_array($this->attrs[$name])) { // needs array
1✔
383
                        $this->attrs[$name][$value] = $option;
1✔
384

385
                } else {
386
                        $this->attrs[$name] = [$this->attrs[$name] => true, $value => $option];
1✔
387
                }
388

389
                return $this;
1✔
390
        }
391

392

393
        /**
394
         * Sets element's attribute.
395
         */
396
        public function setAttribute(string $name, mixed $value): static
1✔
397
        {
398
                $this->attrs[$name] = $value;
1✔
399
                return $this;
1✔
400
        }
401

402

403
        /**
404
         * Returns element's attribute.
405
         */
406
        public function getAttribute(string $name): mixed
1✔
407
        {
408
                return $this->attrs[$name] ?? null;
1✔
409
        }
410

411

412
        /**
413
         * Unsets element's attribute.
414
         */
415
        public function removeAttribute(string $name): static
1✔
416
        {
417
                unset($this->attrs[$name]);
1✔
418
                return $this;
1✔
419
        }
420

421

422
        /**
423
         * Unsets element's attributes.
424
         * @param  list<string>  $attributes
425
         */
426
        public function removeAttributes(array $attributes): static
1✔
427
        {
428
                foreach ($attributes as $name) {
1✔
429
                        unset($this->attrs[$name]);
1✔
430
                }
431

432
                return $this;
1✔
433
        }
434

435

436
        /**
437
         * Overloaded setter for element's attribute.
438
         */
439
        final public function __set(string $name, mixed $value): void
1✔
440
        {
441
                $this->attrs[$name] = $value;
1✔
442
        }
1✔
443

444

445
        /**
446
         * Overloaded getter for element's attribute.
447
         */
448
        final public function &__get(string $name): mixed
1✔
449
        {
450
                return $this->attrs[$name];
1✔
451
        }
452

453

454
        /**
455
         * Overloaded tester for element's attribute.
456
         */
457
        final public function __isset(string $name): bool
1✔
458
        {
459
                return isset($this->attrs[$name]);
1✔
460
        }
461

462

463
        /**
464
         * Overloaded unsetter for element's attribute.
465
         */
466
        final public function __unset(string $name): void
1✔
467
        {
468
                unset($this->attrs[$name]);
1✔
469
        }
1✔
470

471

472
        /**
473
         * Overloaded setter for element's attribute.
474
         * @param  array<mixed>  $args
475
         */
476
        final public function __call(string $m, array $args): mixed
1✔
477
        {
478
                $p = substr($m, 0, 3);
1✔
479
                if ($p === 'get' || $p === 'set' || $p === 'add') {
1✔
480
                        $m = substr($m, 3);
1✔
481
                        $m[0] = $m[0] | "\x20";
1✔
482
                        if ($p === 'get') {
1✔
483
                                return $this->attrs[$m] ?? null;
1✔
484

485
                        } elseif ($p === 'add') {
1✔
486
                                $args[] = true;
1✔
487
                        }
488
                }
489

490
                if (count($args) === 0) { // invalid
1✔
491

492
                } elseif (count($args) === 1) { // set
1✔
493
                        $this->attrs[$m] = $args[0];
1✔
494

495
                } else { // add
496
                        $this->appendAttribute($m, $args[0], $args[1]);
1✔
497
                }
498

499
                return $this;
1✔
500
        }
501

502

503
        /**
504
         * Special setter for element's attribute.
505
         * @param  array<string, mixed>  $query
506
         */
507
        final public function href(string $path, array $query = []): static
1✔
508
        {
509
                if ($query) {
1✔
510
                        $query = http_build_query($query, '', '&');
1✔
511
                        if ($query !== '') {
1✔
512
                                $path .= '?' . $query;
1✔
513
                        }
514
                }
515

516
                $this->attrs['href'] = $path;
1✔
517
                return $this;
1✔
518
        }
519

520

521
        /**
522
         * Setter for data-* attributes. Booleans are converted to 'true' resp. 'false'.
523
         */
524
        public function data(string $name, mixed $value = null): static
1✔
525
        {
526
                if (func_num_args() === 1) {
1✔
527
                        $this->attrs['data'] = $name;
1✔
528
                } else {
529
                        $this->attrs["data-$name"] = is_bool($value)
1✔
530
                                ? json_encode($value)
1✔
531
                                : $value;
1✔
532
                }
533

534
                return $this;
1✔
535
        }
536

537

538
        /**
539
         * Sets element's HTML content.
540
         */
541
        final public function setHtml(mixed $html): static
1✔
542
        {
543
                $this->children = [(string) $html];
1✔
544
                return $this;
1✔
545
        }
546

547

548
        /**
549
         * Returns element's HTML content.
550
         */
551
        final public function getHtml(): string
552
        {
553
                return implode('', $this->children);
1✔
554
        }
555

556

557
        /**
558
         * Sets element's textual content.
559
         */
560
        final public function setText(mixed $text): static
1✔
561
        {
562
                if (!$text instanceof HtmlStringable) {
1✔
563
                        $text = htmlspecialchars((string) $text, ENT_NOQUOTES, 'UTF-8');
1✔
564
                }
565

566
                $this->children = [(string) $text];
1✔
567
                return $this;
1✔
568
        }
569

570

571
        /**
572
         * Returns element's textual content.
573
         */
574
        final public function getText(): string
575
        {
576
                return self::htmlToText($this->getHtml());
1✔
577
        }
578

579

580
        /**
581
         * Adds new element's child.
582
         */
583
        final public function addHtml(HtmlStringable|string $child): static
1✔
584
        {
585
                return $this->insert(null, $child);
1✔
586
        }
587

588

589
        /**
590
         * Appends plain-text string to element content.
591
         */
592
        public function addText(\Stringable|string|int|null $text): static
1✔
593
        {
594
                if (!$text instanceof HtmlStringable) {
1✔
595
                        $text = htmlspecialchars((string) $text, ENT_NOQUOTES, 'UTF-8');
1✔
596
                }
597

598
                return $this->insert(null, $text);
1✔
599
        }
600

601

602
        /**
603
         * Creates and adds a new Html child.
604
         * @param  array<string, mixed>|string|null  $attrs
605
         */
606
        final public function create(string $name, array|string|null $attrs = null): static
1✔
607
        {
608
                $this->insert(null, $child = static::el($name, $attrs));
1✔
609
                return $child;
1✔
610
        }
611

612

613
        /**
614
         * Inserts child node.
615
         */
616
        public function insert(?int $index, HtmlStringable|string $child, bool $replace = false): static
1✔
617
        {
618
                $child = $child instanceof self ? $child : (string) $child;
1✔
619
                if ($index === null) { // append
1✔
620
                        $this->children[] = $child;
1✔
621

622
                } else { // insert or replace
623
                        array_splice($this->children, $index, $replace ? 1 : 0, [$child]);
×
624
                }
625

626
                return $this;
1✔
627
        }
628

629

630
        /**
631
         * Inserts (replaces) child node (\ArrayAccess implementation).
632
         * @param  ?int  $index  position or null for appending
633
         * @param  Html|string  $child  Html node or raw HTML string
634
         */
635
        final public function offsetSet($index, $child): void
636
        {
637
                $this->insert($index, $child, replace: true);
×
638
        }
639

640

641
        /**
642
         * Returns child node (\ArrayAccess implementation).
643
         * @param  int  $index
644
         */
645
        final public function offsetGet($index): HtmlStringable|string
646
        {
647
                return $this->children[$index];
1✔
648
        }
649

650

651
        /**
652
         * Exists child node? (\ArrayAccess implementation).
653
         * @param  int  $index
654
         */
655
        final public function offsetExists($index): bool
656
        {
657
                return isset($this->children[$index]);
1✔
658
        }
659

660

661
        /**
662
         * Removes child node (\ArrayAccess implementation).
663
         * @param  int  $index
664
         */
665
        public function offsetUnset($index): void
666
        {
667
                if (isset($this->children[$index])) {
1✔
668
                        array_splice($this->children, $index, 1);
1✔
669
                }
670
        }
1✔
671

672

673
        /**
674
         * Returns children count.
675
         */
676
        final public function count(): int
677
        {
678
                return count($this->children);
1✔
679
        }
680

681

682
        /**
683
         * Removes all children.
684
         */
685
        public function removeChildren(): void
686
        {
687
                $this->children = [];
1✔
688
        }
1✔
689

690

691
        /**
692
         * Iterates over elements.
693
         * @return \ArrayIterator<int, HtmlStringable|string>
694
         */
695
        final public function getIterator(): \ArrayIterator
696
        {
697
                return new \ArrayIterator($this->children);
1✔
698
        }
699

700

701
        /**
702
         * Returns all children.
703
         * @return array<int, HtmlStringable|string>
704
         */
705
        final public function getChildren(): array
706
        {
707
                return $this->children;
1✔
708
        }
709

710

711
        /**
712
         * Renders element's start tag, content and end tag.
713
         */
714
        final public function render(?int $indent = null): string
1✔
715
        {
716
                $s = $this->startTag();
1✔
717

718
                if (!$this->isEmpty) {
1✔
719
                        // add content
720
                        if ($indent !== null) {
1✔
721
                                $indent++;
1✔
722
                        }
723

724
                        foreach ($this->children as $child) {
1✔
725
                                if ($child instanceof self) {
1✔
726
                                        $s .= $child->render($indent);
1✔
727
                                } else {
728
                                        $s .= $child;
1✔
729
                                }
730
                        }
731

732
                        // add end tag
733
                        $s .= $this->endTag();
1✔
734
                }
735

736
                if ($indent !== null) {
1✔
737
                        return "\n" . str_repeat("\t", $indent - 1) . $s . "\n" . str_repeat("\t", max(0, $indent - 2));
1✔
738
                }
739

740
                return $s;
1✔
741
        }
742

743

744
        final public function __toString(): string
745
        {
746
                return $this->render();
1✔
747
        }
748

749

750
        /**
751
         * Returns element's start tag.
752
         */
753
        final public function startTag(): string
754
        {
755
                return $this->name
1✔
756
                        ? '<' . $this->name . $this->attributes() . '>'
1✔
757
                        : '';
1✔
758
        }
759

760

761
        /**
762
         * Returns element's end tag.
763
         */
764
        final public function endTag(): string
765
        {
766
                return $this->name && !$this->isEmpty ? '</' . $this->name . '>' : '';
1✔
767
        }
768

769

770
        /**
771
         * Returns element's attributes.
772
         * @internal
773
         */
774
        final public function attributes(): string
775
        {
776
                $s = '';
1✔
777
                $attrs = $this->attrs;
1✔
778
                foreach ($attrs as $key => $value) {
1✔
779
                        if ($value === null || $value === false) {
1✔
780
                                continue;
1✔
781

782
                        } elseif ($value === true) {
1✔
783
                                $s .= ' ' . $key;
1✔
784

785
                                continue;
1✔
786

787
                        } elseif (is_array($value)) {
1✔
788
                                if (str_starts_with($key, 'data-')) {
1✔
789
                                        $value = Json::encode($value);
1✔
790

791
                                } else {
792
                                        $tmp = null;
1✔
793
                                        foreach ($value as $k => $v) {
1✔
794
                                                if ($v != null) { // intentionally ==, skip nulls & empty string
1✔
795
                                                        // composite 'style' vs. 'others'
796
                                                        $tmp[] = $v === true
1✔
797
                                                                ? $k
1✔
798
                                                                : (is_string($k) ? $k . ':' . $v : $v);
1✔
799
                                                }
800
                                        }
801

802
                                        if ($tmp === null) {
1✔
803
                                                continue;
1✔
804
                                        }
805

806
                                        $value = implode($key === 'style' || !strncmp($key, 'on', 2) ? ';' : ' ', $tmp);
1✔
807
                                }
808
                        } elseif (is_float($value)) {
1✔
809
                                $value = rtrim(rtrim(number_format($value, 10, '.', ''), '0'), '.');
1✔
810

811
                        } else {
812
                                $value = (string) $value;
1✔
813
                        }
814

815
                        $q = str_contains($value, '"') ? "'" : '"';
1✔
816
                        $s .= ' ' . $key . '=' . $q
1✔
817
                                . str_replace(
1✔
818
                                        ['&', $q, '<'],
1✔
819
                                        ['&amp;', $q === '"' ? '&quot;' : '&#39;', '<'],
1✔
820
                                        $value,
1✔
821
                                )
822
                                . (str_contains($value, '`') && strpbrk($value, ' <>"\'') === false ? ' ' : '')
1✔
823
                                . $q;
1✔
824
                }
825

826
                $s = str_replace('@', '&#64;', $s);
1✔
827
                return $s;
1✔
828
        }
829

830

831
        /**
832
         * Clones all children too.
833
         */
834
        public function __clone()
835
        {
836
                foreach ($this->children as $key => $value) {
1✔
837
                        if (is_object($value)) {
1✔
838
                                $this->children[$key] = clone $value;
1✔
839
                        }
840
                }
841
        }
1✔
842
}
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