1<?php 2 3namespace ComboStrap\Xml; 4 5use ComboStrap\ArrayCaseInsensitive; 6use ComboStrap\ArrayUtility; 7use ComboStrap\ExceptionBadArgument; 8use ComboStrap\ExceptionBadSyntax; 9use ComboStrap\ExceptionNotEquals; 10use ComboStrap\ExceptionNotFound; 11use ComboStrap\ExceptionRuntime; 12use ComboStrap\Html; 13use ComboStrap\StringUtility; 14use ComboStrap\TagAttribute\StyleAttribute; 15use ComboStrap\Web\Url; 16use ComboStrap\Xml\XmlSystems; 17use DOMElement; 18use DOMText; 19 20class XmlElement 21{ 22 23 private DOMElement $domElement; 24 private XmlDocument $document; 25 private array $styleDeclaration = []; 26 27 /** 28 * @param DOMElement $domElement - the dom element wrapped 29 * @param XmlDocument $document - the document 30 */ 31 public function __construct(DOMElement $domElement, XmlDocument $document) 32 { 33 $this->domElement = $domElement; 34 $this->document = $document; 35 36 } 37 38 public static function create($domElement, XmlDocument $xmlDocument): XmlElement 39 { 40 return new XmlElement($domElement, $xmlDocument); 41 } 42 43 public function getAttribute(string $qualifiedName): string 44 { 45 return $this->domElement->getAttribute($qualifiedName); 46 } 47 48 public function getClass(): string 49 { 50 return $this->domElement->getAttribute("class"); 51 } 52 53 /** 54 * @throws ExceptionNotFound 55 */ 56 public function getFirstChildElement(): XmlElement 57 { 58 $firstChild = $this->domElement->firstChild; 59 while (!($firstChild instanceof DOMElement)) { 60 if ($firstChild === null) { 61 throw new ExceptionNotFound("No first child element"); 62 } 63 $firstChild = $firstChild->nextSibling; 64 } 65 return new XmlElement($firstChild, $this->document); 66 } 67 68 /** 69 * @return XmlElement[] 70 */ 71 public function getChildrenElement(): array 72 { 73 $childNodes = []; 74 foreach ($this->domElement->childNodes as $childNode) { 75 if ($childNode instanceof DOMElement) { 76 $childNodes[] = new XmlElement($childNode, $this->document); 77 } 78 } 79 return $childNodes; 80 } 81 82 /** 83 * @return array - the node text values in an array 84 */ 85 public function getChildrenNodeTextValues(): array 86 { 87 $childNodes = []; 88 foreach ($this->domElement->childNodes as $childNode) { 89 if ($childNode instanceof DOMText) { 90 $childNodes[] = $childNode->nodeValue; 91 } else { 92 $childNodes[] = implode('', XmlElement::create($childNode, $this->document)->getChildrenNodeTextValues()); 93 } 94 } 95 return $childNodes; 96 } 97 98 /** 99 * @return XmlElement[] 100 * @throws ExceptionBadSyntax 101 */ 102 public function querySelectorAll(string $selector): array 103 { 104 $xpath = $this->document->cssSelectorToXpath($selector); 105 $nodes = []; 106 foreach ($this->document->xpath($xpath, $this->domElement) as $child) { 107 if ($child instanceof DOMElement) { 108 $nodes[] = new XmlElement($child, $this->document); 109 } 110 } 111 return $nodes; 112 } 113 114 public function getXmlTextNormalized(): string 115 { 116 117 return $this->document->toXmlNormalized($this->domElement); 118 119 } 120 121 public function removeAttribute($attributeName): XmlElement 122 { 123 $attr = $this->domElement->getAttributeNode($attributeName); 124 if ($attr == false) { 125 return $this; 126 } 127 $result = $this->domElement->removeAttributeNode($attr); 128 if ($result === false) { 129 throw new ExceptionRuntime("Not able to delete the attribute $attributeName of the node element {$this->domElement->tagName} in the Xml document"); 130 } 131 return $this; 132 } 133 134 public function remove(): XmlElement 135 { 136 $this->domElement->parentNode->removeChild($this->domElement); 137 return $this; 138 } 139 140 public function getStyle(): string 141 { 142 return $this->domElement->getAttribute("style"); 143 } 144 145 public function getNodeValue() 146 { 147 return $this->domElement->nodeValue; 148 } 149 150 /** 151 * @throws ExceptionBadSyntax 152 * @throws ExceptionNotFound 153 */ 154 public function querySelector(string $selector): XmlElement 155 { 156 $domNodeList = $this->querySelectorAll($selector); 157 if (sizeof($domNodeList) >= 1) { 158 return $domNodeList[0]; 159 } 160 throw new ExceptionNotFound("No element was found with the selector $selector"); 161 } 162 163 public function getLocalName() 164 { 165 return $this->domElement->localName; 166 } 167 168 public function addClass(string $class): XmlElement 169 { 170 $classes = Html::mergeClassNames($class, $this->getClass()); 171 $this->domElement->setAttribute("class", $classes); 172 return $this; 173 } 174 175 public function setAttribute(string $name, string $value): XmlElement 176 { 177 $this->domElement->setAttribute($name, $value); 178 return $this; 179 } 180 181 public function hasAttribute(string $name): bool 182 { 183 return $this->domElement->hasAttribute($name); 184 } 185 186 public function getDomElement(): DOMElement 187 { 188 return $this->domElement; 189 } 190 191 /** 192 * Append a text node as a child 193 * @param string $string - the text 194 * @param string $position - the position on where to insert the text node 195 * @return $this 196 * @throws ExceptionBadArgument - if the text is not a valid text xml expression 197 */ 198 public function insertAdjacentTextNode(string $string, string $position = 'afterbegin'): XmlElement 199 { 200 $textNode = $this->domElement->ownerDocument->createTextNode($string); 201 $this->insertAdjacentDomElement($position, $textNode); 202 return $this; 203 } 204 205 public function toHtml() 206 { 207 return $this->domElement->ownerDocument->saveHTML($this->domElement); 208 } 209 210 public function toXhtml() 211 { 212 return $this->domElement->ownerDocument->saveXML($this->domElement); 213 } 214 215 public function getNodeValueWithoutCdata() 216 { 217 return XmlSystems::extractTextWithoutCdata($this->getNodeValue()); 218 } 219 220 /** 221 * @throws ExceptionBadSyntax 222 * @throws ExceptionBadArgument 223 */ 224 public function insertAdjacentHTML(string $position, string $html): XmlElement 225 { 226 $externalElement = XmlDocument::createHtmlDocFromMarkup($html)->getElement()->getDomElement(); 227 // import/copy item from external document to internal document 228 $internalElement = $this->importIfExternal($externalElement); 229 $this->insertAdjacentDomElement($position, $internalElement); 230 return $this; 231 } 232 233 234 public function getId(): string 235 { 236 return $this->getAttribute("id"); 237 } 238 239 public function hasChildrenElement(): bool 240 { 241 return sizeof($this->getChildrenElement()) !== 0; 242 } 243 244 public function getAttributeOrDefault(string $string, string $default): string 245 { 246 if (!$this->hasAttribute($string)) { 247 return $default; 248 } 249 return $this->getAttribute($string); 250 251 } 252 253 public function appendChild(XmlElement $xmlElement): XmlElement 254 { 255 $element = $this->importIfExternal($xmlElement->domElement); 256 $this->domElement->appendChild($element); 257 return $this; 258 } 259 260 public function getDocument(): XmlDocument 261 { 262 return $this->document; 263 } 264 265 public function setNodeValue(string $nodeValue) 266 { 267 $this->domElement->nodeValue = $nodeValue; 268 } 269 270 public function addStyle(string $name, string $value): XmlElement 271 { 272 ArrayUtility::addIfNotSet($this->styleDeclaration, $name, $value); 273 $this->setAttribute("style", Html::array2InlineStyle($this->styleDeclaration)); 274 return $this; 275 } 276 277 /** 278 * 279 * Utility to change the owner document 280 * otherwise you get an error: 281 * ``` 282 * DOMException : Wrong Document Error 283 * ``` 284 * 285 * @param DOMElement $domElement 286 * @return DOMElement 287 */ 288 private function importIfExternal(DOMElement $domElement): DOMElement 289 { 290 if ($domElement->ownerDocument !== $this->getDocument()->getDomDocument()) { 291 return $this->getDocument()->getDomDocument()->importNode($domElement, true); 292 } 293 return $domElement; 294 } 295 296 /** 297 * @throws ExceptionNotEquals 298 */ 299 public function equals(XmlElement $rightDocument, array $attributeFilter = []) 300 { 301 $error = ""; 302 XmlSystems::diffNode( 303 $this->domElement, 304 $rightDocument->domElement, 305 $error, 306 $attributeFilter 307 ); 308 if ($error !== null) { 309 throw new ExceptionNotEquals($error); 310 } 311 } 312 313 public function removeClass(string $string): XmlElement 314 { 315 $class = $this->getClass(); 316 $newClass = str_replace($string, "", $class); 317 $this->setAttribute("class", $newClass); 318 return $this; 319 } 320 321 /** 322 * @throws ExceptionNotFound 323 */ 324 public function getParent(): XmlElement 325 { 326 $parentNode = $this->domElement->parentNode; 327 if ($parentNode !== null) { 328 while (!($parentNode instanceof DOMElement)) { 329 $parentNode = $parentNode->parentNode; 330 if ($parentNode === null) { 331 break; 332 } 333 } 334 } 335 if ($parentNode === null) { 336 throw new ExceptionNotFound("No parent node found"); 337 } 338 return new XmlElement($parentNode, $this->document); 339 } 340 341 /** 342 * @param string $position 343 * @param \DOMNode $domNode - ie {@Link \DOMElement} or {@link \DOMNode} 344 * @return XmlElement 345 * @throws ExceptionBadArgument 346 */ 347 public function insertAdjacentDomElement(string $position, \DOMNode $domNode): XmlElement 348 { 349 switch ($position) { 350 case 'beforeend': 351 $this->domElement->appendChild($domNode); 352 return $this; 353 case 'afterbegin': 354 $firstChild = $this->domElement->firstChild; 355 if ($firstChild === null) { 356 $this->domElement->appendChild($domNode); 357 } else { 358 // The object on which you actually call the insertBefore() 359 // on the parent node of the reference node 360 // otherwise you get a `not found` 361 // https://www.php.net/manual/en/domnode.insertbefore.php#53506 362 $firstChild->parentNode->insertBefore($domNode, $firstChild); 363 } 364 return $this; 365 case 'beforebegin': 366 $this->domElement->parentNode->insertBefore($domNode, $this->domElement); 367 return $this; 368 default: 369 throw new ExceptionBadArgument("The position ($position) is unknown"); 370 } 371 } 372 373 public function getInnerText(): string 374 { 375 if ($this->hasChildrenElement()) { 376 return implode('', $this->getChildrenNodeTextValues()); 377 } else { 378 return $this->domElement->nodeValue; 379 } 380 } 381 382 public function getInnerTextWithoutCdata() 383 { 384 return XmlSystems::extractTextWithoutCdata($this->getInnerText()); 385 } 386 387 public function getStyleProperties(): ArrayCaseInsensitive 388 { 389 $source = StyleAttribute::HtmlStyleValueToArray($this->getStyle()); 390 return new ArrayCaseInsensitive($source); 391 } 392 393 public function getStyleProperty(string $property): string 394 { 395 return $this->getStyleProperties()[$property]; 396 } 397 398 /** 399 * @throws ExceptionBadSyntax 400 * @throws ExceptionBadArgument 401 */ 402 public function getAttributeAsUrl(string $attributeName): Url 403 { 404 $value = $this->getAttribute($attributeName); 405 if (empty($value)) { 406 return Url::createEmpty(); 407 } 408 return Url::createFromString($value); 409 } 410 411 public function __toString() 412 { 413 $toString = $this->getLocalName(); 414 $class = $this->getClass(); 415 if ($class !== "") { 416 $classes = StringUtility::explodeAndTrim($class, " "); 417 $toString .= '.' . implode(".", $classes); 418 } 419 if ($this->getId() !== "") { 420 $toString .= "#" . $this->getId(); 421 } 422 return $toString; 423 } 424 425 public function hasClass(string $needleClass): bool 426 { 427 $classes = preg_split("/\s/", $this->getClass()); 428 if (in_array($needleClass, $classes)) { 429 return true; 430 } 431 return false; 432 } 433 434 435} 436