PluginProbe
Packeta / trunk
Packeta vtrunk
2.3.2 2.3.1 trunk 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3.0 1.3.1 1.3.2 1.4 1.4.1 1.4.2 1.4.3 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 All 56 releases
packeta / deps / nette / utils / src / Utils / Html.php

Html.php in Packeta trunk, at deps/nette/utils/src/Utils/Html.php

745 lines 22.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 declare (strict_types=1);
8 namespace Packetery\Nette\Utils;
9
10 use Packetery\Nette;
11 use Packetery\Nette\HtmlStringable;
12 use function is_array, is_float, is_object, is_string;
13 /**
14 * HTML helper.
15 *
16 * @property string|null $accept
17 * @property string|null $accesskey
18 * @property string|null $action
19 * @property string|null $align
20 * @property string|null $allow
21 * @property string|null $alt
22 * @property bool|null $async
23 * @property string|null $autocapitalize
24 * @property string|null $autocomplete
25 * @property bool|null $autofocus
26 * @property bool|null $autoplay
27 * @property string|null $charset
28 * @property bool|null $checked
29 * @property string|null $cite
30 * @property string|null $class
31 * @property int|null $cols
32 * @property int|null $colspan
33 * @property string|null $content
34 * @property bool|null $contenteditable
35 * @property bool|null $controls
36 * @property string|null $coords
37 * @property string|null $crossorigin
38 * @property string|null $data
39 * @property string|null $datetime
40 * @property string|null $decoding
41 * @property bool|null $default
42 * @property bool|null $defer
43 * @property string|null $dir
44 * @property string|null $dirname
45 * @property bool|null $disabled
46 * @property bool|null $download
47 * @property string|null $draggable
48 * @property string|null $dropzone
49 * @property string|null $enctype
50 * @property string|null $for
51 * @property string|null $form
52 * @property string|null $formaction
53 * @property string|null $formenctype
54 * @property string|null $formmethod
55 * @property bool|null $formnovalidate
56 * @property string|null $formtarget
57 * @property string|null $headers
58 * @property int|null $height
59 * @property bool|null $hidden
60 * @property float|null $high
61 * @property string|null $href
62 * @property string|null $hreflang
63 * @property string|null $id
64 * @property string|null $integrity
65 * @property string|null $inputmode
66 * @property bool|null $ismap
67 * @property string|null $itemprop
68 * @property string|null $kind
69 * @property string|null $label
70 * @property string|null $lang
71 * @property string|null $list
72 * @property bool|null $loop
73 * @property float|null $low
74 * @property float|null $max
75 * @property int|null $maxlength
76 * @property int|null $minlength
77 * @property string|null $media
78 * @property string|null $method
79 * @property float|null $min
80 * @property bool|null $multiple
81 * @property bool|null $muted
82 * @property string|null $name
83 * @property bool|null $novalidate
84 * @property bool|null $open
85 * @property float|null $optimum
86 * @property string|null $pattern
87 * @property string|null $ping
88 * @property string|null $placeholder
89 * @property string|null $poster
90 * @property string|null $preload
91 * @property string|null $radiogroup
92 * @property bool|null $readonly
93 * @property string|null $rel
94 * @property bool|null $required
95 * @property bool|null $reversed
96 * @property int|null $rows
97 * @property int|null $rowspan
98 * @property string|null $sandbox
99 * @property string|null $scope
100 * @property bool|null $selected
101 * @property string|null $shape
102 * @property int|null $size
103 * @property string|null $sizes
104 * @property string|null $slot
105 * @property int|null $span
106 * @property string|null $spellcheck
107 * @property string|null $src
108 * @property string|null $srcdoc
109 * @property string|null $srclang
110 * @property string|null $srcset
111 * @property int|null $start
112 * @property float|null $step
113 * @property string|null $style
114 * @property int|null $tabindex
115 * @property string|null $target
116 * @property string|null $title
117 * @property string|null $translate
118 * @property string|null $type
119 * @property string|null $usemap
120 * @property string|null $value
121 * @property int|null $width
122 * @property string|null $wrap
123 *
124 * @method self accept(?string $val)
125 * @method self accesskey(?string $val, bool $state = null)
126 * @method self action(?string $val)
127 * @method self align(?string $val)
128 * @method self allow(?string $val, bool $state = null)
129 * @method self alt(?string $val)
130 * @method self async(?bool $val)
131 * @method self autocapitalize(?string $val)
132 * @method self autocomplete(?string $val)
133 * @method self autofocus(?bool $val)
134 * @method self autoplay(?bool $val)
135 * @method self charset(?string $val)
136 * @method self checked(?bool $val)
137 * @method self cite(?string $val)
138 * @method self class(?string $val, bool $state = null)
139 * @method self cols(?int $val)
140 * @method self colspan(?int $val)
141 * @method self content(?string $val)
142 * @method self contenteditable(?bool $val)
143 * @method self controls(?bool $val)
144 * @method self coords(?string $val)
145 * @method self crossorigin(?string $val)
146 * @method self datetime(?string $val)
147 * @method self decoding(?string $val)
148 * @method self default(?bool $val)
149 * @method self defer(?bool $val)
150 * @method self dir(?string $val)
151 * @method self dirname(?string $val)
152 * @method self disabled(?bool $val)
153 * @method self download(?bool $val)
154 * @method self draggable(?string $val)
155 * @method self dropzone(?string $val)
156 * @method self enctype(?string $val)
157 * @method self for(?string $val)
158 * @method self form(?string $val)
159 * @method self formaction(?string $val)
160 * @method self formenctype(?string $val)
161 * @method self formmethod(?string $val)
162 * @method self formnovalidate(?bool $val)
163 * @method self formtarget(?string $val)
164 * @method self headers(?string $val, bool $state = null)
165 * @method self height(?int $val)
166 * @method self hidden(?bool $val)
167 * @method self high(?float $val)
168 * @method self hreflang(?string $val)
169 * @method self id(?string $val)
170 * @method self integrity(?string $val)
171 * @method self inputmode(?string $val)
172 * @method self ismap(?bool $val)
173 * @method self itemprop(?string $val)
174 * @method self kind(?string $val)
175 * @method self label(?string $val)
176 * @method self lang(?string $val)
177 * @method self list(?string $val)
178 * @method self loop(?bool $val)
179 * @method self low(?float $val)
180 * @method self max(?float $val)
181 * @method self maxlength(?int $val)
182 * @method self minlength(?int $val)
183 * @method self media(?string $val)
184 * @method self method(?string $val)
185 * @method self min(?float $val)
186 * @method self multiple(?bool $val)
187 * @method self muted(?bool $val)
188 * @method self name(?string $val)
189 * @method self novalidate(?bool $val)
190 * @method self open(?bool $val)
191 * @method self optimum(?float $val)
192 * @method self pattern(?string $val)
193 * @method self ping(?string $val, bool $state = null)
194 * @method self placeholder(?string $val)
195 * @method self poster(?string $val)
196 * @method self preload(?string $val)
197 * @method self radiogroup(?string $val)
198 * @method self readonly(?bool $val)
199 * @method self rel(?string $val)
200 * @method self required(?bool $val)
201 * @method self reversed(?bool $val)
202 * @method self rows(?int $val)
203 * @method self rowspan(?int $val)
204 * @method self sandbox(?string $val, bool $state = null)
205 * @method self scope(?string $val)
206 * @method self selected(?bool $val)
207 * @method self shape(?string $val)
208 * @method self size(?int $val)
209 * @method self sizes(?string $val)
210 * @method self slot(?string $val)
211 * @method self span(?int $val)
212 * @method self spellcheck(?string $val)
213 * @method self src(?string $val)
214 * @method self srcdoc(?string $val)
215 * @method self srclang(?string $val)
216 * @method self srcset(?string $val)
217 * @method self start(?int $val)
218 * @method self step(?float $val)
219 * @method self style(?string $property, string $val = null)
220 * @method self tabindex(?int $val)
221 * @method self target(?string $val)
222 * @method self title(?string $val)
223 * @method self translate(?string $val)
224 * @method self type(?string $val)
225 * @method self usemap(?string $val)
226 * @method self value(?string $val)
227 * @method self width(?int $val)
228 * @method self wrap(?string $val)
229 */
230 class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringable
231 {
232 use \Packetery\Nette\SmartObject;
233 /** @var array<string, mixed> element's attributes */
234 public $attrs = [];
235 /** @var bool use XHTML syntax? */
236 public static $xhtml = \false;
237 /** @var array<string, int> void elements */
238 public static $emptyElements = ['img' => 1, 'hr' => 1, 'br' => 1, 'input' => 1, 'meta' => 1, 'area' => 1, 'embed' => 1, 'keygen' => 1, 'source' => 1, 'base' => 1, 'col' => 1, 'link' => 1, 'param' => 1, 'basefont' => 1, 'frame' => 1, 'isindex' => 1, 'wbr' => 1, 'command' => 1, 'track' => 1];
239 /** @var array<int, HtmlStringable|string> nodes */
240 protected $children = [];
241 /** @var string element's name */
242 private $name;
243 /** @var bool is element empty? */
244 private $isEmpty;
245 /**
246 * Constructs new HTML element.
247 * @param array|string $attrs element's attributes or plain text content
248 * @return static
249 */
250 public static function el(?string $name = null, $attrs = null)
251 {
252 $el = new static();
253 $parts = \explode(' ', (string) $name, 2);
254 $el->setName($parts[0]);
255 if (is_array($attrs)) {
256 $el->attrs = $attrs;
257 } elseif ($attrs !== null) {
258 $el->setText($attrs);
259 }
260 if (isset($parts[1])) {
261 foreach (Strings::matchAll($parts[1] . ' ', '#([a-z0-9:-]+)(?:=(["\'])?(.*?)(?(2)\\2|\\s))?#i') as $m) {
262 $el->attrs[$m[1]] = $m[3] ?? \true;
263 }
264 }
265 return $el;
266 }
267 /**
268 * Returns an object representing HTML text.
269 */
270 public static function fromHtml(string $html) : self
271 {
272 return (new static())->setHtml($html);
273 }
274 /**
275 * Returns an object representing plain text.
276 */
277 public static function fromText(string $text) : self
278 {
279 return (new static())->setText($text);
280 }
281 /**
282 * Converts to HTML.
283 */
284 public final function toHtml() : string
285 {
286 return $this->render();
287 }
288 /**
289 * Converts to plain text.
290 */
291 public final function toText() : string
292 {
293 return $this->getText();
294 }
295 /**
296 * Converts given HTML code to plain text.
297 */
298 public static function htmlToText(string $html) : string
299 {
300 return \html_entity_decode(\strip_tags($html), \ENT_QUOTES | \ENT_HTML5, 'UTF-8');
301 }
302 /**
303 * Changes element's name.
304 * @return static
305 */
306 public final function setName(string $name, ?bool $isEmpty = null)
307 {
308 $this->name = $name;
309 $this->isEmpty = $isEmpty ?? isset(static::$emptyElements[$name]);
310 return $this;
311 }
312 /**
313 * Returns element's name.
314 */
315 public final function getName() : string
316 {
317 return $this->name;
318 }
319 /**
320 * Is element empty?
321 */
322 public final function isEmpty() : bool
323 {
324 return $this->isEmpty;
325 }
326 /**
327 * Sets multiple attributes.
328 * @return static
329 */
330 public function addAttributes(array $attrs)
331 {
332 $this->attrs = \array_merge($this->attrs, $attrs);
333 return $this;
334 }
335 /**
336 * Appends value to element's attribute.
337 * @param mixed $value
338 * @param mixed $option
339 * @return static
340 */
341 public function appendAttribute(string $name, $value, $option = \true)
342 {
343 if (is_array($value)) {
344 $prev = isset($this->attrs[$name]) ? (array) $this->attrs[$name] : [];
345 $this->attrs[$name] = $value + $prev;
346 } elseif ((string) $value === '') {
347 $tmp =& $this->attrs[$name];
348 // appending empty value? -> ignore, but ensure it exists
349 } elseif (!isset($this->attrs[$name]) || is_array($this->attrs[$name])) {
350 // needs array
351 $this->attrs[$name][$value] = $option;
352 } else {
353 $this->attrs[$name] = [$this->attrs[$name] => \true, $value => $option];
354 }
355 return $this;
356 }
357 /**
358 * Sets element's attribute.
359 * @param mixed $value
360 * @return static
361 */
362 public function setAttribute(string $name, $value)
363 {
364 $this->attrs[$name] = $value;
365 return $this;
366 }
367 /**
368 * Returns element's attribute.
369 * @return mixed
370 */
371 public function getAttribute(string $name)
372 {
373 return $this->attrs[$name] ?? null;
374 }
375 /**
376 * Unsets element's attribute.
377 * @return static
378 */
379 public function removeAttribute(string $name)
380 {
381 unset($this->attrs[$name]);
382 return $this;
383 }
384 /**
385 * Unsets element's attributes.
386 * @return static
387 */
388 public function removeAttributes(array $attributes)
389 {
390 foreach ($attributes as $name) {
391 unset($this->attrs[$name]);
392 }
393 return $this;
394 }
395 /**
396 * Overloaded setter for element's attribute.
397 * @param mixed $value
398 */
399 public final function __set(string $name, $value) : void
400 {
401 $this->attrs[$name] = $value;
402 }
403 /**
404 * Overloaded getter for element's attribute.
405 * @return mixed
406 */
407 public final function &__get(string $name)
408 {
409 return $this->attrs[$name];
410 }
411 /**
412 * Overloaded tester for element's attribute.
413 */
414 public final function __isset(string $name) : bool
415 {
416 return isset($this->attrs[$name]);
417 }
418 /**
419 * Overloaded unsetter for element's attribute.
420 */
421 public final function __unset(string $name) : void
422 {
423 unset($this->attrs[$name]);
424 }
425 /**
426 * Overloaded setter for element's attribute.
427 * @return mixed
428 */
429 public final function __call(string $m, array $args)
430 {
431 $p = \substr($m, 0, 3);
432 if ($p === 'get' || $p === 'set' || $p === 'add') {
433 $m = \substr($m, 3);
434 $m[0] = $m[0] | " ";
435 if ($p === 'get') {
436 return $this->attrs[$m] ?? null;
437 } elseif ($p === 'add') {
438 $args[] = \true;
439 }
440 }
441 if (\count($args) === 0) {
442 // invalid
443 } elseif (\count($args) === 1) {
444 // set
445 $this->attrs[$m] = $args[0];
446 } else {
447 // add
448 $this->appendAttribute($m, $args[0], $args[1]);
449 }
450 return $this;
451 }
452 /**
453 * Special setter for element's attribute.
454 * @return static
455 */
456 public final function href(string $path, ?array $query = null)
457 {
458 if ($query) {
459 $query = \http_build_query($query, '', '&');
460 if ($query !== '') {
461 $path .= '?' . $query;
462 }
463 }
464 $this->attrs['href'] = $path;
465 return $this;
466 }
467 /**
468 * Setter for data-* attributes. Booleans are converted to 'true' resp. 'false'.
469 * @param mixed $value
470 * @return static
471 */
472 public function data(string $name, $value = null)
473 {
474 if (\func_num_args() === 1) {
475 $this->attrs['data'] = $name;
476 } else {
477 $this->attrs["data-{$name}"] = \is_bool($value) ? \json_encode($value) : $value;
478 }
479 return $this;
480 }
481 /**
482 * Sets element's HTML content.
483 * @param HtmlStringable|string $html
484 * @return static
485 */
486 public final function setHtml($html)
487 {
488 $this->children = [(string) $html];
489 return $this;
490 }
491 /**
492 * Returns element's HTML content.
493 */
494 public final function getHtml() : string
495 {
496 return \implode('', $this->children);
497 }
498 /**
499 * Sets element's textual content.
500 * @param HtmlStringable|string|int|float $text
501 * @return static
502 */
503 public final function setText($text)
504 {
505 if (!$text instanceof HtmlStringable) {
506 $text = \htmlspecialchars((string) $text, \ENT_NOQUOTES, 'UTF-8');
507 }
508 $this->children = [(string) $text];
509 return $this;
510 }
511 /**
512 * Returns element's textual content.
513 */
514 public final function getText() : string
515 {
516 return self::htmlToText($this->getHtml());
517 }
518 /**
519 * Adds new element's child.
520 * @param HtmlStringable|string $child Html node or raw HTML string
521 * @return static
522 */
523 public final function addHtml($child)
524 {
525 return $this->insert(null, $child);
526 }
527 /**
528 * Appends plain-text string to element content.
529 * @param HtmlStringable|string|int|float $text
530 * @return static
531 */
532 public function addText($text)
533 {
534 if (!$text instanceof HtmlStringable) {
535 $text = \htmlspecialchars((string) $text, \ENT_NOQUOTES, 'UTF-8');
536 }
537 return $this->insert(null, $text);
538 }
539 /**
540 * Creates and adds a new Html child.
541 * @param array|string $attrs element's attributes or raw HTML string
542 * @return static created element
543 */
544 public final function create(string $name, $attrs = null)
545 {
546 $this->insert(null, $child = static::el($name, $attrs));
547 return $child;
548 }
549 /**
550 * Inserts child node.
551 * @param HtmlStringable|string $child Html node or raw HTML string
552 * @return static
553 */
554 public function insert(?int $index, $child, bool $replace = \false)
555 {
556 $child = $child instanceof self ? $child : (string) $child;
557 if ($index === null) {
558 // append
559 $this->children[] = $child;
560 } else {
561 // insert or replace
562 \array_splice($this->children, $index, $replace ? 1 : 0, [$child]);
563 }
564 return $this;
565 }
566 /**
567 * Inserts (replaces) child node (\ArrayAccess implementation).
568 * @param int|null $index position or null for appending
569 * @param Html|string $child Html node or raw HTML string
570 */
571 public final function offsetSet($index, $child) : void
572 {
573 $this->insert($index, $child, \true);
574 }
575 /**
576 * Returns child node (\ArrayAccess implementation).
577 * @param int $index
578 * @return HtmlStringable|string
579 */
580 #[\ReturnTypeWillChange]
581 public final function offsetGet($index)
582 {
583 return $this->children[$index];
584 }
585 /**
586 * Exists child node? (\ArrayAccess implementation).
587 * @param int $index
588 */
589 public final function offsetExists($index) : bool
590 {
591 return isset($this->children[$index]);
592 }
593 /**
594 * Removes child node (\ArrayAccess implementation).
595 * @param int $index
596 */
597 public function offsetUnset($index) : void
598 {
599 if (isset($this->children[$index])) {
600 \array_splice($this->children, $index, 1);
601 }
602 }
603 /**
604 * Returns children count.
605 */
606 public final function count() : int
607 {
608 return \count($this->children);
609 }
610 /**
611 * Removes all children.
612 */
613 public function removeChildren() : void
614 {
615 $this->children = [];
616 }
617 /**
618 * Iterates over elements.
619 * @return \ArrayIterator<int, HtmlStringable|string>
620 */
621 public final function getIterator() : \ArrayIterator
622 {
623 return new \ArrayIterator($this->children);
624 }
625 /**
626 * Returns all children.
627 */
628 public final function getChildren() : array
629 {
630 return $this->children;
631 }
632 /**
633 * Renders element's start tag, content and end tag.
634 */
635 public final function render(?int $indent = null) : string
636 {
637 $s = $this->startTag();
638 if (!$this->isEmpty) {
639 // add content
640 if ($indent !== null) {
641 $indent++;
642 }
643 foreach ($this->children as $child) {
644 if ($child instanceof self) {
645 $s .= $child->render($indent);
646 } else {
647 $s .= $child;
648 }
649 }
650 // add end tag
651 $s .= $this->endTag();
652 }
653 if ($indent !== null) {
654 return "\n" . \str_repeat("\t", $indent - 1) . $s . "\n" . \str_repeat("\t", \max(0, $indent - 2));
655 }
656 return $s;
657 }
658 public final function __toString() : string
659 {
660 try {
661 return $this->render();
662 } catch (\Throwable $e) {
663 if (\PHP_VERSION_ID >= 70400) {
664 throw $e;
665 }
666 \trigger_error('Exception in ' . __METHOD__ . "(): {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", \E_USER_ERROR);
667 return '';
668 }
669 }
670 /**
671 * Returns element's start tag.
672 */
673 public final function startTag() : string
674 {
675 return $this->name ? '<' . $this->name . $this->attributes() . (static::$xhtml && $this->isEmpty ? ' />' : '>') : '';
676 }
677 /**
678 * Returns element's end tag.
679 */
680 public final function endTag() : string
681 {
682 return $this->name && !$this->isEmpty ? '</' . $this->name . '>' : '';
683 }
684 /**
685 * Returns element's attributes.
686 * @internal
687 */
688 public final function attributes() : string
689 {
690 if (!is_array($this->attrs)) {
691 return '';
692 }
693 $s = '';
694 $attrs = $this->attrs;
695 foreach ($attrs as $key => $value) {
696 if ($value === null || $value === \false) {
697 continue;
698 } elseif ($value === \true) {
699 if (static::$xhtml) {
700 $s .= ' ' . $key . '="' . $key . '"';
701 } else {
702 $s .= ' ' . $key;
703 }
704 continue;
705 } elseif (is_array($value)) {
706 if (\strncmp($key, 'data-', 5) === 0) {
707 $value = Json::encode($value);
708 } else {
709 $tmp = null;
710 foreach ($value as $k => $v) {
711 if ($v != null) {
712 // intentionally ==, skip nulls & empty string
713 // composite 'style' vs. 'others'
714 $tmp[] = $v === \true ? $k : (is_string($k) ? $k . ':' . $v : $v);
715 }
716 }
717 if ($tmp === null) {
718 continue;
719 }
720 $value = \implode($key === 'style' || !\strncmp($key, 'on', 2) ? ';' : ' ', $tmp);
721 }
722 } elseif (is_float($value)) {
723 $value = \rtrim(\rtrim(\number_format($value, 10, '.', ''), '0'), '.');
724 } else {
725 $value = (string) $value;
726 }
727 $q = \strpos($value, '"') === \false ? '"' : "'";
728 $s .= ' ' . $key . '=' . $q . \str_replace(['&', $q, '<'], ['&amp;', $q === '"' ? '&quot;' : '&#39;', self::$xhtml ? '&lt;' : '<'], $value) . (\strpos($value, '`') !== \false && \strpbrk($value, ' <>"\'') === \false ? ' ' : '') . $q;
729 }
730 $s = \str_replace('@', '&#64;', $s);
731 return $s;
732 }
733 /**
734 * Clones all children too.
735 */
736 public function __clone()
737 {
738 foreach ($this->children as $key => $value) {
739 if (is_object($value)) {
740 $this->children[$key] = clone $value;
741 }
742 }
743 }
744 }
745