diff --git a/composer.json b/composer.json index 10b7d86..c3a3024 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "Modern DOM library written in PHP for HTML documents", "type": "library", "require": { - "php": ">=7.1", + "php": ">=7.4", "ext-dom": "*", "mensbeam/html-parser": "dev-master" }, diff --git a/lib/AbstractDocument.php b/lib/Attr.php similarity index 50% rename from lib/AbstractDocument.php rename to lib/Attr.php index 3a6d1f1..d6a90dd 100644 --- a/lib/AbstractDocument.php +++ b/lib/Attr.php @@ -8,7 +8,7 @@ declare(strict_types=1); namespace MensBeam\HTML\DOM; -// Exists so Document can extend methods from its traits. -abstract class AbstractDocument extends \DOMDocument { - use DocumentOrElement, MagicProperties, ParentNode, Walk; + +class Attr extends \DOMAttr { + use Node; } diff --git a/lib/Document.php b/lib/Document.php index be435ad..3ebf25c 100644 --- a/lib/Document.php +++ b/lib/Document.php @@ -11,7 +11,9 @@ use MensBeam\HTML\Parser, MensBeam\HTML\Parser\Data; -class Document extends AbstractDocument { +class Document extends \DOMDocument { + use DocumentOrElement, MagicProperties, ParentNode, Walk; + protected $_body = null; /** Nonstandard */ protected $_documentEncoding = null; @@ -130,6 +132,7 @@ class Document extends AbstractDocument { parent::__construct(); + $this->registerNodeClass('DOMAttr', '\MensBeam\HTML\DOM\Attr'); $this->registerNodeClass('DOMDocument', '\MensBeam\HTML\DOM\Document'); $this->registerNodeClass('DOMComment', '\MensBeam\HTML\DOM\Comment'); $this->registerNodeClass('DOMDocumentFragment', '\MensBeam\HTML\DOM\DocumentFragment'); @@ -176,32 +179,33 @@ class Document extends AbstractDocument { } } - public function createAttributeNS($namespaceURI, $qualifiedName): \DOMAttr { + public function createAttributeNS(?string $namespace, string $qualifiedName): \DOMAttr { # The createAttributeNS(namespace, qualifiedName) method steps are: # 1. Let namespace, prefix, and localName be the result of passing namespace and # qualifiedName to validate and extract. - [ 'namespace' => $namespaceURI, 'prefix' => $prefix, 'localName' => $localName ] = $this->validateAndExtract($qualifiedName, $namespaceURI); + [ 'namespace' => $namespace, 'prefix' => $prefix, 'localName' => $localName ] = $this->validateAndExtract($qualifiedName, $namespace); # 2. Return a new attribute whose namespace is namespace, namespace prefix is # prefix, local name is localName, and node document is this. // We need to do a couple more things here. PHP's XML-based DOM doesn't allow // some characters. We have to coerce them sometimes. try { - return parent::createAttributeNS($namespaceURI, $qualifiedName); + return parent::createAttributeNS($namespace, $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 - if ($namespaceURI !== null) { - $qualifiedName = implode(":", array_map([$this, "coerceName"], explode(":", $qualifiedName, 2))); + if ($namespace !== null) { + $qualifiedName = implode(':', array_map([ $this, 'coerceName' ], explode(':', $qualifiedName, 2))); } else { $qualifiedName = $this->coerceName($qualifiedName); } - return parent::createAttributeNS($namespaceURI, $qualifiedName); + + return parent::createAttributeNS($namespace, $qualifiedName); } } - public function createElement($name, $value = null): Element { + public function createElement(string $name, ?string $value = null): Element { # The createElement(localName, options) method steps are: // DEVIATION: We cannot follow the createElement parameters per the DOM spec // because we cannot change the parameters from \DOMDOcument. This is okay @@ -415,92 +419,6 @@ class Document extends AbstractDocument { } - protected function preInsertionValidity(\DOMNode $node, ?\DOMNode $child = null) { - parent::preInsertionValidity($node, $child); - - # 6. If parent is a document, and any of the statements below, switched on node, - # are true, then throw a "HierarchyRequestError" DOMException. - # - # DocumentFragment node - # If node has more than one element child or has a Text node child. - # Otherwise, if node has one element child and either parent has an element - # child, child is a doctype, or child is non-null and a doctype is following - # child. - if ($node instanceof \DOMDocumentType) { - if ($node->childNodes->length > 1 || $node->firstChild instanceof Text) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); - } else { - if ($node->firstChild instanceof \DOMDocumentType) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); - } - - foreach ($this->childNodes as $c) { - if ($c instanceof Element) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); - } - } - - if ($child !== null) { - $n = $child; - while ($n = $n->nextSibling) { - if ($n instanceof \DOMDocumentType) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); - } - } - } - } - } - # element - # parent has an element child, child is a doctype, or child is non-null and a - # doctype is following child. - elseif ($node instanceof Element) { - if ($child instanceof \DOMDocumentType) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); - } - - if ($child !== null) { - $n = $child; - while ($n = $n->nextSibling) { - if ($n instanceof \DOMDocumentType) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); - } - } - } - - foreach ($this->childNodes as $c) { - if ($c instanceof Element) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); - } - } - } - - # doctype - # parent has a doctype child, child is non-null and an element is preceding - # child, or child is null and parent has an element child. - elseif ($node instanceof \DOMDocumentType) { - foreach ($this->childNodes as $c) { - if ($c instanceof \DOMDocumentType) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); - } - } - - if ($child !== null) { - $n = $child; - while ($n = $n->prevSibling) { - if ($n instanceof Element) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); - } - } - } else { - foreach ($this->childNodes as $c) { - if ($c instanceof Element) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); - } - } - } - } - } - protected function serializeBlockElementFilter(\DOMNode $ignoredNode): \Closure { $blockElementFilter = function($n) use ($ignoredNode) { if (!$n->isSameNode($ignoredNode) && $n instanceof Element && $this->isHTMLNamespace($n) && (in_array($n->nodeName, self::BLOCK_ELEMENTS) || $n->walk(function($nn) { diff --git a/lib/DocumentFragment.php b/lib/DocumentFragment.php index a87f069..b14815b 100644 --- a/lib/DocumentFragment.php +++ b/lib/DocumentFragment.php @@ -8,8 +8,48 @@ declare(strict_types=1); namespace MensBeam\HTML\DOM; + class DocumentFragment extends \DOMDocumentFragment { - use ParentNode, Walk; + use MagicProperties, ParentNode, Walk; + + protected $_host = null; + + protected function __get_host(): ?\DOMNode { + if ($this->_host === null) { + return $this->_host; + } + + return $this->_host->get(); + } + + protected function __set_host(\DOMNode $value) { + if ($this->_host !== null) { + throw new Exception(Exception::READONLY_PROPERTY, 'host'); + } + + // Check to see if this is being set within the HTMLTemplateElement constructor + // and throw a read only exception otherwise. This will ensure the host remains + // readonly. YES. THIS IS DIRTY. We shouldn't do this, but there is no other + // option. While DocumentFragment could be created via a constructor it cannot + // be associated with a document unless created by + // Document::createDocumentFragment. + $backtrace = debug_backtrace(); + $okay = false; + for ($len = count($backtrace), $i = $len - 1; $i >= 0; $i--) { + $cur = $backtrace[$i]; + if ($cur['function'] === '__construct' && $cur['class'] === __NAMESPACE__ . '\\HTMLTemplateElement') { + $okay = true; + break; + } + } + + if (!$okay) { + throw new Exception(Exception::READONLY_PROPERTY, 'host'); + } + + $this->_host = \WeakReference::create($value); + } + public function __toString() { return $this->ownerDocument->saveHTML($this); diff --git a/lib/Element.php b/lib/Element.php index 0b76467..f142edc 100644 --- a/lib/Element.php +++ b/lib/Element.php @@ -17,16 +17,12 @@ class Element extends \DOMElement { protected function __get_classList(): ?TokenList { - // MensBeam\HTML\DOM\TokenList uses WeakReference to prevent a circular reference, - // so it requires PHP 7.4 to work. - if (version_compare(\PHP_VERSION, '7.4.0', '>=')) { - // Only create the class list if it is actually used. - if ($this->_classList === null) { - $this->_classList = new TokenList($this, 'class'); - } - return $this->_classList; + // Only create the class list if it is actually used. + if ($this->_classList === null) { + $this->_classList = new TokenList($this, 'class'); } - return null; // @codeCoverageIgnore + + return $this->_classList; } protected function __get_innerHTML(): string { diff --git a/lib/HTMLTemplateElement.php b/lib/HTMLTemplateElement.php index 9062080..7138f15 100644 --- a/lib/HTMLTemplateElement.php +++ b/lib/HTMLTemplateElement.php @@ -24,7 +24,9 @@ class HTMLTemplateElement extends Element { $frag->removeChild($this); unset($frag); - $this->content = $this->ownerDocument->createDocumentFragment(); + $content = $this->ownerDocument->createDocumentFragment(); + $content->host = $this; + $this->content = $content; } diff --git a/lib/traits/Node.php b/lib/traits/Node.php index c5035dd..71e6915 100644 --- a/lib/traits/Node.php +++ b/lib/traits/Node.php @@ -21,4 +21,24 @@ trait Node { public function C14NFile($uri, $exclusive = null, $with_comments = null, ?array $xpath = null, ?array $ns_prefixes = null): bool { return false; } + + public function getRootNode(): ?\DOMNode { + # The getRootNode(options) method steps are to return this’s shadow-including + # root if options["composed"] is true; otherwise this’s root. + // DEVIATION: This implementation does not have scripting, so there's no Shadow + // DOM. Therefore, there isn't a need for the options parameter. + + # The root of an object is itself, if its parent is null, or else it is the root + # of its parent. The root of a tree is any object participating in that tree + # whose parent is null. + if ($this->parentNode === null) { + return $this; + } + + return $this->moonwalk(function($n) { + if ($n->parentNode === null) { + return true; + } + })->current(); + } } diff --git a/lib/traits/ParentNode.php b/lib/traits/ParentNode.php index 39458ec..7630735 100644 --- a/lib/traits/ParentNode.php +++ b/lib/traits/ParentNode.php @@ -120,41 +120,137 @@ trait ParentNode { // "parent" in the spec comments below is $this # 1. If parent is not a Document, DocumentFragment, or Element node, then throw - # a "HierarchyRequestError" DOMException. + # a "HierarchyRequestError" DOMException. // Not necessary because they've been disabled and return hierarchy request - // errors in "leaf nodes". + // errors in ChildNode trait. # 2. If node is a host-including inclusive ancestor of parent, then throw a - # "HierarchyRequestError" DOMException. + # "HierarchyRequestError" DOMException. # # An object A is a host-including inclusive ancestor of an object B, if either # A is an inclusive ancestor of B, or if B’s root has a non-null host and A is a # host-including inclusive ancestor of B’s root’s host. - // DEVIATION: The baseline for this library is PHP 7.1, and without - // WeakReferences we cannot add a host property to DocumentFragment to check - // against. - // This is handled just fine by PHP's DOM. + if ($node->parentNode !== null) { + if ($this->isSameNode($node) || $this->moonwalk(function($n) use($node) { + if ($n->isSameNode($node)) { + return true; + } + })->current() !== null) { + throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + } else { + $parentRoot = $this->getRootNode(); + $parentRootHost = $parentRoot->host; + if ($parentRoot instanceof DocumentFragment && $parentRootHost !== null && ($host->isSameNode($node) || $host->moonwalk(function($n) use($node) { + if ($n->isSameNode($node)) { + return true; + } + })->current() !== null)) { + throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + } + } + } # 3. If child is non-null and its parent is not parent, then throw a - # "NotFoundError" DOMException. - // This is handled just fine by PHP's DOM. + # "NotFoundError" DOMException. + if ($child !== null && ($child->parentNode === null || !$child->parentNode->isSameNode($this))) { + throw new DOMException(DOMException::NOT_FOUND); + } # 4. If node is not a DocumentFragment, DocumentType, Element, Text, - # ProcessingInstruction, or Comment node, then throw a "HierarchyRequestError" - # DOMException. + # ProcessingInstruction, or Comment node, then throw a "HierarchyRequestError" + # DOMException. if (!$node instanceof DocumentFragment && !$node instanceof \DOMDocumentType && !$node instanceof Element && !$node instanceof Text && !$node instanceof ProcessingInstruction && !$node instanceof Comment) { throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); } # 5. If either node is a Text node and parent is a document, or node is a - # doctype and parent is not a document, then throw a "HierarchyRequestError" - # DOMException. + # doctype and parent is not a document, then throw a "HierarchyRequestError" + # DOMException. // Not necessary because they've been disabled and return hierarchy request - // errors in "leaf nodes". + // errors in ChildNode trait # 6. If parent is a document, and any of the statements below, switched on node, - # are true, then throw a "HierarchyRequestError" DOMException. - // Handled by the Document class. + # are true, then throw a "HierarchyRequestError" DOMException. + if ($this instanceof Document) { + # DocumentFragment node + # If node has more than one element child or has a Text node child. + # Otherwise, if node has one element child and either parent has an element + # child, child is a doctype, or child is non-null and a doctype is following + # child. + if ($node instanceof \DOMDocumentType) { + if ($node->childNodes->length > 1 || $node->firstChild instanceof Text) { + throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + } else { + if ($node->firstChild instanceof \DOMDocumentType) { + throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + } + + foreach ($this->childNodes as $c) { + if ($c instanceof Element) { + throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + } + } + + if ($child !== null) { + $n = $child; + while ($n = $n->nextSibling) { + if ($n instanceof \DOMDocumentType) { + throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + } + } + } + } + } + # element + # parent has an element child, child is a doctype, or child is non-null and a + # doctype is following child. + elseif ($node instanceof Element) { + if ($child instanceof \DOMDocumentType) { + throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + } + + if ($child !== null) { + $n = $child; + while ($n = $n->nextSibling) { + if ($n instanceof \DOMDocumentType) { + throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + } + } + } + + foreach ($this->childNodes as $c) { + if ($c instanceof Element) { + throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + } + } + } + + # doctype + # parent has a doctype child, child is non-null and an element is preceding + # child, or child is null and parent has an element child. + elseif ($node instanceof \DOMDocumentType) { + foreach ($this->childNodes as $c) { + if ($c instanceof \DOMDocumentType) { + throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + } + } + + if ($child !== null) { + $n = $child; + while ($n = $n->prevSibling) { + if ($n instanceof Element) { + throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + } + } + } else { + foreach ($this->childNodes as $c) { + if ($c instanceof Element) { + throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + } + } + } + } + } } diff --git a/tests/cases/TestDocument.php b/tests/cases/TestDocument.php index 10702b2..0bd5bf9 100644 --- a/tests/cases/TestDocument.php +++ b/tests/cases/TestDocument.php @@ -43,11 +43,27 @@ class TestDocument extends \PHPUnit\Framework\TestCase { } + /** + * @covers \MensBeam\HTML\DOM\Document::createAttribute + */ + public function testAttributeNodeCreationFailure(): void { + $this->expectException(DOMException::class); + $this->expectExceptionCode(DOMException::INVALID_CHARACTER); + $d = new Document(); + $d->createAttribute(''); + } + + public function provideAttributeNodeNSCreation(): iterable { return [ - [ 'fake_ns', 'test', 'test', '' ], - [ 'fake_ns', 'test:test', 'test', 'test' ], - [ 'fake_ns', 'TEST:TEST', 'TEST', 'TEST' ] + [ 'fake_ns', 'test', 'fake_ns', '', 'test' ], + [ 'fake_ns', 'test:test', 'fake_ns', 'test', 'test' ], + [ 'fake_ns', 'TEST:TEST', 'fake_ns', 'TEST', 'TEST' ], + [ 'another_fake_ns', 'steaming💩:poop💩', 'another_fake_ns', 'steamingU01F4A9', 'poopU01F4A9' ], + // An empty string for a prefix is technically incorrect, but we cannot fix that. + [ '', 'poop💩', null, '', 'poopU01F4A9' ], + // An empty string for a prefix is technically incorrect, but we cannot fix that. + [ null, 'poop💩', null, '', 'poopU01F4A9' ] ]; } @@ -56,13 +72,13 @@ class TestDocument extends \PHPUnit\Framework\TestCase { * @covers \MensBeam\HTML\DOM\Document::createAttributeNS * @covers \MensBeam\HTML\DOM\Document::validateAndExtract */ - public function testAttributeNodeNSCreation(?string $nsIn, string $nameIn, string $local, string $prefix): void { + public function testAttributeNodeNSCreation(?string $nsIn, string $nameIn, ?string $nsExpected, ?string $prefixExpected, string $localNameExpected): void { $d = new Document(); $d->appendChild($d->createElement('html')); $a = $d->createAttributeNS($nsIn, $nameIn); - $this->assertSame($local, $a->localName); - $this->assertSame($nsIn, $a->namespaceURI); - $this->assertSame($prefix, $a->prefix); + $this->assertSame($nsExpected, $a->namespaceURI); + $this->assertSame($prefixExpected, $a->prefix); + $this->assertSame($localNameExpected, $a->localName); } @@ -117,8 +133,9 @@ class TestDocument extends \PHPUnit\Framework\TestCase { $d2 = new Document(); $d2->appendChild($d2->createElement('html')); $d2->loadDOM($d); - $this->assertSame('MensBeam\HTML\DOM\Element', $d2->firstChild::class); - $this->assertSame('html', $d2->firstChild->nodeName); + $d3 = new Document($d); + $this->assertSame('MensBeam\HTML\DOM\Element', $d3->firstChild::class); + $this->assertSame('html', $d3->firstChild->nodeName); // Test file source $vfs = vfsStream::setup('DOM', 0777, [ 'test.html' => <<createElement($localIn); - $this->assertInstanceOf($class, $n); + $n = $d->createElement($nameIn); + $this->assertInstanceOf($classExpected, $n); $this->assertNotNull($n->ownerDocument); - $this->assertSame($localOut, $n->localName); + $this->assertSame($nameExpected, $n->nodeName); + } + + + public function provideElementCreationFailures(): iterable { + return [ + [ function() { + $d = new Document(); + $d->createElement('ook', 'FAIL'); + }, DOMException::NOT_SUPPORTED ], + [ function() { + $d = new Document(); + $d->createElement(''); + }, DOMException::INVALID_CHARACTER ] + ]; + } + + + /** + * @dataProvider provideElementCreationFailures + * @covers \MensBeam\HTML\DOM\Document::__construct + */ + public function testElementCreationFailures(\Closure $closure, int $errorCode): void { + $this->expectException(DOMException::class); + $this->expectExceptionCode($errorCode); + $closure(); } public function provideElementCreationNS(): iterable { return [ // HTML element with a null namespace - [ null, null, 'div', 'div', Element::class ], + [ null, null, 'div', 'div', Element::class ], // Template element with a null namespace - [ null, null, 'template', 'template', HTMLTemplateElement::class ], + [ null, null, 'template', 'template', HTMLTemplateElement::class ], // Template element with a null namespace and uppercase name - [ null, null, 'TEMPLATE', 'TEMPLATE', HTMLTemplateElement::class ], + [ null, null, 'TEMPLATE', 'TEMPLATE', HTMLTemplateElement::class ], // Template element - [ Parser::HTML_NAMESPACE, Parser::HTML_NAMESPACE, 'template', 'template', HTMLTemplateElement::class ], + [ Parser::HTML_NAMESPACE, Parser::HTML_NAMESPACE, 'template', 'template', HTMLTemplateElement::class ], // SVG element with SVG namespace - [ Parser::SVG_NAMESPACE, Parser::SVG_NAMESPACE, 'svg', 'svg', Element::class ], + [ Parser::SVG_NAMESPACE, Parser::SVG_NAMESPACE, 'svg', 'svg', Element::class ], // SVG element with SVG namespace and uppercase local name - [ Parser::SVG_NAMESPACE, Parser::SVG_NAMESPACE, 'SVG', 'SVG', Element::class ] + [ Parser::SVG_NAMESPACE, Parser::SVG_NAMESPACE, 'SVG', 'SVG', Element::class ], + // Name coercion + [ 'steaming💩', 'steaming💩', 'poop💩', 'poopU01F4A9', Element::class ] ]; } @@ -210,13 +256,42 @@ class TestDocument extends \PHPUnit\Framework\TestCase { * @covers \MensBeam\HTML\DOM\Document::createElementNS * @covers \MensBeam\HTML\DOM\Document::validateAndExtract */ - public function testElementCreationNS(?string $nsIn, ?string $nsOut, string $localIn, string $localOut, string $class): void { + public function testElementCreationNS(?string $nsIn, ?string $nsExpected, string $localNameIn, string $localNameExpected, string $classExpected): void { $d = new Document(); - $n = $d->createElementNS($nsIn, $localIn); - $this->assertInstanceOf($class, $n); + $n = $d->createElementNS($nsIn, $localNameIn); + $this->assertInstanceOf($classExpected, $n); $this->assertNotNull($n->ownerDocument); - $this->assertSame($nsOut, $n->namespaceURI); - $this->assertSame($localOut, $n->localName); + $this->assertSame($nsExpected, $n->namespaceURI); + $this->assertSame($localNameExpected, $n->localName); + } + + + public function provideElementCreationNSFailures(): iterable { + return [ + [ function() { + $d = new Document(); + $d->createElementNS('ook', 'ook', 'FAIL'); + }, DOMException::NOT_SUPPORTED ], + [ function() { + $d = new Document(); + $d->createElementNS(null, ''); + }, DOMException::INVALID_CHARACTER ], + [ function() { + $d = new Document(); + $d->createElementNS(null, 'xmlns'); + }, DOMException::NAMESPACE_ERROR ] + ]; + } + + /** + * @dataProvider provideElementCreationNSFailures + * @covers \MensBeam\HTML\DOM\Document::createElementNS + * @covers \MensBeam\HTML\DOM\Document::validateAndExtract + */ + public function testElementCreationNSFailures(\Closure $closure, int $errorCode): void { + $this->expectException(DOMException::class); + $this->expectExceptionCode($errorCode); + $closure(); } diff --git a/tests/cases/serializer/TestSerializer.php b/tests/cases/serializer/TestSerializer.php index 022f62e..a3827fb 100644 --- a/tests/cases/serializer/TestSerializer.php +++ b/tests/cases/serializer/TestSerializer.php @@ -8,7 +8,10 @@ declare(strict_types=1); namespace MensBeam\HTML\DOM\TestCase; -use MensBeam\HTML\DOM\Document; +use MensBeam\HTML\DOM\{ + Document, + DOMException +}; use MensBeam\HTML\Parser; /** @@ -150,4 +153,43 @@ class TestSerializer extends \PHPUnit\Framework\TestCase { } return $out; } + + + /** @covers \MensBeam\HTML\DOM\Document::saveHTML */ + public function testSerializingDocumentType(): void { + $d = new Document(); + $dt = $d->implementation->createDocumentType('ook', 'eek', 'ack'); + $d->appendChild($dt); + $this->assertSame('', $d->saveHTML($dt)); + } + + + /** + * @covers \MensBeam\HTML\DOM\Document::saveHTML + * @covers \MensBeam\HTML\DOM\Document::serializeFragment + * @covers \MensBeam\HTML\DOM\ToString::__toString + */ + public function testSerializingElements(): void { + $d = new Document(); + $i = $d->createElement('input'); + $i->appendChild($d->createTextNode('You should not see this text')); + $this->assertSame('', (string)$i); + $this->assertSame('', $d->saveHTML($i)); + + $t = $d->createElement('template'); + $t->content->appendChild($d->createTextNode('Ook!')); + $this->assertSame('', (string)$t); + $this->assertSame('Ook!', $d->saveHTML($t)); + } + + + /** @covers \MensBeam\HTML\DOM\Document::saveHTML */ + public function testSerializerFailure(): void { + $this->expectException(DOMException::class); + $this->expectExceptionCode(DOMException::WRONG_DOCUMENT); + $d = new Document(); + $h = $d->createElement('html'); + $d2 = new Document(); + $d2->saveHTML($h); + } }