diff --git a/lib/InnerNode/Attr.php b/lib/CDATASection.php similarity index 68% rename from lib/InnerNode/Attr.php rename to lib/CDATASection.php index 7184420..64269f8 100644 --- a/lib/InnerNode/Attr.php +++ b/lib/CDATASection.php @@ -6,7 +6,7 @@ */ declare(strict_types=1); -namespace MensBeam\HTML\DOM\InnerNode; +namespace MensBeam\HTML\DOM; -class Attr extends \DOMAttr {} +class CDATASection extends Text {} \ No newline at end of file diff --git a/lib/CharacterData.php b/lib/CharacterData.php new file mode 100644 index 0000000..130db49 --- /dev/null +++ b/lib/CharacterData.php @@ -0,0 +1,53 @@ +innerNode->data; + } + + protected function __set_data(string $value): void { + // PHP's DOM does this correctly already. + $this->innerNode->data = $value; + } + + protected function __get_length(): int { + // PHP's DOM does this correctly already. + return $this->innerNode->length; + } + + + public function appendData(string $data) { + // PHP's DOM does this correctly already. + return $this->innerNode->appendData($data); + } + + public function deleteData(int $offset, int $count) { + // PHP's DOM does this correctly already. + return $this->innerNode->deleteData($data); + } + + public function insertData(int $offset, string $data) { + // PHP's DOM does this correctly already. + return $this->innerNode->insertData($offset, $data); + } + + public function replaceData(int $offset, int $count, string $data) { + // PHP's DOM does this correctly already. + return $this->innerNode->replaceData($offset, $count, $data); + } + + public function substringData(int $offset, int $count): string { + // PHP's DOM does this correctly already. + return $this->innerNode->substringData($offset, $count); + } +} \ No newline at end of file diff --git a/lib/ChildNode.php b/lib/ChildNode.php index 1cff8d6..fa505e0 100644 --- a/lib/ChildNode.php +++ b/lib/ChildNode.php @@ -8,7 +8,7 @@ declare(strict_types=1); namespace MensBeam\HTML\DOM; use MensBeam\Framework\MagicProperties, - MensBeam\HTML\DOM\InnerNode\Factory; + MensBeam\HTML\DOM\InnerNode\Reflection; trait ChildNode { @@ -41,7 +41,7 @@ trait ChildNode { } if ($node instanceof DocumentFragment) { - $host = Factory::getProtectedProperty($node, 'host'); + $host = Reflection::getProtectedProperty($node, 'host'); if ($host !== null) { $next = $host->get(); } diff --git a/lib/InnerNode/Text.php b/lib/Comment.php similarity index 68% rename from lib/InnerNode/Text.php rename to lib/Comment.php index 756657b..a3311a2 100644 --- a/lib/InnerNode/Text.php +++ b/lib/Comment.php @@ -6,7 +6,7 @@ */ declare(strict_types=1); -namespace MensBeam\HTML\DOM\InnerNode; +namespace MensBeam\HTML\DOM; -class Text extends \DOMText {} +class Comment extends CharacterData {} \ No newline at end of file diff --git a/lib/DOMException.php b/lib/DOMException.php index 52efdeb..034d070 100644 --- a/lib/DOMException.php +++ b/lib/DOMException.php @@ -11,7 +11,7 @@ use MensBeam\Framework\Exception; class DOMException extends Exception { - // From PHP's DOMException; keeping error codes consistent + public const INDEX_SIZE_ERROR = 1; public const HIERARCHY_REQUEST_ERROR = 3; public const WRONG_DOCUMENT = 4; public const INVALID_CHARACTER = 5; @@ -22,13 +22,11 @@ class DOMException extends Exception { public const INVALID_MODIFICATION_ERROR = 13; public const NAMESPACE_ERROR = 14; public const INVALID_ACCESS_ERROR = 15; - public const VALIDATION_ERROR = 16; - - public const CLIENT_ONLY_NOT_IMPLEMENTED = 301; public function __construct(int $code, ...$args) { self::$messages = array_replace(parent::$messages, [ + 1 => 'Invalid index size', 3 => 'Hierarchy request error', 4 => 'Supplied node does not belong to this document', 5 => 'Invalid character', @@ -38,10 +36,7 @@ class DOMException extends Exception { 12 => 'Syntax error', 13 => 'Invalid modification error', 14 => 'Namespace error', - 15 => 'Invalid access error', - 16 => 'Validation error', - - 301 => '%s is client side only; not implemented' + 15 => 'Invalid access error' ]); parent::__construct($code, ...$args); diff --git a/lib/DOMImplementation.php b/lib/DOMImplementation.php index 0913f23..8c61d65 100644 --- a/lib/DOMImplementation.php +++ b/lib/DOMImplementation.php @@ -7,7 +7,7 @@ declare(strict_types=1); namespace MensBeam\HTML\DOM; -use MensBeam\HTML\DOM\InnerNode\Factory, +use MensBeam\HTML\DOM\InnerNode\Reflection, MensBeam\HTML\DOM\Parser; @@ -24,7 +24,7 @@ class DOMImplementation { # The createDocument(namespace, qualifiedName, doctype) method steps are: # # 1. Let document be a new XMLDocument. - $document = Factory::createFromProtectedConstructor(__NAMESPACE__ . '\\XMLDocument'); + $document = Reflection::createFromProtectedConstructor(__NAMESPACE__ . '\\XMLDocument'); # 2. Let element be null. $element = null; @@ -67,7 +67,7 @@ class DOMImplementation { $contentType = 'application/xml'; } - Factory::setProtectedProperty($document, '_contentType', $contentType); + Reflection::setProtectedProperty($document, '_contentType', $contentType); # 8. Return document. return $document; @@ -86,14 +86,14 @@ class DOMImplementation { # 2. Return a new doctype, with qualifiedName as its name, publicId as its # public ID, and systemId as its system ID, and with its node document set to # the associated document of this. - $innerDocument = Factory::getProtectedProperty($this->document->get(), 'innerNode'); + $innerDocument = Reflection::getProtectedProperty($this->document->get(), 'innerNode'); // PHP's DOM won't accept an empty string as the qualifiedName, so use a space // instead; this will be worked around in DocumentType. return $innerDocument->getWrapperNode($innerDocument->implementation->createDocumentType(($qualifiedName !== '') ? $qualifiedName : ' ', $publicId, $systemId)); } public function createHTMLDocument(string $title = ''): Document { - $document = Factory::createFromProtectedConstructor(__NAMESPACE__ . '\\Document'); + $document = Reflection::createFromProtectedConstructor(__NAMESPACE__ . '\\Document'); if ($title !== '') { $document->title = $title; } diff --git a/lib/Document.php b/lib/Document.php index 731bb47..f62dcdd 100644 --- a/lib/Document.php +++ b/lib/Document.php @@ -9,7 +9,7 @@ declare(strict_types=1); namespace MensBeam\HTML\DOM; use MensBeam\HTML\DOM\InnerNode\{ Document as InnerDocument, - Factory + Reflection }; use MensBeam\HTML\Parser; @@ -31,7 +31,7 @@ class Document extends Node { public function __construct() { parent::__construct(new InnerDocument($this)); - $this->_implementation = Factory::createFromProtectedConstructor(__NAMESPACE__ . '\\DOMImplementation', $this); + $this->_implementation = Reflection::createFromProtectedConstructor(__NAMESPACE__ . '\\DOMImplementation', $this); } @@ -43,6 +43,17 @@ class Document extends Node { return $this->innerNode->getWrapperNode($this->innerNode->createElement($localName)); } + public function createTextNode(string $data): Text { + // Text has a public constructor that creates an inner text node without an + // associated document, so some jiggerypokery must be done instead. + $reflector = new \ReflectionClass(__NAMESPACE__ . '\\Text'); + $text = $reflector->newInstanceWithoutConstructor(); + $property = new \ReflectionProperty($text, 'innerNode'); + $property->setAccessible(true); + $property->setValue($text, $this->innerNode->createTextNode($data)); + return $text; + } + public function importNode(\DOMNode|Node $node, bool $deep = false): Node { $isDOMNode = ($node instanceof \DOMNode); $node = $this->innerNode->getWrapperNode($this->innerNode->importNode((!$isDOMNode) ? $this->getInnerNode($node) : $node, false)); diff --git a/lib/DocumentFragment.php b/lib/DocumentFragment.php index feb36f5..5e3cbca 100644 --- a/lib/DocumentFragment.php +++ b/lib/DocumentFragment.php @@ -7,7 +7,6 @@ declare(strict_types=1); namespace MensBeam\HTML\DOM; -use MensBeam\HTML\DOM\InnerNode\DocumentFragment as InnerDocumentFragment; class DocumentFragment extends Node { @@ -16,7 +15,7 @@ class DocumentFragment extends Node { protected ?\WeakReference $host = null; - protected function __construct(InnerDocumentFragment $fragment) { + protected function __construct(\DOMDocumentFragment $fragment) { parent::__construct($fragment); } } diff --git a/lib/Element.php b/lib/Element.php index f4f4fcb..0e24104 100644 --- a/lib/Element.php +++ b/lib/Element.php @@ -7,8 +7,7 @@ declare(strict_types=1); namespace MensBeam\HTML\DOM; -use MensBeam\HTML\DOM\InnerNode\Element as InnerElement, - MensBeam\HTML\Parser; +use MensBeam\HTML\Parser; class Element extends Node { @@ -24,7 +23,7 @@ class Element extends Node { } - protected function __construct(InnerElement $element) { + protected function __construct(\DOMElement $element) { parent::__construct($element); } diff --git a/lib/InnerNode/Comment.php b/lib/HTMLElement.php similarity index 66% rename from lib/InnerNode/Comment.php rename to lib/HTMLElement.php index eb5d766..672fed2 100644 --- a/lib/InnerNode/Comment.php +++ b/lib/HTMLElement.php @@ -6,7 +6,7 @@ */ declare(strict_types=1); -namespace MensBeam\HTML\DOM\InnerNode; +namespace MensBeam\HTML\DOM; -class Comment extends \DOMComment {} +class HTMLElement extends Element {} \ No newline at end of file diff --git a/lib/HTMLTemplateElement.php b/lib/HTMLTemplateElement.php index 6ae800f..f74dc9a 100644 --- a/lib/HTMLTemplateElement.php +++ b/lib/HTMLTemplateElement.php @@ -8,7 +8,7 @@ declare(strict_types=1); namespace MensBeam\HTML\DOM; use MensBeam\HTML\DOM\InnerNode\Element as InnerElement, - MensBeam\HTML\DOM\InnerNode\Factory; + MensBeam\HTML\DOM\InnerNode\Reflection; class HTMLTemplateElement extends Element { @@ -25,6 +25,6 @@ class HTMLTemplateElement extends Element { parent::__construct($element); $this->_content = $this->ownerDocument->createDocumentFragment(); - Factory::setProtectedProperty($this->_content, 'host', \WeakReference::create($this)); + Reflection::setProtectedProperty($this->_content, 'host', \WeakReference::create($this)); } } \ No newline at end of file diff --git a/lib/InnerNode/CDATASection.php b/lib/InnerNode/CDATASection.php deleted file mode 100644 index 0f36146..0000000 --- a/lib/InnerNode/CDATASection.php +++ /dev/null @@ -1,12 +0,0 @@ -nodeMap = new NodeMap(); // Use a weak reference here to prevent a circular reference @@ -64,36 +56,42 @@ class Document extends \DOMDocument { // If the node didn't exist we must construct the wrapper node's class name // based upon the node's class name - $className = $node::class; - switch ($className) { - case __NAMESPACE__ . '\\Attr': $className = self::$parentNamespace . "\\Attr"; - break; - case __NAMESPACE__ . '\\CDATASection': $className = self::$parentNamespace . "\\CDATASection"; - break; - case __NAMESPACE__ . '\\Comment': $className = self::$parentNamespace . "\\Comment"; - break; - case __NAMESPACE__ . '\\Document': $className = self::$parentNamespace . "\\Document"; - break; - case __NAMESPACE__ . '\\DocumentFragment': $className = self::$parentNamespace . "\\DocumentFragment"; - break; - case 'DOMDocumentType': $className = self::$parentNamespace . "\\DocumentType"; - break; - case __NAMESPACE__ . '\\Element': - if (($node->namespaceURI === null || $node->namespaceURI === Parser::HTML_NAMESPACE) && $node->nodeName === 'template') { - $className = self::$parentNamespace . "\\HTMLTemplateElement"; + if ($node instanceof \DOMAttr) { + $className = 'Attr'; + } elseif ($node instanceof \DOMCDATASection) { + $className = 'CDATASection'; + } elseif ($node instanceof \DOMComment) { + $className = 'Comment'; + } elseif ($node instanceof \DOMDocument) { + $className = 'Document'; + } elseif ($node instanceof \DOMDocumentFragment) { + $className = 'DocumentFragment'; + } elseif ($node instanceof \DOMDocumentType) { + $className = 'DOMDocumentType'; + } elseif ($node instanceof \DOMElement) { + $namespace = $node->namespaceURI; + if ($namespace === null) { + if ($node->nodeName === 'template') { + $className = 'HTMLTemplateElement'; } else { - $className = self::$parentNamespace . "\\Element"; + $className = 'HTMLElement'; } - break; - case __NAMESPACE__ . '\\ProcessingInstruction': $className = self::$parentNamespace . "\\ProcessingInstruction"; - break; - case __NAMESPACE__ . '\\Text': $className = self::$parentNamespace . "\\Text"; - break; - case __NAMESPACE__ . '\\XMLDocument': $className = self::$parentNamespace . "\\XMLDocument"; - break; + } elseif ($namespace === Parser::SVG_NAMESPACE) { + $className = 'SVGElement'; + } elseif ($namespace === Parser::MATHML_NAMESPACE) { + $className = 'MathMLElement'; + } else { + $className = 'Element'; + } + } elseif ($node instanceof \DOMProcessingInstruction) { + $className = 'ProcessingInstruction'; + } elseif ($node instanceof \DOMText) { + $className = 'Text'; + } elseif ($node instanceof XMLDocument) { + $className = 'XMLDocument'; } // Nodes cannot be created from their constructors normally - return Factory::createFromProtectedConstructor($className, $node); + return Reflection::createFromProtectedConstructor(self::$parentNamespace . "\\$className", $node); } } diff --git a/lib/InnerNode/DocumentFragment.php b/lib/InnerNode/DocumentFragment.php deleted file mode 100644 index 7ac86c7..0000000 --- a/lib/InnerNode/DocumentFragment.php +++ /dev/null @@ -1,12 +0,0 @@ -newInstanceWithoutConstructor(); @@ -26,10 +26,10 @@ class Factory { return $property->getValue($instance); } - public static function setProtectedProperty(mixed $instance, string $propertyName, mixed $value): mixed { + public static function setProtectedProperty(mixed $instance, string $propertyName, mixed $value): void { $reflector = new \ReflectionClass($instance::class); $property = new \ReflectionProperty($instance, $propertyName); $property->setAccessible(true); - return $property->setValue($instance, $value); + $property->setValue($instance, $value); } } \ No newline at end of file diff --git a/lib/InnerNode/Element.php b/lib/MathMLElement.php similarity index 66% rename from lib/InnerNode/Element.php rename to lib/MathMLElement.php index c0decbe..a68a69c 100644 --- a/lib/InnerNode/Element.php +++ b/lib/MathMLElement.php @@ -6,7 +6,7 @@ */ declare(strict_types=1); -namespace MensBeam\HTML\DOM\InnerNode; +namespace MensBeam\HTML\DOM; -class Element extends \DOMElement {} +class MathMLElement extends Element {} \ No newline at end of file diff --git a/lib/Node.php b/lib/Node.php index a8b27f3..277fcd7 100644 --- a/lib/Node.php +++ b/lib/Node.php @@ -8,7 +8,7 @@ declare(strict_types=1); namespace MensBeam\HTML\DOM; use MensBeam\Framework\MagicProperties, - MensBeam\HTML\DOM\InnerNode\Factory; + MensBeam\HTML\DOM\InnerNode\Reflection; abstract class Node { @@ -37,6 +37,40 @@ abstract class Node { protected ?NodeList $_childNodes = null; protected \DOMNode $innerNode; + private static ?int $rand = null; + + protected function __get_baseURI(): string { + # The baseURI getter steps are to return this’s node document’s document base + # URL, serialized. + # + # The document base URL of a Document object is the absolute URL obtained by running these steps: + $document = ($this instanceof Document) ? $this : $this->ownerDocument; + $base = $doc->getElementsByNodeName('base'); + foreach ($base as $b) { + $href = $base->getAttribute('href'); + # 2. Otherwise, return the frozen base URL of the first base element in the + # Document that has an href attribute, in tree order. + // URL of base element is always frozen + if ($href !== null) { + return $href; + } + } + + # 1. If there is no base element that has an href attribute in the Document, + # then return the Document's fallback base URL. + // This is going to be done last because I have to iterate over the base elements first. + # The fallback base URL of a Document object document is the URL record obtained by running these steps: + # + # 1. If document is an iframe srcdoc document, then return the document base URL + # of document's browsing context's container document. + // DEVIATION: There can't be an iframe srcdoc document in this implementation. + # 2. If document's URL is about:blank, and document's browsing context's creator + # base URL is non-null, then return that creator base URL. + // DEVIATION: Document's URL cannot be about:blank in this implementation. + # 3. Return document's URL. + return $document->URL; + } + protected function __get_childNodes(): NodeList { // NodeLists cannot be created from their constructors normally. // OPTIMIZATION: Going to optimize here and only create a truly live NodeList if @@ -44,7 +78,7 @@ abstract class Node { // NodeList. There is no sense in generating a live list that will never update. if ($this instanceof Document || $this instanceof DocumentFragment || $this instanceof Element) { $doc = ($this instanceof Document) ? $this->innerNode : $this->innerNode->ownerDocument; - return Factory::createFromProtectedConstructor(__NAMESPACE__ . '\\NodeList', function() use($doc) { + return Reflection::createFromProtectedConstructor(__NAMESPACE__ . '\\NodeList', function() use($doc) { $result = []; $innerChildNodes = $this->innerNode->childNodes; foreach ($innerChildNodes as $i) { @@ -59,7 +93,7 @@ abstract class Node { return $this->_childNodes; } - $this->_childNodes = Factory::createFromProtectedConstructor(__NAMESPACE__ . '\\Nodelist', []); + $this->_childNodes = Reflection::createFromProtectedConstructor(__NAMESPACE__ . '\\Nodelist', []); return $this->_childNodes; } @@ -68,6 +102,13 @@ abstract class Node { return $this->innerNode->firstChild; } + protected function __get_isConnected(): bool { + # The isConnected getter steps are to return true, if this is connected; + # otherwise false. + # An element is connected if its shadow-including root is a document. + return ($this->getRootNode() instanceof Document); + } + protected function __get_lastChild(): ?Node { // PHP's DOM does this correctly already. return $this->innerNode->lastChild; @@ -196,6 +237,93 @@ abstract class Node { return $newInner->ownerDocument->getWrapperNode($newInner); } + public function compareDocumentPosition(Node $other): int { + # The compareDocumentPosition(other) method steps are: + # + # 1. If this is other, then return zero. + if ($this === $other) { + return 0; + } + + # 2. Let node1 be other and node2 be this. + $node1 = $other; + $node2 = $this; + + # 3. Let attr1 and attr2 be null. + $attr1 = $attr2 = null; + + # 4. If node1 is an attribute, then set attr1 to node1 and node1 to attr1’s + # element. + if ($node1 instanceof Attr) { + $attr1 = $node1; + $node1 = $attr1->ownerElement; + } + + # 5. If node2 is an attribute, then: + if ($node2 instanceof Attr) { + # 1. Set attr2 to node2 and node2 to attr2’s element. + $attr2 = $node2; + $node2 = $attr2->ownerElement; + + # 2. If attr1 and node1 are non-null, and node2 is node1, then: + if ($attr1 !== null && $node1 !== null && $node2 === $node1) { + # 1. For each attr in node2’s attribute list: + foreach ($node2->attributes as $attr) { + # 1. If attr equals attr1, then return the result of adding DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC and DOCUMENT_POSITION_PRECEDING. + if ($attr === $attr1) { + return Node::DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC + Node::DOCUMENT_POSITION_PRECEDING; + } + + # 2. If attr equals attr2, then return the result of adding DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC and DOCUMENT_POSITION_FOLLOWING. + if ($attr === $attr2) { + return Node::DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC + Node::DOCUMENT_POSITION_FOLLOWING; + } + } + } + } + + # 6. If node1 or node2 is null, or node1’s root is not node2’s root, then return the + # result of adding DOCUMENT_POSITION_DISCONNECTED, + # DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC, and either + # DOCUMENT_POSITION_PRECEDING or DOCUMENT_POSITION_FOLLOWING, with the constraint + # that this is to be consistent, together. + # + # NOTE: Whether to return DOCUMENT_POSITION_PRECEDING or + # DOCUMENT_POSITION_FOLLOWING is typically implemented via pointer comparison. + # In JavaScript implementations a cached Math.random() value can be used. + if (self::$rand === null) { + self::$rand = rand(0, 1); + } + + if ($node1 === null || $node2 === null || $node1->getRootNode() !== $node2->getRootNode()) { + return Node::DOCUMENT_POSITION_DISCONNECTED + Node::DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC + ((self::$rand === 0) ? Node::DOCUMENT_POSITION_PRECEDING : Node::DOCUMENT_POSITION_FOLLOWING); + } + + # 7. If node1 is an ancestor of node2 and attr1 is null, or node1 is node2 and attr2 + # is non-null, then return the result of adding DOCUMENT_POSITION_CONTAINS to + # DOCUMENT_POSITION_PRECEDING. + if (($node1 === $node2 && $attr2 !== null) || ($attr1 === null && $node2->contains($node1))) { + return Node::DOCUMENT_POSITION_CONTAINS + Node::DOCUMENT_POSITION_PRECEDING; + } + + # 8. If node1 is a descendant of node2 and attr2 is null, or node1 is node2 and attr1 + # is non-null, then return the result of adding DOCUMENT_POSITION_CONTAINED_BY to + # DOCUMENT_POSITION_FOLLOWING. + if (($node1 === $node2 && $attr1 !== null) || ($attr2 === null && $node2->contains($node1))) { + return Node::DOCUMENT_POSITION_CONTAINED_BY + Node::DOCUMENT_POSITION_FOLLOWING; + } + + # 9. If node1 is preceding node2, then return DOCUMENT_POSITION_PRECEDING. + if ($node2->walkPreceding(function($n) use($node1) { + return ($n === $node1); + })->current() !== null) { + return Node::DOCUMENT_POSITION_PRECEDING; + } + + # 10. Return DOCUMENT_POSITION_FOLLOWING. + return Node::DOCUMENT_POSITION_FOLLOWING; + } + public function contains(?Node $other): bool { # The contains(other) method steps are to return true if other is an inclusive # descendant of this; otherwise false (including when other is null). @@ -204,6 +332,24 @@ abstract class Node { })->current() !== null); } + public function getRootNode(array $options): Node { + # 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) { + return ($n->parentNode === null); + })->current(); + } + public function hasChildNodes(): bool { // PHP's DOM does this correctly already. return $this->innerNode->hasChildNodes(); @@ -218,12 +364,156 @@ abstract class Node { return $node; } + public function isDefaultNamespace(?string $namespace = null): bool { + # The isDefaultNamespace(namespace) method steps are: + // PHP DOM's implementation of this is broken for HTML, so let's do this + // manually. + + # 1. If namespace is the empty string, then set it to null. + if ($namespace === '') { + $namespace = null; + } + + # 2. Let defaultNamespace be the result of running locate a namespace for this + # using null. + # 3. Return true if defaultNamespace is the same as namespace; otherwise false. + return ($this->locateNamespace($this, null) === $namespace); + } + + public function isEqualNode(?Node $otherNode) { + # The isEqualNode(otherNode) method steps are to return true if otherNode is + # non-null and this equals otherNode; otherwise false. + + # A node A equals a node B if all of the following conditions are true: + # + # • A and B implement the same interfaces. + if ($this::class !== $otherNode::class) { + return false; + } + + # • The following are equal, switching on the interface A implements: + # + # ↪ DocumentType + # Its name, public ID, and system ID. + if ($this instanceof DocumentType) { + if ($this->name !== $otherNode->name || $this->publicId !== $otherNode->publicId || $this->systemId !== $this->publicId) { + return false; + } + } + # ↪ Element + # Its namespace, namespace prefix, local name, and its attribute list’s size. + elseif ($this instanceof Element) { + if ($this->namespaceURI !== $otherNode->namespaceURI || $this->prefix !== $otherNode->prefix || $this->localName !== $otherNode->localName || $this->attributes->length !== $otherNode->attributes->length) { + return false; + } + + # • If A is an element, each attribute in its attribute list has an attribute that + # equals an attribute in B’s attribute list. + foreach ($this->attributes as $key => $attr) { + if (!$attr->isEqualNode($otherNode->attributes[$key])) { + return false; + } + } + } + # ↪ Attr + # Its namespace, local name, and value. + elseif ($this instanceof Attr) { + if ($this->namespaceURI !== $otherNode->namespaceURI || $this->localName !== $otherNode->localName || $this->value !== $otherNode->value) { + return false; + } + } + # ↪ Text + # ↪ Comment + # Its data. + elseif ($this instanceof Text || $this instanceof Comment) { + if ($this->data !== $otherNode->data) { + return false; + } + } + + if ($this instanceof Document || $this instanceof DocumentFragment || $this instanceof Element) { + # • A and B have the same number of children. + if ($this->childNodes->length !== $otherNode->childNodes->length) { + return false; + } + + # • Each child of A equals the child of B at the identical index. + foreach ($this->childNodes as $key => $child) { + $other = $otherNode->childNodes[$key]; + if ($child->name !== $other->name || $child->publicId !== $other->publicId || $child->systemId !== $other->systemId) { + return false; + } + } + } + + return true; + } + public function isSameNode(?Node $otherNode) { # The isSameNode(otherNode) method steps are to return true if otherNode is # this; otherwise false. return ($otherNode === $this); } + public function lookupPrefix(?string $namespace = null): ?string { + # The lookupPrefix(namespace) method steps are: + // PHP DOM's implementation of this is broken for HTML, so let's do this + // manually. + + # 1. If namespace is null or the empty string, then return null. + if ($namespace === null || $namespace === '') { + return null; + } + + # 2. Switch on the interface this implements: + # + # ↪ Element + if ($this instanceof Element) { + # Return the result of locating a namespace prefix for it using namespace. + return $this->locateNamespacePrefix($this, $namespace); + } + + # ↪ Document + elseif ($this instanceof Document) { + # Return the result of locating a namespace prefix for its document element, if + # its document element is non-null; otherwise null. + return ($this->documentElement !== null) ? $this->locateNamespacePrefix($this->documentElement, $namespace) : null; + } + + # ↪ DocumentType + # ↪ DocumentFragment + elseif ($this instanceof DocumentType || $this instanceof DocumentFragment) { + return null; + } + + # ↪ Attr + elseif ($this instanceof Attr) { + # Return the result of locating a namespace prefix for its element, if its + # element is non-null; otherwise null. + return $this->locateNamespacePrefix($this->ownerElement, $namespace); + } + + # ↪ Otherwise + # Return the result of locating a namespace prefix for its parent element, + # if its parent element is non-null; otherwise null. + $parentElement = $this->parentElement; + return ($parentElement !== null) ? $this->locateNamespacePrefix($this->parentElement, $namespace) : null; + } + + public function lookupNamespaceURI(?string $prefix = null): ?string { + # The lookupNamespaceURI(prefix) method steps are: + // PHP DOM's implementation of this is broken for HTML, so let's do this + // manually. + + # 1. If prefix is the empty string, then set it to null. + if ($prefix === '') { + $prefix = null; + } + + # 2. Return the result of running locate a namespace for this using prefix. + return $this->locateNamespace($this, $prefix); + } + public function normalize(): void { // PHP's DOM does this correctly already. $this->innerNode->normalize(); @@ -289,19 +579,22 @@ abstract class Node { }, true)->current() !== null) { throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); } elseif ($nodeChildElementCount === 1) { - $n = $this->firstChild; $beforeChild = true; - do { + if ($node->firstChild->walkFollowing(function($n) use(&$beforeChild, $child) { if (!$beforeChild && $n instanceof DocumentType) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + return true; } if ($n instanceof Element && $n !== $child) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + return true; } elseif ($n === $child) { $beforeChild = false; } - } while ($n = $n->nextSibling); + + return false; + }, true)->current() !== null) { + throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + } } } @@ -309,38 +602,44 @@ abstract class Node { # parent has an element child that is not child or a doctype is following # child. elseif ($node instanceof Element) { - $n = $this->firstChild; $beforeChild = true; - do { + if ($node->firstChild->walkFollowing(function($n) use(&$beforeChild, $child) { if (!$beforeChild && $n instanceof DocumentType) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + return true; } if ($n instanceof Element && $n !== $child) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + return true; } elseif ($n === $child) { $beforeChild = false; } - } while ($n = $n->nextSibling); + + return false; + }, true)->current() !== null) { + throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + } } # ↪ DocumentType # parent has a doctype child that is not child, or an element is preceding # child. elseif ($node instanceof DocumentType) { - $n = $this->firstChild; $beforeChild = true; - do { + if ($node->firstChild->walkFollowing(function($n) use(&$beforeChild, $child) { if ($beforeChild && $n instanceof Element) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + return true; } if ($n instanceof DocumentType && $n !== $child) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + return true; } elseif ($n === $child) { $beforeChild = false; } - } while ($n = $n->nextSibling); + + return false; + }, true)->current() !== null) { + throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + } } } @@ -355,7 +654,115 @@ abstract class Node { return $this->innerNode; } - return Factory::getProtectedProperty($node, 'innerNode'); + return Reflection::getProtectedProperty($node, 'innerNode'); + } + + protected function locateNamespace(Node $node, ?string $prefix = null): ?string { + # To locate a namespace for a node using prefix, switch on the interface node + # implements: + # + # ↪ Element + if ($node instanceof Element) { + # 1. If its namespace is non-null and its namespace prefix is prefix, then return + # namespace. + if ($node->namespaceURI !== null && $node->prefix === $prefix) { + return $node->namespaceURI; + } + + # 2. If it has an attribute whose namespace is the XMLNS namespace, namespace prefix + # is "xmlns", and local name is prefix, or if prefix is null and it has an + # attribute whose namespace is the XMLNS namespace, namespace prefix is null, and + # local name is "xmlns", then return its value if it is not the empty string, and + # null otherwise. + $attributes = $node->attributes; + foreach ($attributes as $attr) { + if (($attr->namespaceURI === Parser::XMLNS_NAMESPACE && $attr->prefix === 'xmlns' && $attr->localName === $prefix) || ($prefix === null && $attr->namespaceURI === Parser::XMLNS_NAMESPACE && $attr->prefix === null && $attr->localName === 'xmlns')) { + return ($attr->value !== '') ? $attr->value : null; + } + } + + $parentElement = $node->parentElement; + + # 3. If its parent element is null, then return null. + if ($parentElement === null) { + return null; + } + + # 4. Return the result of running locate a namespace on its parent element using + # prefix. + return $this->locateNamespace($parentElement, $prefix); + } + + # ↪ Document + elseif ($node instanceof Document) { + # 1. If its document element is null, then return null. + if ($node->documentElement === null) { + return null; + } + + # 2. Return the result of running locate a namespace on its document element + # using prefix. + return $this->locateNamespace($node->documentElement, $prefix); + } + + # ↪ DocumentType + # ↪ DocumentFragment + elseif ($node instanceof DocumentType || $node instanceof DocumentFragment) { + # Return null. + return null; + } + + # ↪ Attr + elseif ($node instanceof Attr) { + # 1. If its element is null, then return null. + if ($node->ownerElement === null) { + return null; + } + + # 2. Return the result of running locate a namespace on its element using + # prefix. + return $this->locateNamespace($node->ownerElement, $prefix); + } + + # ↪ Otherwise + # 1. If its parent element is null, then return null. + $parentElement = $node->parentElement; + if ($parentElement === null) { + return null; + } + + # 2. Return the result of running locate a namespace on its parent element using + # prefix. + return $this->locateNamespace($parentElement, $prefix); + } + + protected function locateNamespacePrefix(Element $element, ?string $namespace = null) { + # To locate a namespace prefix for an element using namespace, run these steps: + # + # 1. If element’s namespace is namespace and its namespace prefix is non-null, + # then return its namespace prefix. + if ($element->namespaceURI === $namespace && $element->$prefix !== null) { + return $element->prefix; + } + + # 2. If element has an attribute whose namespace prefix is "xmlns" and value is + # namespace, then return element’s first such attribute’s local name. + $attributes = $element->attributes; + foreach ($attributes as $attr) { + if ($attr->prefix === 'xmlns' && $attr->value === $namespace) { + return $attr->localName; + } + } + + # 3. If element’s parent element is not null, then return the result of running + # locate a namespace prefix on that element using namespace. + $parentElement = $element->parentElement; + if ($parentElement !== null) { + return $this->locateNamespacePrefix($parentElement, $namespace); + } + + # Return null. + return null; } protected function preInsertionValidity(Node $node, ?Node $child = null) { @@ -379,7 +786,7 @@ abstract class Node { } else { $parentRoot = $this->getRootNode(); if ($parentRoot instanceof DocumentFragment) { - $parentRootHost = Factory::getProtectedProperty($parentRoot, 'host')->get(); + $parentRootHost = Reflection::getProtectedProperty($parentRoot, 'host')->get(); if ($parentRootHost !== null && ($parentRootHost === $node || $node->contains($parentRootHost))) { throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); } diff --git a/lib/ProcessingInstruction.php b/lib/ProcessingInstruction.php new file mode 100644 index 0000000..b4b7fbf --- /dev/null +++ b/lib/ProcessingInstruction.php @@ -0,0 +1,12 @@ +innerNode = new \DOMText($data); + } +} \ No newline at end of file