Subversion-Projekte lars-tiefland.laravel_shop

Revision

Zur aktuellen Revision | Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 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;
13
use Nette\HtmlStringable;
14
use function is_array, is_float, is_object, is_string;
15
 
16
 
17
/**
18
 * HTML helper.
19
 *
20
 * @property string|null $accept
21
 * @property string|null $accesskey
22
 * @property string|null $action
23
 * @property string|null $align
24
 * @property string|null $allow
25
 * @property string|null $alt
26
 * @property bool|null   $async
27
 * @property string|null $autocapitalize
28
 * @property string|null $autocomplete
29
 * @property bool|null   $autofocus
30
 * @property bool|null   $autoplay
31
 * @property string|null $charset
32
 * @property bool|null   $checked
33
 * @property string|null $cite
34
 * @property string|null $class
35
 * @property int|null    $cols
36
 * @property int|null    $colspan
37
 * @property string|null $content
38
 * @property bool|null   $contenteditable
39
 * @property bool|null   $controls
40
 * @property string|null $coords
41
 * @property string|null $crossorigin
42
 * @property string|null $data
43
 * @property string|null $datetime
44
 * @property string|null $decoding
45
 * @property bool|null   $default
46
 * @property bool|null   $defer
47
 * @property string|null $dir
48
 * @property string|null $dirname
49
 * @property bool|null   $disabled
50
 * @property bool|null   $download
51
 * @property string|null $draggable
52
 * @property string|null $dropzone
53
 * @property string|null $enctype
54
 * @property string|null $for
55
 * @property string|null $form
56
 * @property string|null $formaction
57
 * @property string|null $formenctype
58
 * @property string|null $formmethod
59
 * @property bool|null   $formnovalidate
60
 * @property string|null $formtarget
61
 * @property string|null $headers
62
 * @property int|null    $height
63
 * @property bool|null   $hidden
64
 * @property float|null  $high
65
 * @property string|null $href
66
 * @property string|null $hreflang
67
 * @property string|null $id
68
 * @property string|null $integrity
69
 * @property string|null $inputmode
70
 * @property bool|null   $ismap
71
 * @property string|null $itemprop
72
 * @property string|null $kind
73
 * @property string|null $label
74
 * @property string|null $lang
75
 * @property string|null $list
76
 * @property bool|null   $loop
77
 * @property float|null  $low
78
 * @property float|null  $max
79
 * @property int|null    $maxlength
80
 * @property int|null    $minlength
81
 * @property string|null $media
82
 * @property string|null $method
83
 * @property float|null  $min
84
 * @property bool|null   $multiple
85
 * @property bool|null   $muted
86
 * @property string|null $name
87
 * @property bool|null   $novalidate
88
 * @property bool|null   $open
89
 * @property float|null  $optimum
90
 * @property string|null $pattern
91
 * @property string|null $ping
92
 * @property string|null $placeholder
93
 * @property string|null $poster
94
 * @property string|null $preload
95
 * @property string|null $radiogroup
96
 * @property bool|null   $readonly
97
 * @property string|null $rel
98
 * @property bool|null   $required
99
 * @property bool|null   $reversed
100
 * @property int|null    $rows
101
 * @property int|null    $rowspan
102
 * @property string|null $sandbox
103
 * @property string|null $scope
104
 * @property bool|null   $selected
105
 * @property string|null $shape
106
 * @property int|null    $size
107
 * @property string|null $sizes
108
 * @property string|null $slot
109
 * @property int|null    $span
110
 * @property string|null $spellcheck
111
 * @property string|null $src
112
 * @property string|null $srcdoc
113
 * @property string|null $srclang
114
 * @property string|null $srcset
115
 * @property int|null    $start
116
 * @property float|null  $step
117
 * @property string|null $style
118
 * @property int|null    $tabindex
119
 * @property string|null $target
120
 * @property string|null $title
121
 * @property string|null $translate
122
 * @property string|null $type
123
 * @property string|null $usemap
124
 * @property string|null $value
125
 * @property int|null    $width
126
 * @property string|null $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
class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringable
235
{
236
	use Nette\SmartObject;
237
 
238
	/** @var array<string, mixed>  element's attributes */
239
	public $attrs = [];
240
 
241
	/** @var bool  use XHTML syntax? */
242
	public static $xhtml = false;
243
 
244
	/** @var array<string, int>  void elements */
