element's attributes */
public $attrs = [];
/** 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 nodes */
protected $children = [];
/** element's name
* @var string */
private $name = '';
/**
* @var bool
*/
private $isEmpty = \false;
/**
* 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.
* @return static
*/
public static function fromHtml(string $html)
{
return (new static())->setHtml($html);
}
/**
* Returns an object representing plain text.
* @return static
*/
public static function fromText(string $text)
{
return (new static())->setText($text);
}
/**
* Converts to HTML.
*/
public final function toHtml() : string
{
return $this->render();
}
/**
* Converts to plain text.
*/
public final 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
*/
public final function setName(string $name, ?bool $isEmpty = null)
{
$this->name = $name;
$this->isEmpty = $isEmpty ?? isset(static::$emptyElements[$name]);
return $this;
}
/**
* Returns element's name.
*/
public final function getName() : string
{
return $this->name;
}
/**
* Is element empty?
*/
public final 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
*/
public final function __set(string $name, $value) : void
{
$this->attrs[$name] = $value;
}
/**
* Overloaded getter for element's attribute.
* @return mixed
*/
public final function &__get(string $name)
{
return $this->attrs[$name];
}
/**
* Overloaded tester for element's attribute.
*/
public final function __isset(string $name) : bool
{
return isset($this->attrs[$name]);
}
/**
* Overloaded unsetter for element's attribute.
*/
public final function __unset(string $name) : void
{
unset($this->attrs[$name]);
}
/**
* Overloaded setter for element's attribute.
* @return mixed
*/
public final 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] | " ";
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
*/
public final function href(string $path, array $query = [])
{
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 mixed $html
* @return static
*/
public final function setHtml($html)
{
$this->children = [(string) $html];
return $this;
}
/**
* Returns element's HTML content.
*/
public final function getHtml() : string
{
return \implode('', $this->children);
}
/**
* Sets element's textual content.
* @param mixed $text
* @return static
*/
public final 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.
*/
public final function getText() : string
{
return self::htmlToText($this->getHtml());
}
/**
* Adds new element's child.
* @param mixed $child
* @return static
*/
public final function addHtml($child)
{
return $this->insert(null, $child);
}
/**
* Appends plain-text string to element content.
* @param mixed $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 mixed[]|string|null $attrs
* @return static
*/
public final function create(string $name, $attrs = null)
{
$this->insert(null, $child = static::el($name, $attrs));
return $child;
}
/**
* Inserts child node.
* @param \Nette\HtmlStringable|string $child
* @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 replace
\array_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
*/
public final function offsetSet($index, $child) : void
{
$this->insert($index, $child, \true);
}
/**
* Returns child node (\ArrayAccess implementation).
* @param int $index
* @return \Nette\HtmlStringable|string
*/
#[\ReturnTypeWillChange]
public final function offsetGet($index)
{
return $this->children[$index];
}
/**
* Exists child node? (\ArrayAccess implementation).
* @param int $index
*/
public final 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.
*/
public final function count() : int
{
return \count($this->children);
}
/**
* Removes all children.
*/
public function removeChildren() : void
{
$this->children = [];
}
/**
* Iterates over elements.
* @return \ArrayIterator
*/
public final function getIterator() : \ArrayIterator
{
return new \ArrayIterator($this->children);
}
/**
* Returns all children.
*/
public final function getChildren() : array
{
return $this->children;
}
/**
* Renders element's start tag, content and end tag.
*/
public final function render(?int $indent = null) : string
{
$s = $this->startTag();
if (!$this->isEmpty) {
// add content
if ($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;
}
public final function __toString() : string
{
return $this->render();
}
/**
* Returns element's start tag.
*/
public final function startTag() : string
{
return $this->name ? '<' . $this->name . $this->attributes() . '>' : '';
}
/**
* Returns element's end tag.
*/
public final function endTag() : string
{
return $this->name && !$this->isEmpty ? '' . $this->name . '>' : '';
}
/**
* Returns element's attributes.
* @internal
*/
public final 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) {
$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 === '"' ? '"' : ''', '<'], $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;
}
}
}
}