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