1<?php 2 3/** 4 * Copyright (c) 2021. ComboStrap, Inc. and its affiliates. All Rights Reserved. 5 * 6 * This source code is licensed under the GPL license found in the 7 * COPYING file in the root directory of this source tree. 8 * 9 * @license GPL 3 (https://www.gnu.org/licenses/gpl-3.0.en.html) 10 * @author ComboStrap <support@combostrap.com> 11 * 12 */ 13 14namespace ComboStrap\Xml; 15 16use ComboStrap\ExceptionBadState; 17use ComboStrap\ExceptionBadSyntax; 18use ComboStrap\ExceptionNotFound; 19use ComboStrap\FileSystems; 20use ComboStrap\LogUtility; 21use ComboStrap\Path; 22use ComboStrap\PluginUtility; 23use DOMAttr; 24use DOMDocument; 25use DOMElement; 26use DOMNodeList; 27use DOMXPath; 28use LibXMLError; 29use PhpCss; 30 31 32/** 33 * A xml document that follows the Web Api interface. 34 * 35 * Note Dokuwiki now uses since [jack_jackrum](https://www.dokuwiki.org/changes#release_2023-04-04_jack_jackrum): 36 * the [dom-wrapper](https://github.com/scotteh/php-dom-wrapper) 37 * that follow the Jquery API and uses [css-selector](https://symfony.com/doc/current/components/css_selector.html) 38 * to get Xpath expression from Css selector 39 * 40 */ 41class XmlDocument 42{ 43 const HTML_TYPE = "html"; 44 const XML_TYPE = "xml"; 45 /** 46 * The error that the HTML loading 47 * may returns 48 */ 49 const KNOWN_HTML_LOADING_ERRORS = [ 50 "Tag section invalid\n", // section is HTML5 tag 51 "Tag footer invalid\n", // footer is HTML5 tag 52 "error parsing attribute name\n", // name is an HTML5 attribute 53 "Unexpected end tag : blockquote\n", // name is an HTML5 attribute 54 "Tag bdi invalid\n", 55 "Tag path invalid\n", // svg 56 "Tag svg invalid\n", // svg 57 "Unexpected end tag : a\n", // when the document is only a anchor 58 "Unexpected end tag : p\n", // when the document is only a p 59 "Unexpected end tag : button\n", // when the document is only a button 60 ]; 61 62 const CANONICAL = "xml"; 63 64 /** 65 * @var DOMDocument 66 */ 67 private DOMDocument $domDocument; 68 /** 69 * @var DOMXPath 70 */ 71 private DOMXPath $domXpath; 72 73 /** 74 * XmlFile constructor. 75 * @param $text 76 * @param string $type - HTML or not 77 * @throws ExceptionBadSyntax - if the document is not valid or the lib xml is not available 78 * 79 * Getting the width of an error HTML document if the file was downloaded 80 * from a server has no use at all 81 */ 82 public function __construct($text, string $type = self::XML_TYPE) 83 { 84 85 if (empty($text)) { 86 throw new ExceptionBadSyntax("The xml text markup should not be empty.", self::CANONICAL); 87 } 88 if (!$this->isXmlExtensionLoaded()) { 89 /** 90 * If the XML module is not present 91 */ 92 throw new ExceptionBadSyntax("The php `libxml` module was not found on your installation, the xml/svg file could not be modified / instantiated", self::CANONICAL); 93 } 94 95 // https://www.php.net/manual/en/libxml.constants.php 96 $options = LIBXML_NOCDATA 97 // | LIBXML_NOBLANKS // same as preserveWhiteSpace=true, not set to be able to format the output 98 | LIBXML_NOXMLDECL // Drop the XML declaration when saving a document 99 | LIBXML_NONET // No network during load 100 | LIBXML_NSCLEAN // Remove redundant namespace declarations - for whatever reason, the formatting does not work if this is set 101 ; 102 103 // HTML 104 if ($type == self::HTML_TYPE) { 105 106 // Options that cause the process to hang if this is not for a html file 107 // Empty tag option may also be used only on save 108 // at https://www.php.net/manual/en/domdocument.save.php 109 // and https://www.php.net/manual/en/domdocument.savexml.php 110 $options = $options 111 // | LIBXML_NOEMPTYTAG // Expand empty tags (e.g. <br/> to <br></br>) 112 | LIBXML_HTML_NODEFDTD // No doctype 113 | LIBXML_HTML_NOIMPLIED; 114 115 116 } 117 118 /** 119 * No warning reporting 120 * Load XML issue E_STRICT warning seen in the log 121 */ 122 if (!PluginUtility::isTest()) { 123 $oldLevel = error_reporting(E_ERROR); 124 } 125 126 $this->domDocument = new DOMDocument('1.0', 'UTF-8'); 127 128 $this->mandatoryFormatConfigBeforeLoading(); 129 130 131 $text = $this->processTextBeforeLoading($text); 132 133 /** 134 * Because the load does handle HTML5tag as error 135 * (ie section for instance) 136 * We take over the errors and handle them after the below load 137 * 138 * https://www.php.net/manual/en/function.libxml-use-internal-errors.php 139 * 140 */ 141 libxml_use_internal_errors(true); 142 143 if ($type == self::XML_TYPE) { 144 145 $result = $this->domDocument->loadXML($text, $options); 146 147 } else { 148 149 /** 150 * Unlike loading XML, HTML does not have to be well-formed to load. 151 * While malformed HTML should load successfully, this function may generate E_WARNING errors 152 * @deprecated as we try to be XHTML compliantXML but yeah this is not always possible 153 */ 154 155 /** 156 * Bug: Even if we set that the document is an UTF-8 157 * loadHTML treat the string as being in ISO-8859-1 if without any heading 158 * (ie <xml encoding="utf-8"..> 159 * https://stackoverflow.com/questions/8218230/php-domdocument-loadhtml-not-encoding-utf-8-correctly 160 * Otherwise French and other language are not well loaded 161 * 162 * We use the trick to transform UTF-8 to HTML 163 */ 164 $htmlEntityEncoded = mb_convert_encoding($text, 'HTML-ENTITIES', 'UTF-8'); 165 $result = $this->domDocument->loadHTML($htmlEntityEncoded, $options); 166 167 } 168 if ($result === false) { 169 170 /** 171 * Error 172 */ 173 $errors = libxml_get_errors(); 174 175 foreach ($errors as $error) { 176 177 /* @var LibXMLError 178 * @noinspection PhpComposerExtensionStubsInspection 179 * 180 * Section is an html5 tag (and is invalid for libxml) 181 */ 182 if (!in_array($error->message, self::KNOWN_HTML_LOADING_ERRORS)) { 183 /** 184 * This error is an XML and HTML error 185 */ 186 if ( 187 strpos($error->message, "htmlParseEntityRef: expecting ';' in Entity") !== false 188 || 189 $error->message == "EntityRef: expecting ';'\n" 190 ) { 191 $message = "There is big probability that there is an ampersand alone `&`. ie You forgot to call html/Xml entities in a `src` or `url` attribute."; 192 } else { 193 $message = "Error while loading HTML"; 194 } 195 /** 196 * inboolean attribute XML loading error 197 */ 198 if (strpos($error->message, "Specification mandates value for attribute") !== false) { 199 $message = "Xml does not allow boolean attribute (ie without any value). If you skip this error, you will get a general attribute constructing error as next error. Load as HTML."; 200 } 201 202 $message .= "Error: " . $error->message . ", Loaded text: " . $text; 203 204 /** 205 * We clean the errors, otherwise 206 * in a test series, they failed the next test 207 * 208 */ 209 libxml_clear_errors(); 210 211 // The xml dom object is null, we got NULL pointer exception everywhere 212 // just throw, the code will see it 213 throw new ExceptionBadSyntax($message, self::CANONICAL); 214 215 } 216 217 } 218 } 219 220 /** 221 * We clean the known errors (otherwise they are added in a queue) 222 */ 223 libxml_clear_errors(); 224 225 /** 226 * Error reporting back 227 */ 228 if (!PluginUtility::isTest() && isset($oldLevel)) { 229 error_reporting($oldLevel); 230 } 231 232 // namespace error : Namespace prefix dc on format is not defined 233 // missing the ns declaration in the file. example: 234 // xmlns:dc="http://purl.org/dc/elements/1.1/" 235 236 237 } 238 239 /** 240 * To not have a collusion with {@link FetcherSvg::createFetchImageSvgFromPath()} 241 * @param Path $path 242 * @return XmlDocument 243 * @throws ExceptionNotFound - if the file does not exist 244 * @throws ExceptionBadSyntax - if the content is not valid 245 */ 246 public 247 static function createXmlDocFromPath(Path $path): XmlDocument 248 { 249 $mime = XmlDocument::XML_TYPE; 250 if (in_array($path->getExtension(), ["html", "htm"])) { 251 $mime = XmlDocument::HTML_TYPE; 252 } 253 $content = FileSystems::getContent($path); 254 return (new XmlDocument($content, $mime)); 255 } 256 257 /** 258 * 259 * @throws ExceptionBadSyntax 260 */ 261 public 262 static function createXmlDocFromMarkup($string, $asHtml = false): XmlDocument 263 { 264 265 $mime = XmlDocument::XML_TYPE; 266 if ($asHtml) { 267 $mime = XmlDocument::HTML_TYPE; 268 } 269 return new XmlDocument($string, $mime); 270 } 271 272 /** 273 * HTML loading is more permissive 274 * 275 * For instance, you would not get an error on boolean attribute 276 * ``` 277 * Error while loading HTMLError: Specification mandates value for attribute defer 278 * ``` 279 * In Xml, it's mandatory but not in HTML, they are known as: 280 * https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#boolean-attribute 281 * 282 * 283 * @throws ExceptionBadSyntax 284 */ 285 public static function createHtmlDocFromMarkup($markup): XmlDocument 286 { 287 return self::createXmlDocFromMarkup($markup, true); 288 } 289 290 public 291 function &getDomDocument(): DOMDocument 292 { 293 return $this->domDocument; 294 } 295 296 /** 297 * @param $name 298 * @param $value 299 * @return void 300 * @deprecated use {@link XmlDocument::getElement()} instead 301 */ 302 public function setRootAttribute($name, $value) 303 { 304 if ($this->isXmlExtensionLoaded()) { 305 $this->domDocument->documentElement->setAttribute($name, $value); 306 } 307 } 308 309 /** 310 * @param $name 311 * @return string null if not found 312 * @deprecated uses {@link XmlElement::getAttribute()} of {@link self::getElement()} 313 */ 314 public function getRootAttributeValue($name): ?string 315 { 316 $value = $this->domDocument->documentElement->getAttribute($name); 317 if ($value === "") { 318 return null; 319 } 320 return $value; 321 } 322 323 public function toXhtml(DOMElement $element = null): string 324 { 325 return $this->toXml($element); 326 } 327 328 public function toXml(DOMElement $element = null): string 329 { 330 331 if ($element === null) { 332 $element = $this->getDomDocument()->documentElement; 333 } 334 /** 335 * LIBXML_NOXMLDECL (no xml declaration) does not work because only empty tag is recognized 336 * https://www.php.net/manual/en/domdocument.savexml.php 337 */ 338 $xmlText = $this->getDomDocument()->saveXML( 339 $element, 340 LIBXML_NOXMLDECL 341 ); 342 // Delete doctype (for svg optimization) 343 // php has only doctype manipulation for HTML 344 $xmlText = preg_replace('/^<!DOCTYPE.+?>/', '', $xmlText); 345 return trim($xmlText); 346 347 } 348 349 /** 350 * https://www.php.net/manual/en/dom.installation.php 351 * 352 * Check it with 353 * ``` 354 * php -m 355 * ``` 356 * Install with 357 * ``` 358 * sudo apt-get install php-xml 359 * ``` 360 * @return bool 361 */ 362 public function isXmlExtensionLoaded(): bool 363 { 364 // A suffix used in the bad message 365 $suffixBadMessage = "php extension is not installed. To install it, you need to install xml. Example: `sudo apt-get install php-xml`, `yum install php-xml`"; 366 367 // https://www.php.net/manual/en/dom.requirements.php 368 $loaded = extension_loaded("libxml"); 369 if ($loaded === false) { 370 LogUtility::msg("The libxml {$suffixBadMessage}"); 371 } else { 372 $loaded = extension_loaded("xml"); 373 if ($loaded === false) { 374 LogUtility::msg("The xml {$suffixBadMessage}"); 375 } else { 376 $loaded = extension_loaded("dom"); 377 if ($loaded === false) { 378 LogUtility::msg("The dom {$suffixBadMessage}"); 379 } 380 } 381 } 382 return $loaded; 383 } 384 385 /** 386 * https://stackoverflow.com/questions/30257438/how-to-completely-remove-a-namespace-using-domdocument 387 * @param $namespaceUri 388 */ 389 function removeNamespace($namespaceUri) 390 { 391 if (empty($namespaceUri)) { 392 throw new \RuntimeException("The namespace is empty and should be specified"); 393 } 394 395 if (strpos($namespaceUri, "http") === false) { 396 LogUtility::msg("Internal warning: The namespaceURI ($namespaceUri) does not seems to be an URI", LogUtility::LVL_MSG_WARNING, "support"); 397 } 398 399 /** 400 * @var DOMNodeList $nodes 401 * finds all nodes that have a namespace node called $ns where their parent node doesn't also have the same namespace. 402 * @var DOMNodeList $nodes 403 */ 404 try { 405 $nodes = $this->xpath("//*[namespace-uri()='$namespaceUri']"); 406 foreach ($nodes as $node) { 407 /** @var DOMElement $node */ 408 $node->parentNode->removeChild($node); 409 } 410 } catch (ExceptionBadSyntax $e) { 411 LogUtility::error("Internal Error on xpath: {$e->getMessage()}"); 412 } 413 414 try { 415 $nodes = $this->xpath("//@*[namespace-uri()='$namespaceUri']"); 416 foreach ($nodes as $node) { 417 /** @var DOMAttr $node */ 418 /** @var DOMElement $DOMNode */ 419 $DOMNode = $node->parentNode; 420 $DOMNode->removeAttributeNode($node); 421 } 422 } catch (ExceptionBadSyntax $e) { 423 LogUtility::error("Internal Error on xpath: {$e->getMessage()}"); 424 } 425 426 427 //Node namespace can be select only from the document 428 $xpath = new DOMXPath($this->getDomDocument()); 429 $DOMNodeList = $xpath->query("namespace::*", $this->getDomDocument()->ownerDocument); 430 foreach ($DOMNodeList as $node) { 431 $namespaceURI = $node->namespaceURI; 432 if ($namespaceURI == $namespaceUri) { 433 $parentNode = $node->parentNode; 434 $parentNode->removeAttributeNS($namespaceUri, $node->localName); 435 } 436 } 437 438 439 } 440 441 public function getNamespaces(): array 442 { 443 /** 444 * We can't query with the library {@link XmlDocument::xpath()} function because 445 * we register in the xpath the namespace 446 */ 447 $xpath = new DOMXPath($this->getDomDocument()); 448 // `namespace::*` means selects all the namespace attribute of the context node 449 // namespace is an axes 450 // See https://www.w3.org/TR/1999/REC-xpath-19991116/#axes 451 // the namespace axis contains the namespace nodes of the context node; the axis will be empty unless the context node is an element 452 $DOMNodeList = $xpath->query('namespace::*', $this->getDomDocument()->ownerDocument); 453 $nameSpace = array(); 454 foreach ($DOMNodeList as $node) { 455 /** @var DOMElement $node */ 456 457 $namespaceURI = $node->namespaceURI; 458 $localName = $node->prefix; 459 if ($namespaceURI != null) { 460 $nameSpace[$localName] = $namespaceURI; 461 } 462 } 463 return $nameSpace; 464 } 465 466 /** 467 * A wrapper that register namespace for the query 468 * with the defined prefix 469 * See comment: 470 * https://www.php.net/manual/en/domxpath.registernamespace.php#51480 471 * @param $query 472 * @param DOMElement|null $contextNode 473 * @return DOMNodeList 474 * 475 * Note that this is possible to do evaluation to return a string instead 476 * https://www.php.net/manual/en/domxpath.evaluate.php 477 * @throws ExceptionBadSyntax - if the query is invalid 478 */ 479 public 480 function xpath($query, DOMElement $contextNode = null): DOMNodeList 481 { 482 if (!isset($this->domXpath)) { 483 484 $this->domXpath = new DOMXPath($this->getDomDocument()); 485 486 /** 487 * Prefix mapping 488 * It is necessary to use xpath to handle documents which have default namespaces. 489 * The xpath expression will search for items with no namespace by default. 490 */ 491 foreach ($this->getNamespaces() as $prefix => $namespaceUri) { 492 /** 493 * You can't register an empty prefix 494 * Default namespace (without a prefix) can only be accessed by the local-name() and namespace-uri() attributes. 495 */ 496 if (!empty($prefix)) { 497 $result = $this->domXpath->registerNamespace($prefix, $namespaceUri); 498 if (!$result) { 499 LogUtility::msg("Not able to register the prefix ($prefix) for the namespace uri ($namespaceUri)"); 500 } 501 } 502 } 503 } 504 505 if ($contextNode === null) { 506 $contextNode = $this->domDocument; 507 } 508 $domList = $this->domXpath->query($query, $contextNode); 509 if ($domList === false) { 510 throw new ExceptionBadSyntax("The query expression ($query) may be malformed"); 511 } 512 return $domList; 513 514 } 515 516 517 public 518 function removeRootAttribute($attribute) 519 { 520 521 // This function does not work 522 // $result = $this->getXmlDom()->documentElement->removeAttribute($attribute); 523 524 for ($i = 0; $i < $this->getDomDocument()->documentElement->attributes->length; $i++) { 525 if ($this->getDomDocument()->documentElement->attributes[$i]->name == $attribute) { 526 $result = $this->getDomDocument()->documentElement->removeAttributeNode($this->getDomDocument()->documentElement->attributes[$i]); 527 if ($result === false) { 528 throw new \RuntimeException("Not able to delete the $attribute"); 529 } 530 // There is no break here because you may find multiple version attribute for instance 531 } 532 } 533 534 } 535 536 public 537 function removeRootChildNode($nodeName) 538 { 539 for ($i = 0; $i < $this->getDomDocument()->documentElement->childNodes->length; $i++) { 540 $childNode = &$this->getDomDocument()->documentElement->childNodes[$i]; 541 if ($childNode->nodeName == $nodeName) { 542 $result = $this->getDomDocument()->documentElement->removeChild($childNode); 543 if ($result == false) { 544 throw new \RuntimeException("Not able to delete the child node $nodeName"); 545 } 546 break; 547 } 548 } 549 } 550 551 /** 552 * 553 * Add a value to an attribute value 554 * Example 555 * <a class="actual"> 556 * 557 * if you add "new" 558 * <a class="actual new"> 559 * 560 * @param $attName 561 * @param $attValue 562 * @param DOMElement $xml 563 */ 564 public 565 function addAttributeValue($attName, $attValue, $xml) 566 { 567 568 /** 569 * Empty condition is better than {@link DOMElement::hasAttribute()} 570 * because even if the dom element has the attribute, the value 571 * may be empty 572 */ 573 $value = $xml->getAttribute($attName); 574 if (empty($value)) { 575 $xml->setAttribute($attName, $attValue); 576 } else { 577 $actualAttValue = $xml->getAttribute($attName); 578 $explodeArray = explode(" ", $actualAttValue); 579 if (!in_array($attValue, $explodeArray)) { 580 $xml->setAttribute($attName, (string)$actualAttValue . " $attValue"); 581 } 582 } 583 584 } 585 586 public function diff(XmlDocument $rightDocument): string 587 { 588 $error = ""; 589 XmlSystems::diffNode($this->getDomDocument(), $rightDocument->getDomDocument(), $error); 590 return $error; 591 } 592 593 /** 594 * @return string a XML formatted 595 * 596 * !!!! The parameter preserveWhiteSpace should have been set to false before loading 597 * https://www.php.net/manual/en/class.domdocument.php#domdocument.props.formatoutput 598 * $this->xmlDom->preserveWhiteSpace = false; 599 * 600 * We do it with the function {@link XmlDocument::mandatoryFormatConfigBeforeLoading()} 601 * 602 */ 603 public function toXmlFormatted(DOMElement $element = null): string 604 { 605 606 $this->domDocument->formatOutput = true; 607 return $this->toXml($element); 608 609 } 610 611 /** 612 * @return string that can be diff 613 * * EOL diff are not seen 614 * * space are 615 * 616 * See also {@link XmlDocument::processTextBeforeLoading()} 617 * that is needed before loading 618 */ 619 public function toXmlNormalized(DOMElement $element = null): string 620 { 621 622 /** 623 * If the text was a list 624 * of sibling text without parent 625 * We may get a body 626 * @deprecated letting the code until 627 * TODO: delete this code when the test pass 628 */ 629// $body = $doc->getElementsByTagName("body"); 630// if ($body->length != 0) { 631// $DOMNodeList = $body->item(0)->childNodes; 632// $output = ""; 633// foreach ($DOMNodeList as $value) { 634// $output .= $doc->saveXML($value) . DOKU_LF; 635// } 636// } 637 638 if ($element == null) { 639 $element = $this->domDocument->documentElement; 640 } 641 $element->normalize(); 642 return $this->toXmlFormatted($element); 643 } 644 645 /** 646 * Not really conventional but 647 * to be able to {@link toXmlNormalized} 648 * the EOL should be deleted 649 * We do it before loading and not with a XML documentation 650 */ 651 private function processTextBeforeLoading($text) 652 { 653 $text = str_replace(DOKU_LF, "", $text); 654 $text = preg_replace("/\r\n\s*\r\n/", "\r\n", $text); 655 $text = preg_replace("/\n\s*\n/", "\n", $text); 656 $text = preg_replace("/\n\n/", "\n", $text); 657 return $text; 658 659 } 660 661 662 /** 663 * This function is called just before loading 664 * in order to be able to {@link XmlDocument::toXmlFormatted() format the output } 665 * https://www.php.net/manual/en/class.domdocument.php#domdocument.props.formatoutput 666 * Mandatory for a a good formatting before loading 667 * 668 */ 669 private function mandatoryFormatConfigBeforeLoading() 670 { 671 // not that 672 // the loading option: LIBXML_NOBLANKS 673 // is equivalent to $this->xmlDom->preserveWhiteSpace = true; 674 $this->domDocument->preserveWhiteSpace = false; 675 } 676 677 /** 678 * @param string $attributeName 679 * @param DOMElement $nodeElement 680 * @return void 681 * @deprecated use the {@link XmlElement::removeAttribute()} if possible 682 */ 683 public function removeAttributeValue(string $attributeName, DOMElement $nodeElement) 684 { 685 $attr = $nodeElement->getAttributeNode($attributeName); 686 if (!$attr) { 687 return; 688 } 689 $result = $nodeElement->removeAttributeNode($attr); 690 if ($result === false) { 691 LogUtility::msg("Not able to delete the attribute $attributeName of the node element $nodeElement->tagName in the Xml document"); 692 } 693 } 694 695 696 /** 697 * Query via a CSS selector 698 * (not that it will not work with other namespace than the default one, ie xmlns will not work) 699 * @throws ExceptionBadSyntax - if the selector is not valid 700 * @throws ExceptionNotFound - if the selector selects nothing 701 */ 702 public function querySelector(string $selector): XmlElement 703 { 704 $domNodeList = $this->querySelectorAll($selector); 705 if (sizeof($domNodeList) >= 1) { 706 return $domNodeList[0]; 707 } 708 throw new ExceptionNotFound("No element was found with the selector $selector"); 709 710 } 711 712 /** 713 * @return XmlElement[] 714 * @throws ExceptionBadSyntax 715 */ 716 public function querySelectorAll(string $selector): array 717 { 718 $xpath = $this->cssSelectorToXpath($selector); 719 $domNodeList = $this->xpath($xpath); 720 $domNodes = []; 721 foreach ($domNodeList as $domNode) { 722 if ($domNode instanceof DOMElement) { 723 $domNodes[] = new XmlElement($domNode, $this); 724 } 725 } 726 return $domNodes; 727 728 } 729 730 /** 731 * @throws ExceptionBadSyntax 732 */ 733 public function cssSelectorToXpath(string $selector): string 734 { 735 try { 736 return PhpCss::toXpath($selector); 737 } catch (PhpCss\Exception\ParserException $e) { 738 throw new ExceptionBadSyntax("The selector ($selector) is not valid. Error: {$e->getMessage()}"); 739 } 740 } 741 742 /** 743 * An utility function to know how to remove a node 744 * @param \DOMNode $nodeElement 745 * @deprecated use {@link XmlElement::remove} instead 746 */ 747 public function removeNode(\DOMNode $nodeElement) 748 { 749 750 $nodeElement->parentNode->removeChild($nodeElement); 751 752 } 753 754 public function getElement(): XmlElement 755 { 756 return XmlElement::create($this->getDomDocument()->documentElement, $this); 757 } 758 759 public function toHtml() 760 { 761 return $this->domDocument->saveHTML(); 762 } 763 764 /** 765 * @throws \DOMException - if invalid local name 766 */ 767 public function createElement(string $localName): XmlElement 768 { 769 $element = $this->domDocument->createElement($localName); 770 return XmlElement::create($element, $this); 771 } 772 773 /** 774 * @throws ExceptionBadSyntax 775 * @throws ExceptionBadState 776 */ 777 public function xpathFirstDomElement(string $xpath): DOMElement 778 { 779 $domList = $this->xpath($xpath); 780 $domElement = $domList->item(0); 781 if ($domElement instanceof DOMElement) { 782 return $domElement; 783 } else { 784 throw new ExceptionBadState("The first DOM node is not a DOM element"); 785 } 786 } 787 788 789} 790