diff --git a/lib/DOM/Document.php b/lib/DOM/Document.php index 18c000f..40fcc04 100644 --- a/lib/DOM/Document.php +++ b/lib/DOM/Document.php @@ -50,15 +50,19 @@ class Document extends \DOMDocument { public function createAttributeNS($namespaceURI, $qualifiedName) { try { - return parent::createAttributeNS($namespaceURI, $qualifiedName); + $out = @parent::createAttributeNS($namespaceURI, $qualifiedName); } catch (\DOMException $e) { // The element name is invalid for XML // Replace any offending characters with "UHHHHHH" where H are the // uppercase hexadecimal digits of the character's code point $this->mangledAttributes = true; $qualifiedName = $this->coerceName($qualifiedName); - return parent::createAttributeNS($namespaceURI, $qualifiedName); + $out = parent::createAttributeNS($namespaceURI, $qualifiedName); } + if ($out === false) { + throw new \DOMException("Document element must be inserted first"); + } + return $out; } public function createElement($name, $value = "") { diff --git a/lib/DOM/Element.php b/lib/DOM/Element.php index 160e551..679982d 100644 --- a/lib/DOM/Element.php +++ b/lib/DOM/Element.php @@ -50,52 +50,54 @@ class Element extends \DOMElement { } public function setAttribute($name, $value) { - try { - // If setting a class attribute and classList has been invoked use classList to - // set it. - if ($this->_classList !== null && $name === 'class') { - $this->_classList->value = $value; - } else { + // If setting a class attribute and classList has been invoked use classList to + // set it. + if ($name === 'class' && $this->_classList !== null) { + $this->_classList->value = $value; + } else { + try { + parent::setAttribute($name, $value); + } catch (\DOMException $e) { + // The attribute name is invalid for XML + // Replace any offending characters with "UHHHHHH" where H are the + // uppercase hexadecimal digits of the character's code point + $this->ownerDocument->mangledAttributes = true; + $name = $this->coerceName($name); parent::setAttribute($name, $value); } - } catch (\DOMException $e) { - // The attribute name is invalid for XML - // Replace any offending characters with "UHHHHHH" where H are the - // uppercase hexadecimal digits of the character's code point - $this->ownerDocument->mangledAttributes = true; - $name = $this->coerceName($name); - parent::setAttribute($name, $value); - } - if ($name === "id") { - $this->setIdAttribute($name, true); + if ($name === "id") { + $this->setIdAttribute($name, true); + } } } public function setAttributeNS($namespaceURI, $qualifiedName, $value) { - try { - // If setting a class attribute and classList has been invoked use classList to - // set it. - if ($this->_classList !== null && $namespaceURI === null && $qualifiedName === 'class') { - $this->_classList->value = $value; - } else { - parent::setAttributeNS($namespaceURI, $qualifiedName, $value); + // If setting a class attribute and classList has been invoked use classList to + // set it. + if ($qualifiedName === 'class' && $namespaceURI === null && $this->_classList !== null) { + $this->_classList->value = $value; + } else { + try { + // NOTE: We create attribute nodes so that xmlns attributes don't get lost + $a = $this->ownerDocument->createAttributeNS($namespaceURI, $qualifiedName); + } catch (\DOMException $e) { + // The attribute name is invalid for XML + // Replace any offending characters with "UHHHHHH" where H are the + // uppercase hexadecimal digits of the character's code point + $this->ownerDocument->mangledAttributes = true; + $qualifiedName = $this->coerceName($qualifiedName); + $a = $this->ownerDocument->createAttributeNS($namespaceURI, $qualifiedName); + } + $a->value = $this->escapeString($value, true); + $this->appendChild($a); + if ($qualifiedName === "id" && $namespaceURI === null) { + $this->setIdAttribute($qualifiedName, true); } - } catch (\DOMException $e) { - // The attribute name is invalid for XML - // Replace any offending characters with "UHHHHHH" where H are the - // uppercase hexadecimal digits of the character's code point - $this->ownerDocument->mangledAttributes = true; - $qualifiedName = $this->coerceName($qualifiedName); - parent::setAttributeNS($namespaceURI, $qualifiedName, $value); - } - if ($qualifiedName === "id" && $namespaceURI === null) { - $this->setIdAttribute($qualifiedName, true); } } public function setAttributeNode(\DOMAttr $attribute) { parent::setAttributeNode($attribute); - if ($attribute->name === 'id') { $this->setIdAttribute($attribute->name, true); } @@ -103,8 +105,7 @@ class Element extends \DOMElement { public function setAttributeNodeNS(\DOMAttr $attribute) { parent::setAttributeNodeNS($attribute); - - if ($attribute->name === 'id') { + if ($attribute->name === 'id' && $attribute->namespaceURI === null) { $this->setIdAttribute($attribute->name, true); } } diff --git a/lib/TreeBuilder.php b/lib/TreeBuilder.php index ace1ae4..7060d79 100644 --- a/lib/TreeBuilder.php +++ b/lib/TreeBuilder.php @@ -4150,6 +4150,12 @@ class TreeBuilder { # Let element be the result of creating an element given document, # localName, given namespace, null, and is. $element = $this->DOM->createElementNS($namespace, $token->name); + // DEVIATION: If there is no document (root) element yet, temporarily + // insert this element so that creating attributes for it does not + // fail due to a PHP DOM limitation + if (!$this->DOM->documentElement) { + $this->DOM->appendChild($element); + } # Append each attribute in the given token to element. foreach ($token->attributes as $attr) { # If element has an xmlns attribute in the XMLNS namespace whose value @@ -4168,6 +4174,10 @@ class TreeBuilder { $element->setAttributeNS($attr->namespace, $attr->name, $attr->value); } } + if ($this->DOM->documentElement && $this->DOM->documentElement->isSameNode($element)) { + // Pop off the document element if it was inserted above + $this->DOM->removeChild($element); + } # Return element. return $element; } diff --git a/tests/cases/TestSerializer.php b/tests/cases/TestSerializer.php index d210b1a..73fdfde 100644 --- a/tests/cases/TestSerializer.php +++ b/tests/cases/TestSerializer.php @@ -17,7 +17,7 @@ use MensBeam\HTML\Parser; * @covers \MensBeam\HTML\Comment * @covers \MensBeam\HTML\Text */ -class TestTreeConstructor extends \PHPUnit\Framework\TestCase { +class TestSerializer extends \PHPUnit\Framework\TestCase { use \MensBeam\HTML\EscapeString; protected $out; @@ -85,6 +85,7 @@ class TestTreeConstructor extends \PHPUnit\Framework\TestCase { protected function buildTree(array $data, bool $fragment): \DOMNode { $document = new Document; + $document->appendChild($document->createElement("html")); $out = $fragment ? $document->createDocumentFragment() : $document; $cur = $out; $pad = 2; diff --git a/tests/cases/tree-construction/mensbeam01.dat b/tests/cases/tree-construction/mensbeam01.dat index f960495..08a8f66 100644 --- a/tests/cases/tree-construction/mensbeam01.dat +++ b/tests/cases/tree-construction/mensbeam01.dat @@ -7,6 +7,7 @@ | | | +| xmlns xmlns="http://www.w3.org/2000/svg" #data @@ -28,6 +29,7 @@ | | | +| xmlns xlink="http://www.w3.org/1999/xlink" #data