245
	public static $emptyElements = [
246
		'img' => 1, 'hr' => 1, 'br' => 1, 'input' => 1, 'meta' => 1, 'area' => 1, 'embed' => 1, 'keygen' => 1,
247
		'source' => 1, 'base' => 1, 'col' => 1, 'link' => 1, 'param' => 1, 'basefont' => 1, 'frame' => 1,
248
		'isindex' => 1, 'wbr' => 1, 'command' => 1, 'track' => 1,
249
	];
250
 
251
	/** @var array<int, HtmlStringable|string> nodes */
252
	protected $children = [];
253
 
254
	/** @var string  element's name */
255
	private $name;
256
 
257
	/** @var bool  is element empty? */
258
	private $isEmpty;
259
 
260
 
261
	/**
262
	 * Constructs new HTML element.
263
	 * @param  array|string $attrs element's attributes or plain text content
264
	 * @return static
265
	 */
266
	public static function el(?string $name = null, $attrs = null)
267
	{
268
		$el = new static;
269
		$parts = explode(' ', (string) $name, 2);
270
		$el->setName($parts[0]);
271
 
272
		if (is_array($attrs)) {
273
			$el->attrs = $attrs;
274
 
275
		} elseif ($attrs !== null) {
276
			$el->setText($attrs);
277
		}
278
 
279
		if (isset($parts[1])) {
280
			foreach (Strings::matchAll($parts[1] . ' ', '#([a-z0-9:-]+)(?:=(["\'])?(.*?)(?(2)\2|\s))?#i') as $m) {
281
				$el->attrs[$m[1]] = $m[3] ?? true;
282
			}
283
		}
284
 
285
		return $el;
286
	}
287
 
288
 
289
	/**
290
	 * Returns an object representing HTML text.
291
	 */
292
	public static function fromHtml(string $html): self
293
	{
294
		return (new static)->setHtml($html);
295
	}
296
 
297
 
298
	/**
299
	 * Returns an object representing plain text.
300
	 */
301
	public static function fromText(string $text): self
302
	{
303
		return (new static)->setText($text);
304
	}
305
 
306
 
307
	/**
308
	 * Converts to HTML.
309
	 */
310
	final public function toHtml(): string
311
	{
312
		return $this->render();
313
	}
314
 
315
 
316
	/**
317
	 * Converts to plain text.
318
	 */
319
	final public function toText(): string
320
	{
321
		return $this->getText();
322
	}
323
 
324
 
325
	/**
326
	 * Converts given HTML code to plain text.
327
	 */
328
	public static function htmlToText(string $html): string
329
	{
330
		return html_entity_decode(strip_tags($html), ENT_QUOTES | ENT_HTML5, 'UTF-8');
331
	}
332
 
333
 
334
	/**
335
	 * Changes element's name.
336
	 * @return static
337
	 */
338
	final public function setName(string $name, ?bool $isEmpty = null)
339
	{
340
		$this->name = $name;
341
		$this->isEmpty = $isEmpty ?? isset(static::$emptyElements[$name]);
342
		return $this;
343
	}
344
 
345
 
346
	/**
347
	 * Returns element's name.
348
	 */
349
	final public function getName(): string
350
	{
351
		return $this->name;
352
	}
353
 
354
 
355
	/**
356
	 * Is element empty?
357
	 */
358
	final public function isEmpty(): bool
359
	{
360
		return $this->isEmpty;
361
	}
362
 
363
 
364
	/**
365
	 * Sets multiple attributes.
366
	 * @return static
367
	 */
368
	public function addAttributes(array $attrs)
369
	{
370
		$this->attrs = array_merge($this->attrs, $attrs);
371
		return $this;
372
	}
373
 
374
 
375
	/**
376
	 * Appends value to element's attribute.
377
	 * @param  mixed  $value
378
	 * @param  mixed  $option
379
	 * @return static
380
	 */
381
	public function appendAttribute(string $name, $value, $option = true)
382
	{
383
		if (is_array($value)) {
384
			$prev = isset($this->attrs[$name]) ? (array) $this->attrs[$name] : [];
385
			$this->attrs[$name] = $value + $prev;
386
 
387
		} elseif ((string) $value === '') {
388
			$tmp = &$this->attrs[$name]; // appending empty value? -> ignore, but ensure it exists
389
 
390
		} elseif (!isset($this->attrs[$name]) || is_array($this->attrs[$name])) { // needs array
391
			$this->attrs[$name][$value] = $option;
392
 
393
		} else {
394
			$this->attrs[$name] = [$this->attrs[$name] => true, $value => $option];
395
		}
396
 
397
		return $this;
398
	}
