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