diff --git a/lib/Attr.php b/lib/Attr.php index 2bc6f33..f82ed90 100644 --- a/lib/Attr.php +++ b/lib/Attr.php @@ -24,7 +24,7 @@ class Attr extends Node { return (!str_contains(needle: 'U', haystack: $name)) ? $name : $this->uncoerceName($name); } - protected function __get_namespaceURI(): string { + protected function __get_namespaceURI(): ?string { // PHP's DOM does this correctly already. return $this->innerNode->namespaceURI; } diff --git a/lib/Document.php b/lib/Document.php index 638b10b..4eb6a31 100644 --- a/lib/Document.php +++ b/lib/Document.php @@ -23,7 +23,7 @@ class Document extends Node { protected DOMImplementation $_implementation; protected string $_URL = ''; - protected function __get_body(): Element { + protected function __get_body(): ?Element { if ($this->documentElement === null || !$this->documentElement->hasChildNodes()) { return null; } @@ -126,7 +126,7 @@ class Document extends Node { return $this->innerNode->getWrapperNode($attr); } - public function createAttributeNS(string $namespace, string $qualifiedName): Attr { + public function createAttributeNS(?string $namespace, string $qualifiedName): Attr { # The createAttributeNS(namespace, qualifiedName) method steps are: # # 1. Let namespace, prefix, and localName be the result of passing namespace and diff --git a/lib/Element.php b/lib/Element.php index f47e00d..96093e8 100644 --- a/lib/Element.php +++ b/lib/Element.php @@ -13,6 +13,10 @@ use MensBeam\HTML\Parser; class Element extends Node { use ChildNode, DocumentOrElement, ParentNode; + protected function __get_localName(): ?string { + return $this->innerNode->localName; + } + protected function __get_namespaceURI(): string { // PHP's DOM uses null incorrectly for the HTML namespace, and if you attempt to // use the HTML namespace anyway it has additional bugs we don't have to work @@ -23,6 +27,10 @@ class Element extends Node { return (($doc instanceof Document && !$doc instanceof XMLDocument) && $namespace === null) ? Parser::HTML_NAMESPACE : $namespace; } + protected function __get_prefix(): ?string { + return $this->innerNode->prefix; + } + protected function __construct(\DOMElement $element) { parent::__construct($element); diff --git a/lib/Node.php b/lib/Node.php index fc550b3..d0be073 100644 --- a/lib/Node.php +++ b/lib/Node.php @@ -322,24 +322,110 @@ abstract class Node { } public function cloneNode(?bool $deep = false): Node { - // PHP's DOM does this correctly already. - $newInner = $this->innerNode->cloneNode($deep); + # The cloneNode(deep) method steps are: + # 1. If this is a shadow root, then throw a "NotSupportedError" DOMException. + // DEVIATION: There is no scripting in this implementation + + # 2. Return a clone of this, with the clone children flag set if deep is true. + # + # To clone a node, with an optional document and clone children flag, run these steps: + // node is $this + + # 1. If document is not given, let document be node’s node document. + // No need for this step. There will always be a provided document + + # 2. If node is an element, then: + if ($this instanceof Element) { + # 1. Let copy be the result of creating an element, given document, node’s local + # name, node’s namespace, node’s namespace prefix, and node’s is value, with the + # synchronous custom elements flag unset. + # 2. For each attribute in node’s attribute list: + # 1. Let copyAttribute be a clone of attribute. + # 2. Append copyAttribute to copy. + // PHP's DOM can do this part correctly by shallow cloning. + $copy = $this->innerNode->ownerDocument->getWrapperNode($this->innerNode->cloneNode()); + } + + # 3. Otherwise, let copy be a node that implements the same interfaces as node, and + # fulfills these additional requirements, switching on the interface node + # implements: + # + # ↪ Document + # Set copy’s encoding, content type, URL, origin, type, and mode to those of node. + elseif ($this instanceof Document) { + $copy = $this->innerNode->getWrapperNode($this->innerNode->cloneNode()); - // Documents have some userland properties to transfer - if ($this instanceof Document) { - $newDoc = $this->innerNode->getWrapperNode($newInner); if ($this->characterSet !== 'UTF-8' || $this->compatMode !== 'CSS1Compat' || $this->contentType !== 'text/html' || $this->URL !== '') { - Reflection::setProtectedProperties($newDoc, [ + Reflection::setProtectedProperties($copy, [ '_characterSet' => $this->characterSet, '_compatMode' => $this->compatMode, '_contentType' => $this->contentType, '_URL' => $this->URL ]); } - return $newDoc; } - return $this->innerNode->ownerDocument->getWrapperNode($newInner); + # ↪ DocumentType + # Set copy’s name, public ID, and system ID to those of node. + elseif ($this instanceof DocumentType) { + // OPTIMIZATION: No need for the other steps as the DocumentType node is created + // using this document's implementation + return $this->ownerDocument->implementation->createDocumentType($this->name, $this->publicId, $this->systemId); + } + + # ↪ Attr + # Set copy’s namespace, namespace prefix, local name, and value to those of node. + # ↪ Text + # ↪ Comment + # Set copy’s data to that of node. + # ↪ ProcessingInstruction + # Set copy’s target and data to those of node. + elseif ($this instanceof Attr || $this instanceof Text || $this instanceof Comment || $this instanceof ProcessingInstruction) { + // OPTIMIZATION: No need for the other steps as PHP's DOM handles this fine + return $this->innerNode->ownerDocument->getWrapperNode($this->innerNode->cloneNode()); + } + + # ↪ Otherwise + # Do nothing. + else { + $copy = $this->innerNode->ownerDocument->getWrapperNode($this->innerNode->cloneNode()); + } + + # 4. Set copy’s node document and document to copy, if copy is a document, and + # set copy’s node document to document otherwise. + // PHP's DOM does this already + + if ($deep) { + # 5. Run any cloning steps defined for node in other applicable specifications + # and pass copy, node, document and the clone children flag if set, as + # parameters. + if ($this instanceof HTMLTemplateElement) { + # The cloning steps for a template element node being cloned to a copy copy must + # run the following steps: + # + # 1. If the clone children flag is not set in the calling clone algorithm, return. + // This is done with the if statements above. + + # 2. Let copied contents be the result of cloning all the children of node's template + # contents, with document set to copy's template contents's node document, and + # with the clone children flag set. + # 3. Append copied contents to copy's template contents. + $copy->content = $this->content->cloneNode(true); + } + + # 6. If the clone children flag is set, clone all the children of node and append + # them to copy, with document as specified and the clone children flag being + # set. + if ($this instanceof Document || $this instanceof DocumentFragment || $this instanceof Element) { + $childNodes = $this->childNodes; + foreach ($childNodes as $child) { + $copy->appendChild($child->cloneNode(true)); + } + } + } + + # 7. Return copy. + return $copy; } public function compareDocumentPosition(Node $other): int { diff --git a/tests/cases/TestNode.php b/tests/cases/TestNode.php index ed8de4d..db4db45 100644 --- a/tests/cases/TestNode.php +++ b/tests/cases/TestNode.php @@ -41,6 +41,7 @@ class TestNode extends \PHPUnit\Framework\TestCase { * @covers \MensBeam\HTML\DOM\DOMImplementation::createDocumentType * @covers \MensBeam\HTML\DOM\Element::__construct * @covers \MensBeam\HTML\DOM\Node::__construct + * @covers \MensBeam\HTML\DOM\Node::isEqualNode * @covers \MensBeam\HTML\DOM\ProcessingInstruction::__construct * @covers \MensBeam\HTML\DOM\Text::__construct * @covers \MensBeam\HTML\DOM\InnerNode\Document::__construct @@ -56,10 +57,80 @@ class TestNode extends \PHPUnit\Framework\TestCase { */ public function testMethod_cloneNode() { $d = new Document(); + $d2 = new XMLDocument(); + $doctype = $d->appendChild($d->implementation->createDocumentType('html', '', '')); + $attr = $d->createAttribute('href'); + $attr->value = 'https://poop💩.poop'; + $cdata = $d2->createCDATASection('ook'); + $comment = $d->createComment('comment'); + $element = $d->createElement('html'); + $element->appendChild($d->createElement('body')); + $pi = $d->createProcessingInstruction('ook', 'eek'); + $text = $d->createTextNode('ook'); + $frag = $d->createDocumentFragment(); + $frag->appendChild($d->createTextNode('ook')); + + // Node::cloneNode on attribute node + $attrClone = $attr->cloneNode(); + $this->assertNotSame($attrClone, $attr); + $this->assertTrue($attrClone->isEqualNode($attr)); + + // Node::cloneNode on CDATA section + $cdataClone = $cdata->cloneNode(); + $this->assertNotSame($cdataClone, $cdata); + $this->assertTrue($cdataClone->isEqualNode($cdata)); + + // Node::cloneNode on comment + $commentClone = $comment->cloneNode(); + $this->assertNotSame($commentClone, $comment); + $this->assertTrue($commentClone->isEqualNode($comment)); + + // Node::cloneNode on document + $dClone = $d->cloneNode(true); + $this->assertNotSame($dClone, $d); + // Children on documents aren't cloned + $this->assertFalse($dClone->isEqualNode($d)); + + // Node::cloneNode on doctype + $doctypeClone = $doctype->cloneNode(); + $this->assertNotSame($doctypeClone, $doctype); + $this->assertTrue($doctypeClone->isEqualNode($doctype)); + + // Node::cloneNode on document fragment + $fragClone = $frag->cloneNode(true); + $this->assertNotSame($fragClone, $frag); + // Children on document fragments aren't cloned + $this->assertFalse($fragClone->isEqualNode($frag)); + + // Node::cloneNode on element + $elementClone = $element->cloneNode(true); + $this->assertNotSame($elementClone, $element); + // Children on documents aren't cloned + $this->assertTrue($elementClone->isEqualNode($element)); + + /* + + // Node::nodeType on comment + $this->assertSame($d, $d->createComment('comment')->ownerDocument); + + // Node::nodeType on document + $this->assertNull($d->ownerDocument); + + // Node::nodeType on doctype + $this->assertSame($d, $d->implementation->createDocumentType('html', '', '')->ownerDocument); + + // Node::nodeType on document fragment + $this->assertSame($d, $d->createDocumentFragment()->ownerDocument); - // Node::cloneNode on Document - $d2 = $d->cloneNode(true); - $this->assertSame(Document::class, $d2::class); + // Node::nodeType on element + $this->assertSame($d, $d->createElement('html')->ownerDocument); + + // Node::nodeType on processing instruction + $this->assertSame($d, $d->createProcessingInstruction('ook', 'eek')->ownerDocument); + + // Node::nodeType on text node + $this->assertSame($d, $d->createTextNode('ook')->ownerDocument); + */ }