399
 
400
 
401
	/**
402
	 * Sets element's attribute.
403
	 * @param  mixed  $value
404
	 * @return static
405
	 */
406
	public function setAttribute(string $name, $value)
407
	{
408
		$this->attrs[$name] = $value;
409
		return $this;
410
	}
411
 
412
 
413
	/**
414
	 * Returns element's attribute.
415
	 * @return mixed
416
	 */
417
	public function getAttribute(string $name)
418
	{
419
		return $this->attrs[$name] ?? null;
420
	}
421
 
422
 
423
	/**
424
	 * Unsets element's attribute.
425
	 * @return static
426
	 */
427
	public function removeAttribute(string $name)
428
	{
429
		unset($this->attrs[$name]);
430
		return $this;
431
	}
432
 
433
 
434
	/**
435
	 * Unsets element's attributes.
436
	 * @return static
437
	 */
438
	public function removeAttributes(array $attributes)
439
	{
440
		foreach ($attributes as $name) {
441
			unset($this->attrs[$name]);
442
		}
443
 
444
		return $this;
445
	}
446
 
447
 
448
	/**
449
	 * Overloaded setter for element's attribute.
450
	 * @param  mixed  $value
451
	 */
452
	final public function __set(string $name, $value): void
453
	{
454
		$this->attrs[$name] = $value;
455
	}
456
 
457
 
458
	/**
459
	 * Overloaded getter for element's attribute.
460
	 * @return mixed
461
	 */
462
	final public function &__get(string $name)
463
	{
464
		return $this->attrs[$name];
465
	}
466
 
467
 
468
	/**
469
	 * Overloaded tester for element's attribute.
470
	 */
471
	final public function __isset(string $name): bool
472
	{
473
		return isset($this->attrs[$name]);
474
	}
475
 
476
 
477
	/**
478
	 * Overloaded unsetter for element's attribute.
479
	 */
480
	final public function __unset(string $name): void
481
	{
482
		unset($this->attrs[$name]);
483
	}
484
 
485
 
486
	/**
487
	 * Overloaded setter for element's attribute.
488
	 * @return mixed
489
	 */
490
	final public function __call(string $m, array $args)
491
	{
492
		$p = substr($m, 0, 3);
493
		if ($p === 'get' || $p === 'set' || $p === 'add') {
494
			$m = substr($m, 3);
495
			$m[0] = $m[0] | "\x20";
496
			if ($p === 'get') {
497
				return $this->attrs[$m] ?? null;
498
 
499
			} elseif ($p === 'add') {
500
				$args[] = true;
501
			}
502
		}
503
 
504
		if (count($args) === 0) { // invalid
505
 
506
		} elseif (count($args) === 1) { // set
507
			$this->attrs[$m] = $args[0];
508
 
509
		} else { // add
510
			$this->appendAttribute($m, $args[0], $args[1]);
511
		}
512
 
513
		return $this;
514
	}
515
 
516
 
517
	/**
518
	 * Special setter for element's attribute.
519
	 * @return static
520
	 */
521
	final public function href(string $path, ?array $query = null)
522
	{
523
		if ($query) {
524
			$query = http_build_query($query, '', '&');
525
			if ($query !== '') {
526
				$path .= '?' . $query;
527
			}
528
		}
529
 
530
		$this->attrs['href'] = $path;
531
		return $this;
532
	}
533
 
534
 
535
	/**
536
	 * Setter for data-* attributes. Booleans are converted to 'true' resp. 'false'.
537
	 * @param  mixed  $value
538
	 * @return static
539
	 */
540
	public function data(string $name, $value = null)
541
	{
542
		if (func_num_args() === 1) {
543
			$this->attrs['data'] = $name;
544
		} else {
545
			$this->attrs["data-$name"] = is_bool($value)
546
				? json_encode($value)
547
				: $value;
548
		}
549
 
550
		return $this;
551
	}
552
 
553
 
554
	/**
555
	 * Sets element's HTML content.
556
	 * @param  HtmlStringable|string  $html
557
	 * @return static
558
	 */
559
	final public function setHtml($html)
560
	{
561
		$this->children = [(string) $html];
562
		return $this;
563
	}
564
 
565
 
566
	/**
567
	 * Returns element's HTML content.
568
	 */
