1<?php 2 3declare(strict_types=1); 4 5/* 6 * This file is part of the league/commonmark package. 7 * 8 * (c) Colin O'Dell <colinodell@gmail.com> 9 * 10 * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) 11 * - (c) John MacFarlane 12 * 13 * For the full copyright and license information, please view the LICENSE 14 * file that was distributed with this source code. 15 */ 16 17namespace League\CommonMark\Util; 18 19final class HtmlElement implements \Stringable 20{ 21 /** @psalm-readonly */ 22 private string $tagName; 23 24 /** @var array<string, string|bool> */ 25 private array $attributes = []; 26 27 /** @var \Stringable|\Stringable[]|string */ 28 private $contents; 29 30 /** @psalm-readonly */ 31 private bool $selfClosing; 32 33 /** 34 * @param string $tagName Name of the HTML tag 35 * @param array<string, string|string[]|bool> $attributes Array of attributes (values should be unescaped) 36 * @param \Stringable|\Stringable[]|string|null $contents Inner contents, pre-escaped if needed 37 * @param bool $selfClosing Whether the tag is self-closing 38 */ 39 public function __construct(string $tagName, array $attributes = [], $contents = '', bool $selfClosing = false) 40 { 41 $this->tagName = $tagName; 42 $this->selfClosing = $selfClosing; 43 44 foreach ($attributes as $name => $value) { 45 $this->setAttribute($name, $value); 46 } 47 48 $this->setContents($contents ?? ''); 49 } 50 51 /** @psalm-immutable */ 52 public function getTagName(): string 53 { 54 return $this->tagName; 55 } 56 57 /** 58 * @return array<string, string|bool> 59 * 60 * @psalm-immutable 61 */ 62 public function getAllAttributes(): array 63 { 64 return $this->attributes; 65 } 66 67 /** 68 * @return string|bool|null 69 * 70 * @psalm-immutable 71 */ 72 public function getAttribute(string $key) 73 { 74 return $this->attributes[$key] ?? null; 75 } 76 77 /** 78 * @param string|string[]|bool $value 79 */ 80 public function setAttribute(string $key, $value = true): self 81 { 82 if (\is_array($value)) { 83 $this->attributes[$key] = \implode(' ', \array_unique($value)); 84 } else { 85 $this->attributes[$key] = $value; 86 } 87 88 return $this; 89 } 90 91 /** 92 * @return \Stringable|\Stringable[]|string 93 * 94 * @psalm-immutable 95 */ 96 public function getContents(bool $asString = true) 97 { 98 if (! $asString) { 99 return $this->contents; 100 } 101 102 return $this->getContentsAsString(); 103 } 104 105 /** 106 * Sets the inner contents of the tag (must be pre-escaped if needed) 107 * 108 * @param \Stringable|\Stringable[]|string $contents 109 * 110 * @return $this 111 */ 112 public function setContents($contents): self 113 { 114 $this->contents = $contents ?? ''; // @phpstan-ignore-line 115 116 return $this; 117 } 118 119 /** @psalm-immutable */ 120 public function __toString(): string 121 { 122 $result = '<' . $this->tagName; 123 124 foreach ($this->attributes as $key => $value) { 125 if ($value === true) { 126 $result .= ' ' . $key; 127 } elseif ($value === false) { 128 continue; 129 } else { 130 $result .= ' ' . $key . '="' . Xml::escape($value) . '"'; 131 } 132 } 133 134 if ($this->contents !== '') { 135 $result .= '>' . $this->getContentsAsString() . '</' . $this->tagName . '>'; 136 } elseif ($this->selfClosing && $this->tagName === 'input') { 137 $result .= '>'; 138 } elseif ($this->selfClosing) { 139 $result .= ' />'; 140 } else { 141 $result .= '></' . $this->tagName . '>'; 142 } 143 144 return $result; 145 } 146 147 /** @psalm-immutable */ 148 private function getContentsAsString(): string 149 { 150 if (\is_string($this->contents)) { 151 return $this->contents; 152 } 153 154 if (\is_array($this->contents)) { 155 return \implode('', $this->contents); 156 } 157 158 return (string) $this->contents; 159 } 160} 161