Zur aktuellen Revision | Blame | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed
<?php/*** This file is part of the Nette Framework (https://nette.org)* Copyright (c) 2004 David Grudl (https://davidgrudl.com)*/declare(strict_types=1);namespace Nette\Utils;use Nette;use Nette\HtmlStringable;use function is_array, is_float, is_object, is_string;/*** HTML helper.** @property string|null $accept* @property string|null $accesskey* @property string|null $action* @property string|null $align* @property string|null $allow* @property string|null $alt* @property bool|null $async* @property string|null $autocapitalize* @property string|null $autocomplete* @property bool|null $autofocus* @property bool|null $autoplay* @property string|null $charset* @property bool|null $checked* @property string|null $cite* @property string|null $class* @property int|null $cols* @property int|null $colspan* @property string|null $content* @property bool|null $contenteditable* @property bool|null $controls* @property string|null $coords* @property string|null $crossorigin* @property string|null $data* @property string|null $datetime* @property string|null $decoding* @property bool|null $default* @property bool|null $defer* @property string|null $dir* @property string|null $dirname* @property bool|null $disabled* @property bool|null $download* @property string|null $draggable* @property string|null $dropzone* @property string|null $enctype* @property string|null $for* @property string|null $form* @property string|null $formaction* @property string|null $formenctype* @property string|null $formmethod* @property bool|null $formnovalidate* @property string|null $formtarget* @property string|null $headers* @property int|null $height* @property bool|null $hidden* @property float|null $high* @property string|null $href* @property string|null $hreflang* @property string|null $id* @property string|null $integrity* @property string|null $inputmode* @property bool|null $ismap* @property string|null $itemprop* @property string|null $kind* @property string|null $label* @property string|null $lang* @property string|null $list* @property bool|null $loop* @property float|null $low* @property float|null $max* @property int|null $maxlength* @property int|null $minlength* @property string|null $media* @property string|null $method* @property float|null $min* @property bool|null $multiple* @property bool|null $muted* @property string|null $name* @property bool|null $novalidate* @property bool|null $open* @property float|null $optimum* @property string|null $pattern* @property string|null $ping* @property string|null $placeholder* @property string|null $poster* @property string|null $preload* @property string|null $radiogroup* @property bool|null $readonly* @property string|null $rel* @property bool|null $required* @property bool|null $reversed* @property int|null $rows* @property int|null $rowspan* @property string|null $sandbox* @property string|null $scope* @property bool|null $selected* @property string|null $shape* @property int|null $size* @property string|null $sizes* @property string|null $slot* @property int|null $span* @property string|null $spellcheck* @property string|null $src* @property string|null $srcdoc* @property string|null $srclang* @property string|null $srcset* @property int|null $start* @property float|null $step* @property string|null $style* @property int|null $tabindex* @property string|null $target* @property string|null $title* @property string|null $translate* @property string|null $type* @property string|null $usemap* @property string|null $value* @property int|null $width* @property string|null $wrap** @method self accept(?string $val)* @method self accesskey(?string $val, bool $state = null)* @method self action(?string $val)* @method self align(?string $val)* @method self allow(?string $val, bool $state = null)* @method self alt(?string $val)* @method self async(?bool $val)* @method self autocapitalize(?string $val)* @method self autocomplete(?string $val)* @method self autofocus(?bool $val)* @method self autoplay(?bool $val)* @method self charset(?string $val)* @method self checked(?bool $val)* @method self cite(?string $val)* @method self class(?string $val, bool $state = null)* @method self cols(?int $val)* @method self colspan(?int $val)* @method self content(?string $val)* @method self contenteditable(?bool $val)* @method self controls(?bool $val)* @method self coords(?string $val)* @method self crossorigin(?string $val)* @method self datetime(?string $val)* @method self decoding(?string $val)* @method self default(?bool $val)* @method self defer(?bool $val)* @method self dir(?string $val)* @method self dirname(?string $val)* @method self disabled(?bool $val)* @method self download(?bool $val)* @method self draggable(?string $val)* @method self dropzone(?string $val)* @method self enctype(?string $val)* @method self for(?string $val)* @method self form(?string $val)* @method self formaction(?string $val)* @method self formenctype(?string $val)* @method self formmethod(?string $val)* @method self formnovalidate(?bool $val)* @method self formtarget(?string $val)* @method self headers(?string $val, bool $state = null)* @method self height(?int $val)* @method self hidden(?bool $val)* @method self high(?float $val)* @method self hreflang(?string $val)* @method self id(?string $val)* @method self integrity(?string $val)* @method self inputmode(?string $val)* @method self ismap(?bool $val)* @method self itemprop(?string $val)* @method self kind(?string $val)* @method self label(?string $val)* @method self lang(?string $val)* @method self list(?string $val)* @method self loop(?bool $val)* @method self low(?float $val)* @method self max(?float $val)* @method self maxlength(?int $val)* @method self minlength(?int $val)* @method self media(?string $val)* @method self method(?string $val)* @method self min(?float $val)* @method self multiple(?bool $val)* @method self muted(?bool $val)* @method self name(?string $val)* @method self novalidate(?bool $val)* @method self open(?bool $val)* @method self optimum(?float $val)* @method self pattern(?string $val)* @method self ping(?string $val, bool $state = null)* @method self placeholder(?string $val)* @method self poster(?string $val)* @method self preload(?string $val)* @method self radiogroup(?string $val)* @method self readonly(?bool $val)* @method self rel(?string $val)* @method self required(?bool $val)* @method self reversed(?bool $val)* @method self rows(?int $val)* @method self rowspan(?int $val)* @method self sandbox(?string $val, bool $state = null)* @method self scope(?string $val)* @method self selected(?bool $val)* @method self shape(?string $val)* @method self size(?int $val)* @method self sizes(?string $val)* @method self slot(?string $val)* @method self span(?int $val)* @method self spellcheck(?string $val)* @method self src(?string $val)* @method self srcdoc(?string $val)* @method self srclang(?string $val)* @method self srcset(?string $val)* @method self start(?int $val)* @method self step(?float $val)* @method self style(?string $property, string $val = null)* @method self tabindex(?int $val)* @method self target(?string $val)* @method self title(?string $val)* @method self translate(?string $val)* @method self type(?string $val)* @method self usemap(?string $val)* @method self value(?string $val)* @method self width(?int $val)* @method self wrap(?string $val)*/class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringable{use Nette\SmartObject;/** @var array<string, mixed> element's attributes */public $attrs = [];/** @var bool use XHTML syntax? */public static $xhtml = false;/** @var array<string, int> void elements */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,];/** @var array<int, HtmlStringable|string> nodes */protected $children = [];/** @var string element's name */private $name;/** @var bool is element empty? */private $isEmpty;/*** Constructs new HTML element.* @param array|string $attrs element's attributes or plain text content* @return static*/public static function el(?string $name = null, $attrs = null){$el = new static;$parts = explode(' ', (string) $name, 2);$el->setName($parts[0]);if (is_array($attrs)) {$el->attrs = $attrs;} elseif ($attrs !== null) {$el->setText($attrs);}if (isset($parts[1])) {foreach (Strings::matchAll($parts[1] . ' ', '#([a-z0-9:-]+)(?:=(["\'])?(.*?)(?(2)\2|\s))?#i') as $m) {$el->attrs[$m[1]] = $m[3] ?? true;}}return $el;}/*** Returns an object representing HTML text.*/public static function fromHtml(string $html): self{return (new static)->setHtml($html);}/*** Returns an object representing plain text.*/public static function fromText(string $text): self{return (new static)->setText($text);}/*** Converts to HTML.*/final public function toHtml(): string{return $this->render();}/*** Converts to plain text.*/final public function toText(): string{return $this->getText();}/*** Converts given HTML code to plain text.*/public static function htmlToText(string $html): string{return html_entity_decode(strip_tags($html), ENT_QUOTES | ENT_HTML5, 'UTF-8');}/*** Changes element's name.* @return static*/final public function setName(string $name, ?bool $isEmpty = null){$this->name = $name;$this->isEmpty = $isEmpty ?? isset(static::$emptyElements[$name]);return $this;}/*** Returns element's name.*/final public function getName(): string{return $this->name;}/*** Is element empty?*/final public function isEmpty(): bool{return $this->isEmpty;}/*** Sets multiple attributes.* @return static*/public function addAttributes(array $attrs){$this->attrs = array_merge($this->attrs, $attrs);return $this;}/*** Appends value to element's attribute.* @param mixed $value* @param mixed $option* @return static*/public function appendAttribute(string $name, $value, $option = true){if (is_array($value)) {$prev = isset($this->attrs[$name]) ? (array) $this->attrs[$name] : [];$this->attrs[$name] = $value + $prev;} elseif ((string) $value === '') {$tmp = &$this->attrs[$name]; // appending empty value? -> ignore, but ensure it exists} elseif (!isset($this->attrs[$name]) || is_array($this->attrs[$name])) { // needs array$this->attrs[$name][$value] = $option;} else {$this->attrs[$name] = [$this->attrs[$name] => true, $value => $option];}return $this;}/*** Sets element's attribute.* @param mixed $value* @return static*/public function setAttribute(string $name, $value){$this->attrs[$name] = $value;return $this;}/*** Returns element's attribute.* @return mixed*/public function getAttribute(string $name){return $this->attrs[$name] ?? null;}/*** Unsets element's attribute.* @return static*/public function removeAttribute(string $name){unset($this->attrs[$name]);return $this;}/*** Unsets element's attributes.* @return static*/public function removeAttributes(array $attributes){foreach ($attributes as $name) {unset($this->attrs[$name]);}return $this;}/*** Overloaded setter for element's attribute.* @param mixed $value*/final public function __set(string $name, $value): void{$this->attrs[$name] = $value;}/*** Overloaded getter for element's attribute.* @return mixed*/final public function &__get(string $name){return $this->attrs[$name];}/*** Overloaded tester for element's attribute.*/final public function __isset(string $name): bool{return isset($this->attrs[$name]);}/*** Overloaded unsetter for element's attribute.*/final public function __unset(string $name): void{unset($this->attrs[$name]);}/*** Overloaded setter for element's attribute.* @return mixed*/final public function __call(string $m, array $args){$p = substr($m, 0, 3);if ($p === 'get' || $p === 'set' || $p === 'add') {$m = substr($m, 3);$m[0] = $m[0] | "\x20";if ($p === 'get') {return $this->attrs[$m] ?? null;} elseif ($p === 'add') {$args[] = true;}}if (count($args) === 0) { // invalid} elseif (count($args) === 1) { // set$this->attrs[$m] = $args[0];} else { // add$this->appendAttribute($m, $args[0], $args[1]);}return $this;}/*** Special setter for element's attribute.* @return static*/final public function href(string $path, ?array $query = null){if ($query) {$query = http_build_query($query, '', '&');if ($query !== '') {$path .= '?' . $query;}}$this->attrs['href'] = $path;return $this;}/*** Setter for data-* attributes. Booleans are converted to 'true' resp. 'false'.* @param mixed $value* @return static*/public function data(string $name, $value = null){if (func_num_args() === 1) {$this->attrs['data'] = $name;} else {$this->attrs["data-$name"] = is_bool($value)? json_encode($value): $value;}return $this;}/*** Sets element's HTML content.* @param HtmlStringable|string $html* @return static*/final public function setHtml($html){$this->children = [(string) $html];return $this;}/*** Returns element's HTML content.*/final public function getHtml(): string{return implode('', $this->children);}/*** Sets element's textual content.* @param HtmlStringable|string|int|float $text* @return static*/final public function setText($text){if (!$text instanceof HtmlStringable) {$text = htmlspecialchars((string) $text, ENT_NOQUOTES, 'UTF-8');}$this->children = [(string) $text];return $this;}/*** Returns element's textual content.*/final public function getText(): string{return self::htmlToText($this->getHtml());}/*** Adds new element's child.* @param HtmlStringable|string $child Html node or raw HTML string* @return static*/final public function addHtml($child){return $this->insert(null, $child);}/*** Appends plain-text string to element content.* @param HtmlStringable|string|int|float $text* @return static*/public function addText($text){if (!$text instanceof HtmlStringable) {$text = htmlspecialchars((string) $text, ENT_NOQUOTES, 'UTF-8');}return $this->insert(null, $text);}/*** Creates and adds a new Html child.* @param array|string $attrs element's attributes or raw HTML string* @return static created element*/final public function create(string $name, $attrs = null){$this->insert(null, $child = static::el($name, $attrs));return $child;}/*** Inserts child node.* @param HtmlStringable|string $child Html node or raw HTML string* @return static*/public function insert(?int $index, $child, bool $replace = false){$child = $child instanceof self ? $child : (string) $child;if ($index === null) { // append$this->children[] = $child;} else { // insert or replacearray_splice($this->children, $index, $replace ? 1 : 0, [$child]);}return $this;}/*** Inserts (replaces) child node (\ArrayAccess implementation).* @param int|null $index position or null for appending* @param Html|string $child Html node or raw HTML string*/final public function offsetSet($index, $child): void{$this->insert($index, $child, true);}/*** Returns child node (\ArrayAccess implementation).* @param int $index* @return HtmlStringable|string*/#[\ReturnTypeWillChange]final public function offsetGet($index){return $this->children[$index];}/*** Exists child node? (\ArrayAccess implementation).* @param int $index*/final public function offsetExists($index): bool{return isset($this->children[$index]);}/*** Removes child node (\ArrayAccess implementation).* @param int $index*/public function offsetUnset($index): void{if (isset($this->children[$index])) {array_splice($this->children, $index, 1);}}/*** Returns children count.*/final public function count(): int{return count($this->children);}/*** Removes all children.*/public function removeChildren(): void{$this->children = [];}/*** Iterates over elements.* @return \ArrayIterator<int, HtmlStringable|string>*/final public function getIterator(): \ArrayIterator{return new \ArrayIterator($this->children);}/*** Returns all children.*/final public function getChildren(): array{return $this->children;}/*** Renders element's start tag, content and end tag.*/final public function render(?int $indent = null): string{$s = $this->startTag();if (!$this->isEmpty) {// add contentif ($indent !== null) {$indent++;}foreach ($this->children as $child) {if ($child instanceof self) {$s .= $child->render($indent);} else {$s .= $child;}}// add end tag$s .= $this->endTag();}if ($indent !== null) {return "\n" . str_repeat("\t", $indent - 1) . $s . "\n" . str_repeat("\t", max(0, $indent - 2));}return $s;}final public function __toString(): string{try {return $this->render();} catch (\Throwable $e) {if (PHP_VERSION_ID >= 70400) {throw $e;}trigger_error('Exception in ' . __METHOD__ . "(): {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", E_USER_ERROR);return '';}}/*** Returns element's start tag.*/final public function startTag(): string{return $this->name? '<' . $this->name . $this->attributes() . (static::$xhtml && $this->isEmpty ? ' />' : '>'): '';}/*** Returns element's end tag.*/final public function endTag(): string{return $this->name && !$this->isEmpty ? '</' . $this->name . '>' : '';}/*** Returns element's attributes.* @internal*/final public function attributes(): string{if (!is_array($this->attrs)) {return '';}$s = '';$attrs = $this->attrs;foreach ($attrs as $key => $value) {if ($value === null || $value === false) {continue;} elseif ($value === true) {if (static::$xhtml) {$s .= ' ' . $key . '="' . $key . '"';} else {$s .= ' ' . $key;}continue;} elseif (is_array($value)) {if (strncmp($key, 'data-', 5) === 0) {$value = Json::encode($value);} else {$tmp = null;foreach ($value as $k => $v) {if ($v != null) { // intentionally ==, skip nulls & empty string// composite 'style' vs. 'others'$tmp[] = $v === true? $k: (is_string($k) ? $k . ':' . $v : $v);}}if ($tmp === null) {continue;}$value = implode($key === 'style' || !strncmp($key, 'on', 2) ? ';' : ' ', $tmp);}} elseif (is_float($value)) {$value = rtrim(rtrim(number_format($value, 10, '.', ''), '0'), '.');} else {$value = (string) $value;}$q = strpos($value, '"') === false ? '"' : "'";$s .= ' ' . $key . '=' . $q. str_replace(['&', $q, '<'],['&', $q === '"' ? '"' : ''', self::$xhtml ? '<' : '<'],$value). (strpos($value, '`') !== false && strpbrk($value, ' <>"\'') === false ? ' ' : ''). $q;}$s = str_replace('@', '@', $s);return $s;}/*** Clones all children too.*/public function __clone(){foreach ($this->children as $key => $value) {if (is_object($value)) {$this->children[$key] = clone $value;}}}}