569
	final public function getHtml(): string
570
	{
571
		return implode('', $this->children);
572
	}
573
 
574
 
575
	/**
576
	 * Sets element's textual content.
577
	 * @param  HtmlStringable|string|int|float  $text
578
	 * @return static
579
	 */
580
	final public function setText($text)
581
	{
582
		if (!$text instanceof HtmlStringable) {
583
			$text = htmlspecialchars((string) $text, ENT_NOQUOTES, 'UTF-8');
584
		}
585
 
586
		$this->children = [(string) $text];
587
		return $this;
588
	}
589
 
590
 
591
	/**
592
	 * Returns element's textual content.
593
	 */
594
	final public function getText(): string
595
	{
596
		return self::htmlToText($this->getHtml());
597
	}
598
 
599
 
600
	/**
601
	 * Adds new element's child.
602
	 * @param  HtmlStringable|string  $child  Html node or raw HTML string
603
	 * @return static
604
	 */
605
	final public function addHtml($child)
606
	{
607
		return $this->insert(null, $child);
608
	}
609
 
610
 
611
	/**
612
	 * Appends plain-text string to element content.
613
	 * @param  HtmlStringable|string|int|float  $text
614
	 * @return static
615
	 */
616
	public function addText($text)
617
	{
618
		if (!$text instanceof HtmlStringable) {
619
			$text = htmlspecialchars((string) $text, ENT_NOQUOTES, 'UTF-8');
620
		}
621
 
622
		return $this->insert(null, $text);
623
	}
624
 
625
 
626
	/**
627
	 * Creates and adds a new Html child.
628
	 * @param  array|string $attrs  element's attributes or raw HTML string
629
	 * @return static  created element
630
	 */
631
	final public function create(string $name, $attrs = null)
632
	{
633
		$this->insert(null, $child = static::el($name, $attrs));
634
		return $child;
635
	}
636
 
637
 
638
	/**
639
	 * Inserts child node.
640
	 * @param  HtmlStringable|string $child Html node or raw HTML string
641
	 * @return static
642
	 */
643
	public function insert(?int $index, $child, bool $replace = false)
644
	{
645
		$child = $child instanceof self ? $child : (string) $child;
646
		if ($index === null) { // append
647
			$this->children[] = $child;
648
 
649
		} else { // insert or replace
650
			array_splice($this->children, $index, $replace ? 1 : 0, [$child]);
651
		}
652
 
653
		return $this;
654
	}
655
 
656
 
657
	/**
658
	 * Inserts (replaces) child node (\ArrayAccess implementation).
659
	 * @param  int|null  $index  position or null for appending
660
	 * @param  Html|string  $child  Html node or raw HTML string
661
	 */
662
	final public function offsetSet($index, $child): void
663
	{
664
		$this->insert($index, $child, true);
665
	}
666
 
667
 
668
	/**
669
	 * Returns child node (\ArrayAccess implementation).
670
	 * @param  int  $index
671
	 * @return HtmlStringable|string
672
	 */
673
	#[\ReturnTypeWillChange]
674
	final public function offsetGet($index)
675
	{
676
		return $this->children[$index];
677
	}
678
 
679
 
680
	/**
681
	 * Exists child node? (\ArrayAccess implementation).
682
	 * @param  int  $index
683
	 */
684
	final public function offsetExists($index): bool
685
	{
686
		return isset($this->children[$index]);
687
	}
688
 
689
 
690
	/**
691
	 * Removes child node (\ArrayAccess implementation).
692
	 * @param  int  $index
693
	 */
694
	public function offsetUnset($index): void
695
	{
696
		if (isset($this->children[$index])) {
697
			array_splice($this->children, $index, 1);
698
		}
699
	}
700
 
701
 
702
	/**
703
	 * Returns children count.
704
	 */
705
	final public function count(): int
706
	{
707
		return count($this->children);
708
	}
709
 
710
 
711
	/**
712
	 * Removes all children.
713
	 */
714
	public function removeChildren(): void
715
	{
716
		$this->children = [];
717
	}
718
 
719
 
720
	/**
721
	 * Iterates over elements.
722
	 * @return \ArrayIterator<int, HtmlStringable|string>
723
	 */
724
	final public function getIterator(): \ArrayIterator
725
	{
726
		return new \ArrayIterator($this->children);
727
	}
728
 
729
 
730
	/**
731
	 * Returns all children.
732
	 */
733
	final public function getChildren(): array
734
	{
735
		return $this->children;
736
	}
737
 
738
 
739
	/**
740
	 * Renders element's start tag, content and end tag.
741
	 */
742
	final public function render(?int $indent = null): string
743
	{
744
		$s = $this->startTag();
745
 
746
		if (!$this->isEmpty) {
747
			// add content
748
			if ($indent !== null) {
749
				$indent++;
750
			}
751
 
752
			foreach ($this->children as $child) {
753
				if ($child instanceof self) {
754
					$s .= $child->render($indent);
755
				} else {
756
					$s .= $child;
757
				}
758
			}
759
 
760
			// add end tag
761
			$s .= $this->endTag();
762
		}
763
 
764
		if ($indent !== null) {
765
			return "\n" . str_repeat("\t", $indent - 1) . $s . "\n" . str_repeat("\t", max(0, $indent - 2));
766
		}
767
 
768
		return $s;
769
	}
770
 
771
 
772
	final public function __toString(): string
773
	{
774
		try {
775
			return $this->render();
776
		} catch (\Throwable $e) {
777
			if (PHP_VERSION_ID >= 70400) {
778
				throw $e;
779
			}
780
 
781
			trigger_error('Exception in ' . __METHOD__ . "(): {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", E_USER_ERROR);
782
			return '';
783
		}
784
	}
785
 
786
 
787
	/**
788
	 * Returns element's start tag.
789
	 */
790
	final public function startTag(): string
791
	{
792
		return $this->name
793
			? '<' . $this->name . $this->attributes() . (static::$xhtml && $this->isEmpty ? ' />' : '>')
794
			: '';
795
	}
796
 
797
 
798
	/**
799
	 * Returns element's end tag.
800
	 */
801
	final public function endTag(): string
802
	{
803
		return $this->name && !$this->isEmpty ? '</' . $this->name . '>' : '';
804
	}
805
 
806
 
807
	/**
808
	 * Returns element's attributes.
809
	 * @internal
810
	 */
811
	final public function attributes(): string
812
	{
813
		if (!is_array($this->attrs)) {
814
			return '';
815
		}
816
 
817
		$s = '';
818
		$attrs = $this->attrs;
819
		foreach ($attrs as $key => $value) {
820
			if ($value === null || $value === false) {
821
				continue;
822
 
823
			} elseif ($value === true) {
824
				if (static::$xhtml) {
825
					$s .= ' ' . $key . '="' . $key . '"';
826
				} else {
827
					$s .= ' ' . $key;
828
				}
829
 
830
				continue;
831
 
832
			} elseif (is_array($value)) {
833
				if (strncmp($key, 'data-', 5) === 0) {
834
					$value = Json::encode($value);
835
 
836
				} else {
837
					$tmp = null;
838
					foreach ($value as $k => $v) {
839
						if ($v != null) { // intentionally ==, skip nulls & empty string
840
							// composite 'style' vs. 'others'
841
							$tmp[] = $v === true
842
								? $k
843
								: (is_string($k) ? $k . ':' . $v : $v);
844
						}
845
					}
846
 
847
					if ($tmp === null) {
848
						continue;
849
					}
850
 
851
					$value = implode($key === 'style' || !strncmp($key, 'on', 2) ? ';' : ' ', $tmp);
852
				}
853
			} elseif (is_float($value)) {
854
				$value = rtrim(rtrim(number_format($value, 10, '.', ''), '0'), '.');
855
 
856
			} else {
857
				$value = (string) $value;
858
			}
859
 
860
			$q = strpos($value, '"') === false ? '"' : "'";
861
			$s .= ' ' . $key . '=' . $q
862
				. str_replace(
863
					['&', $q, '<'],
864
					['&amp;', $q === '"' ? '&quot;' : '&#39;', self::$xhtml ? '&lt;' : '<'],
865
					$value
866
				)
867
				. (strpos($value, '`') !== false && strpbrk($value, ' <>"\'') === false ? ' ' : '')
868
				. $q;
869
		}
870
 
871
		$s = str_replace('@', '&#64;', $s);
872
		return $s;
873
	}
874
 
875
 
876
	/**
877
	 * Clones all children too.
878
	 */
879
	public function __clone()
880
	{
881
		foreach ($this->children as $key => $value) {
882
			if (is_object($value)) {
883
				$this->children[$key] = clone $value;
884
			}
885
		}
886
	}
887
}