diff --git a/childNodes b/childNodes new file mode 100644 index 0000000..e69de29 diff --git a/lib/AbstractDocument.php b/lib/AbstractDocument.php index 540bf8b..213abec 100644 --- a/lib/AbstractDocument.php +++ b/lib/AbstractDocument.php @@ -10,5 +10,5 @@ namespace MensBeam\HTML\DOM; // Exists so Document can extend methods from its traits. abstract class AbstractDocument extends \DOMDocument { - use ContainerNode, DocumentOrElement, EscapeString, MagicProperties, ParentNode, Walk; + use DocumentOrElement, EscapeString, MagicProperties, ParentNode, Walk; } diff --git a/lib/Comment.php b/lib/Comment.php index ed3d7ff..cd87ded 100644 --- a/lib/Comment.php +++ b/lib/Comment.php @@ -9,5 +9,5 @@ declare(strict_types=1); namespace MensBeam\HTML\DOM; class Comment extends \DOMComment { - use LeafNode, Moonwalk, ToString; + use ChildNode, Moonwalk, ToString; } diff --git a/lib/DocumentFragment.php b/lib/DocumentFragment.php index e4347dc..a87f069 100644 --- a/lib/DocumentFragment.php +++ b/lib/DocumentFragment.php @@ -9,7 +9,7 @@ declare(strict_types=1); namespace MensBeam\HTML\DOM; class DocumentFragment extends \DOMDocumentFragment { - use ContainerNode, ParentNode, Walk; + use ParentNode, Walk; public function __toString() { return $this->ownerDocument->saveHTML($this); diff --git a/lib/Element.php b/lib/Element.php index e5bdd35..4564532 100644 --- a/lib/Element.php +++ b/lib/Element.php @@ -11,7 +11,7 @@ use MensBeam\HTML\Parser; class Element extends \DOMElement { - use ContainerNode, DocumentOrElement, EscapeString, MagicProperties, Moonwalk, ParentNode, ToString, Walk; + use DocumentOrElement, EscapeString, MagicProperties, Moonwalk, ParentNode, ToString, Walk; protected $_classList; @@ -52,7 +52,7 @@ class Element extends \DOMElement { # 2. Let fragment be the result of invoking the fragment parsing algorithm with # the new value as markup, and with context element. - $fragment = Parser::parseFragment($this, 0, $value, 'UTF-8'); + $fragment = Parser::parseFragment($this, $this->ownerDocument->quirksMode, $value, 'UTF-8'); $fragment = $this->ownerDocument->importNode($fragment); # 3. If the context object is a template element, then let context object be the @@ -170,7 +170,7 @@ class Element extends \DOMElement { # 5. Let fragment be the result of invoking the fragment parsing algorithm with # the new value as markup, and parent as the context element. - $fragment = Parser::parseFragment($parent, 0, $value, 'UTF-8'); + $fragment = Parser::parseFragment($parent, $this->ownerDocument->quirksMode, $value, 'UTF-8'); $fragment = $this->ownerDocument->importNode($fragment); # 6. Replace the context object with fragment within the context object's diff --git a/lib/ElementMap.php b/lib/ElementSet.php similarity index 92% rename from lib/ElementMap.php rename to lib/ElementSet.php index b54b1f8..4b3c926 100644 --- a/lib/ElementMap.php +++ b/lib/ElementSet.php @@ -8,13 +8,23 @@ declare(strict_types=1); namespace MensBeam\HTML\DOM; -// This is a write-only map of elements which need to be kept in memory; it +// This is a write-only set of elements which need to be kept in memory; it // exists because values of properties on derived DOM classes are lost unless at // least one PHP reference is kept for the element somewhere in userspace. This // is that somewhere. It is at present only used for template elements. -class ElementMap { +class ElementSet { protected static $_storage = []; + + public static function add(Element $element) { + if (!self::has($element)) { + self::$_storage[] = $element; + return true; + } + + return false; + } + public static function delete(Element $element) { foreach (self::$_storage as $k => $v) { if ($v->isSameNode($element)) { @@ -59,13 +69,4 @@ class ElementMap { return false; } - - public static function set(Element $element) { - if (!self::has($element)) { - self::$_storage[] = $element; - return true; - } - - return false; - } } diff --git a/lib/HTMLElement.php b/lib/HTMLElement.php deleted file mode 100644 index 6a675ce..0000000 --- a/lib/HTMLElement.php +++ /dev/null @@ -1,88 +0,0 @@ -getAttribute('accesskey'); - } - - protected function __set_accessKey(string $value) { - return $this->setAttribute('accesskey', $value); - } - - protected function __get_contentEditable(): string { - # The contentEditable IDL attribute, on getting, must return the string "true" - # if the content attribute is set to the true state, "false" if the content - # attribute is set to the false state, and "inherit" otherwise. - $result = $this->getAttribute('contenteditable'); - switch ($value) { - case 'false': - case 'true': - return $result; - default: - return 'inherit'; - } - } - - protected function __set_contentEditable(string $value) { - # On setting, if the new value is an ASCII case-insensitive match for the - # string "inherit" then the content attribute must be removed, if the new value - # is an ASCII case-insensitive match for the string "true" then the content - # attribute must be set to the string "true", if the new value is an ASCII - # case-insensitive match for the string "false" then the content attribute must - # be set to the string "false", and otherwise the attribute setter must throw a - # "SyntaxError" DOMException. - switch ($value) { - case 'inherit' - $this->removeAttribute('contenteditable'); - case 'false': - case 'true': - return $this->setAttribute('contenteditable', $value); - default: - throw new DOMException(DOMException::SYNTAX_ERROR); - } - } - - protected function __get_isContentEditable(): bool { - # The isContentEditable IDL attribute, on getting, must return true if the - # element is either an editing host or editable, and false otherwise. - # - # An editing host is either an HTML element with its contenteditable attribute - # in the true state, or a child HTML element of a Document whose design mode - # enabled is true. - # - # Something is editable if it is a node; it is not an editing host; it does - # not have a contenteditable attribute set to the false state; its parent is an - # editing host or editable; and either it is an HTML element, or it is an svg or - # math element, or it is not an Element and its parent is an HTML element. - $contentEditable = $this->__get_contentEditable(); - $designMode = ($this->ownerDocument->designMode === 'on'); - if ($contentEditable === 'true' || $designMode) { - return true; - } elseif ($contentEditable !== 'false') { - // If the parent can be either an editing host or editable then all is needed - // is to see if there's an ancestor that's an editing host. Just seems absurd - // to word the specification like that. Since isContentEditable is a property - // of HTMLElement there's no need to check if it's an HTML element, svg, or - // non-element child of foreign content. There is also no need to check for - // design mode enabled on the document because it's checked above. - if ($this->moonwalk(function($n) { - if ($n instanceof HTMLElement && $n->contentEditable === 'true') { - return true; - } - })->current() !== null) { - return true; - } - } - - return false; - } -} diff --git a/lib/HTMLTemplateElement.php b/lib/HTMLTemplateElement.php index f9dc74f..9309a06 100644 --- a/lib/HTMLTemplateElement.php +++ b/lib/HTMLTemplateElement.php @@ -26,7 +26,7 @@ class HTMLTemplateElement extends Element { $this->content = $this->ownerDocument->createDocumentFragment(); // Template elements need to have a reference kept in userland - ElementMap::set($this); + ElementSet::add($this); } diff --git a/lib/ProcessingInstruction.php b/lib/ProcessingInstruction.php index eeae605..9e6a3cb 100644 --- a/lib/ProcessingInstruction.php +++ b/lib/ProcessingInstruction.php @@ -9,5 +9,5 @@ declare(strict_types=1); namespace MensBeam\HTML\DOM; class ProcessingInstruction extends \DOMProcessingInstruction { - use LeafNode, Moonwalk, ToString; + use ChildNode, Moonwalk, ToString; } diff --git a/lib/Text.php b/lib/Text.php index 2424e87..9fd4768 100644 --- a/lib/Text.php +++ b/lib/Text.php @@ -9,5 +9,5 @@ declare(strict_types=1); namespace MensBeam\HTML\DOM; class Text extends \DOMText { - use LeafNode, Moonwalk, ToString; + use ChildNode, Moonwalk, ToString; } diff --git a/lib/traits/LeafNode.php b/lib/traits/ChildNode.php similarity index 76% rename from lib/traits/LeafNode.php rename to lib/traits/ChildNode.php index d056ed5..3b5e2eb 100644 --- a/lib/traits/LeafNode.php +++ b/lib/traits/ChildNode.php @@ -8,10 +8,8 @@ declare(strict_types=1); namespace MensBeam\HTML\DOM; -// Node in the DOM spec is dirty. Many nodes which inherit from it inherit -// methods it cannot use which all check for this and throw exceptions. This is -// for nodes which DO NOT have child nodes. -trait LeafNode { + +trait ChildNode { use Node; diff --git a/lib/traits/ContainerNode.php b/lib/traits/ContainerNode.php deleted file mode 100644 index b9de639..0000000 --- a/lib/traits/ContainerNode.php +++ /dev/null @@ -1,106 +0,0 @@ -preInsertionValidity($node); - - $result = parent::appendChild($node); - if ($result !== false && $result instanceof TemplateElement) { - if ($result instanceof TemplateElement) { - ElementMap::set($result); - } - } - return $result; - } - - public function insertBefore($node, $child = null) { - $this->preInsertionValidity($node, $child); - - $result = parent::insertBefore($node, $child); - if ($result !== false) { - if ($result instanceof TemplateElement) { - ElementMap::set($result); - } - if ($child instanceof TemplateElement) { - ElementMap::delete($child); - } - } - return $result; - } - - public function removeChild($child) { - $result = parent::removeChild($child); - if ($result !== false && $result instanceof TemplateElement) { - ElementMap::delete($child); - } - return $result; - } - - public function replaceChild($node, $child) { - $result = parent::replaceChild($node, $child); - if ($result !== false) { - if ($result instanceof TemplateElement) { - ElementMap::set($child); - } - if ($child instanceof TemplateElement) { - ElementMap::delete($child); - } - } - return $result; - } - - - protected function preInsertionValidity(\DOMNode $node, ?\DOMNode $child = null) { - // "parent" in the spec comments below is $this - - # 1. If parent is not a Document, DocumentFragment, or Element node, then throw - # a "HierarchyRequestError" DOMException. - // Not necessary because they've been disabled and return hierarchy request - // errors in "leaf nodes". - - # 2. If node is a host-including inclusive ancestor of parent, then throw a - # "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. - - # 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. - - # 4. If node is not a DocumentFragment, DocumentType, Element, Text, - # 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. - // Not necessary because they've been disabled and return hierarchy request - // errors in "leaf nodes". - - # 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. - } -} diff --git a/lib/traits/DocumentOrElement.php b/lib/traits/DocumentOrElement.php index 6af1c01..cafb683 100644 --- a/lib/traits/DocumentOrElement.php +++ b/lib/traits/DocumentOrElement.php @@ -47,7 +47,8 @@ trait DocumentOrElement { # # The comparisons for the classes must be done in an ASCII case-insensitive manner # if root’s node document’s mode is "quirks"; otherwise in an identical to manner. - // DEVIATION: Since we can't just create a \DOMNodeList we must instead query the document with XPath with the root element to get a list. + // DEVIATION: Since we can't just create a \DOMNodeList we must instead query + // the document with XPath with the root element to get a list. $query = '//*'; foreach ($inputTokens as $token) { diff --git a/lib/traits/Moonwalk.php b/lib/traits/Moonwalk.php index 145e0f1..8e34a4b 100644 --- a/lib/traits/Moonwalk.php +++ b/lib/traits/Moonwalk.php @@ -46,7 +46,7 @@ trait Moonwalk { // templates; if it is change node to the template element and reprocess. Magic! // Can walk backwards THROUGH templates! if ($node instanceof DocumentFragment) { - foreach (ElementMap::getIterator() as $element) { + foreach (ElementSet::getIterator() as $element) { if ($element->ownerDocument->isSameNode($node->ownerDocument) && $element instanceof TemplateElement && $element->content->isSameNode($node)) { $node = $element; continue; diff --git a/lib/traits/ParentNode.php b/lib/traits/ParentNode.php index 0dd0d26..ae0a020 100644 --- a/lib/traits/ParentNode.php +++ b/lib/traits/ParentNode.php @@ -8,240 +8,186 @@ declare(strict_types=1); namespace MensBeam\HTML\DOM; -if (version_compare(\PHP_VERSION, '8.0', '>=')) { - # 4.2.6. Mixin ParentNode - trait ParentNode { - protected function __get_children(): \DOMNodeList { - # The children getter steps are to return an HTMLCollection collection rooted at - # this matching only element children. - // DEVIATION: HTMLCollection doesn't exist in PHP's DOM, and \DOMNodeList is - // almost identical; so, using that. PHP's DOM doesn't provide the end user any - // way to create a \DOMNodeList from scratch, so going to cheat and use XPath to - // make one for us. - - $isDocument = ($this instanceof Document); - $document = ($isDocument) ? $this : $this->ownerDocument; - return $document->xpath->query('//*', (!$isDocument) ? $this : null); - } +# 4.2.6. Mixin ParentNode +trait ParentNode { + use Node, ParentNodePolyfill; + + + protected function __get_children(): \DOMNodeList { + # The children getter steps are to return an HTMLCollection collection rooted at + # this matching only element children. + // DEVIATION: HTMLCollection doesn't exist in PHP's DOM, and \DOMNodeList is + // almost identical; so, using that. PHP's DOM doesn't provide the end user any + // way to create a \DOMNodeList from scratch, so going to cheat and use XPath to + // make one for us. + + $isDocument = ($this instanceof Document); + $document = ($isDocument) ? $this : $this->ownerDocument; + return $document->xpath->query('//*', (!$isDocument) ? $this : null); + } - public function replaceChildren(...$nodes) { - # The replaceChildren(nodes) method steps are: - # 1. Let node be the result of converting nodes into a node given nodes and - # this’s node document. - $node = $this->convertNodesToNode($nodes); - # 2. Ensure pre-insertion validity of node into this before null. - $this->preInsertionValidity($node); - # 3. Replace all with node within this. - # - # To replace all with a node within a parent, run these steps: - # 1. Let removedNodes be parent’s children. - $removedNodes = $this->childNodes; - # 2. Let addedNodes be the empty set. - $addedNodes = []; - # 3. If node is a DocumentFragment node, then set addedNodes to node’s children. - if ($node instanceof DocumentFragment) { - $addedNodes = $node->childNodes; - } - # 4. Otherwise, if node is non-null, set addedNodes to « node ». - elseif ($node !== null) { - $addedNodes = node; - } - # 5. Remove all parent’s children, in tree order, with the suppress observers - # flag set. - // DEVIATION: There is no scripting in this implementation, so cannnot set - // suppress observers flag. - while ($this->hasChildNodes()) { - $this->removeChild($this->firstChild); - } - # 6. If node is non-null, then insert node into parent before null with the - # suppress observers flag set. - // DEVIATION: There is no scripting in this implementation, so cannnot set - // suppress observers flag. - if ($node !== null) { - $this->appendChild($node); + + public function appendChild($node) { + $this->preInsertionValidity($node); + + $result = parent::appendChild($node); + if ($result !== false && $result instanceof TemplateElement) { + if ($result instanceof TemplateElement) { + ElementSet::add($result); } - # 7. If either addedNodes or removedNodes is not empty, then queue a tree - # mutation record for parent with addedNodes, removedNodes, null, and null. - // DEVIATION: There is no scripting in this implementation } + return $result; + } - private function convertNodesToNode(array $nodes): \DOMNode { - # To convert nodes into a node, given nodes and document, run these steps: - # 1. Let node be null. - # 2. Replace each string in nodes with a new Text node whose data is the string - # and node document is document. - # 3. If nodes contains one node, then set node to nodes[0]. - # 4. Otherwise, set node to a new DocumentFragment node whose node document is - # document, and then append each node in nodes, if any, to it. - // The spec would have us iterate through the provided nodes and then iterate - // through them again to append. Let's optimize this a wee bit, shall we? - $document = ($this instanceof Document) ? $this : $this->ownerDocument; - $node = ($node->length > 1) ? $document->createDocumentFragment() : null; - foreach ($nodes as &$n) { - // Can't do union types until PHP 8... OTL - if (!$n instanceof \DOMNode && !is_string($n)) { - trigger_error(sprintf("Uncaught TypeError: %s::%s(): Argument #1 (\$%s) must be of type \DOMNode|string, %s given", __CLASS__, __METHOD__, 'nodes', gettype($n))); - } - - if (is_string($n)) { - $n = $this->ownerDocument->createTextNode($n); - } - - if ($node !== null) { - $node->appendChild($n); - } else { - $node = $n; - } - } + public function insertBefore($node, $child = null) { + $this->preInsertionValidity($node, $child); - return $node; + $result = parent::insertBefore($node, $child); + if ($result !== false) { + if ($result instanceof TemplateElement) { + ElementSet::add($result); + } + if ($child instanceof TemplateElement) { + ElementSet::delete($child); + } } + return $result; } -} else { - trait ParentNode { - protected function __get_childElementCount(): int { - # The childElementCount getter steps are to return the number of children of - # this that are elements. - $count = 0; - foreach ($this->childNodes as $child) { - if ($child instanceof Element) { - $count++; - } - } - return $count; + public function removeChild($child) { + $result = parent::removeChild($child); + if ($result !== false && $result instanceof TemplateElement) { + ElementSet::delete($child); } + return $result; + } - protected function __get_children(): \DOMNodeList { - # The children getter steps are to return an HTMLCollection collection rooted at - # this matching only element children. - // DEVIATION: HTMLCollection doesn't exist in PHP's DOM, and \DOMNodeList is - // almost identical; so, using that. PHP's DOM doesn't provide the end user any - // way to create a \DOMNodeList from scratch, so going to cheat and use XPath to - // make one for us. - - $isDocument = ($this instanceof Document); - $document = ($isDocument) ? $this : $this->ownerDocument; - return $document->xpath->query('//*', (!$isDocument) ? $this : null); + public function replaceChild($node, $child) { + $result = parent::replaceChild($node, $child); + if ($result !== false) { + if ($result instanceof TemplateElement) { + ElementSet::add($child); + } + if ($child instanceof TemplateElement) { + ElementSet::delete($child); + } } + return $result; + } - protected function __get_firstElementChild(): Element { - # The firstElementChild getter steps are to return the first child that is an - # element; otherwise null. - foreach ($this->childNodes as $child) { - if ($child instanceof Element) { - return $child; - } - } - return null; + public function replaceChildren(...$nodes) { + # The replaceChildren(nodes) method steps are: + # 1. Let node be the result of converting nodes into a node given nodes and + # this’s node document. + $node = $this->convertNodesToNode($nodes); + # 2. Ensure pre-insertion validity of node into this before null. + $this->preInsertionValidity($node); + # 3. Replace all with node within this. + # + # To replace all with a node within a parent, run these steps: + # 1. Let removedNodes be parent’s children. + $removedNodes = $this->childNodes; + # 2. Let addedNodes be the empty set. + $addedNodes = []; + # 3. If node is a DocumentFragment node, then set addedNodes to node’s children. + if ($node instanceof DocumentFragment) { + $addedNodes = $node->childNodes; + } + # 4. Otherwise, if node is non-null, set addedNodes to « node ». + elseif ($node !== null) { + $addedNodes = node; + } + # 5. Remove all parent’s children, in tree order, with the suppress observers + # flag set. + // DEVIATION: There is no scripting in this implementation, so cannnot set + // suppress observers flag. + while ($this->hasChildNodes()) { + $this->removeChild($this->firstChild); + } + # 6. If node is non-null, then insert node into parent before null with the + # suppress observers flag set. + // DEVIATION: There is no scripting in this implementation, so cannnot set + // suppress observers flag. + if ($node !== null) { + $this->appendChild($node); } + # 7. If either addedNodes or removedNodes is not empty, then queue a tree + # mutation record for parent with addedNodes, removedNodes, null, and null. + // DEVIATION: There is no scripting in this implementation + } - protected function __get_lastElementChild(): Element { - # The lastElementChild getter steps are to return the last child that is an - # element; otherwise null. - for ($i = $this->childNodes->length - 1; $i >= 0; $i--) { - $child = $this->childNodes->item($i); - if ($child instanceof Element) { - return $child; - } - } - return null; + protected function preInsertionValidity(\DOMNode $node, ?\DOMNode $child = null) { + // "parent" in the spec comments below is $this + + # 1. If parent is not a Document, DocumentFragment, or Element node, then throw + # a "HierarchyRequestError" DOMException. + // Not necessary because they've been disabled and return hierarchy request + // errors in "leaf nodes". + + # 2. If node is a host-including inclusive ancestor of parent, then throw a + # "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. + + # 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. + + # 4. If node is not a DocumentFragment, DocumentType, Element, Text, + # 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. + // Not necessary because they've been disabled and return hierarchy request + // errors in "leaf nodes". - public function append(...$nodes): void { - # The append(nodes) method steps are: - # 1. Let node be the result of converting nodes into a node given nodes and - # this’s node document. - $node = $this->convertNodesToNode($nodes); - # 2. Append node to this. - $this->appendChild($node); - } + # 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. + } - public function prepend(...$nodes): void { - # The prepend(nodes) method steps are: - # - # 1. Let node be the result of converting nodes into a node given nodes and - # this’s node document. - $node = $this->convertNodesToNode($nodes); - # 2. Pre-insert node into this before this’s first child. - $this->insertBefore($node, $this->firstChild); - } - public function replaceChildren(...$nodes) { - # The replaceChildren(nodes) method steps are: - # 1. Let node be the result of converting nodes into a node given nodes and - # this’s node document. - $node = $this->convertNodesToNode($nodes); - # 2. Ensure pre-insertion validity of node into this before null. - $this->preInsertionValidity($node); - # 3. Replace all with node within this. - # - # To replace all with a node within a parent, run these steps: - # 1. Let removedNodes be parent’s children. - $removedNodes = $this->childNodes; - # 2. Let addedNodes be the empty set. - $addedNodes = []; - # 3. If node is a DocumentFragment node, then set addedNodes to node’s children. - if ($node instanceof DocumentFragment) { - $addedNodes = $node->childNodes; + private function convertNodesToNode(array $nodes): \DOMNode { + # To convert nodes into a node, given nodes and document, run these steps: + # 1. Let node be null. + # 2. Replace each string in nodes with a new Text node whose data is the string + # and node document is document. + # 3. If nodes contains one node, then set node to nodes[0]. + # 4. Otherwise, set node to a new DocumentFragment node whose node document is + # document, and then append each node in nodes, if any, to it. + // The spec would have us iterate through the provided nodes and then iterate + // through them again to append. Let's optimize this a wee bit, shall we? + $document = ($this instanceof Document) ? $this : $this->ownerDocument; + $node = ($node->length > 1) ? $document->createDocumentFragment() : null; + foreach ($nodes as &$n) { + // Can't do union types until PHP 8... OTL + if (!$n instanceof \DOMNode && !is_string($n)) { + trigger_error(sprintf("Uncaught TypeError: %s::%s(): Argument #1 (\$%s) must be of type \DOMNode|string, %s given", __CLASS__, __METHOD__, 'nodes', gettype($n))); } - # 4. Otherwise, if node is non-null, set addedNodes to « node ». - elseif ($node !== null) { - $addedNodes = node; - } - # 5. Remove all parent’s children, in tree order, with the suppress observers - # flag set. - // DEVIATION: There is no scripting in this implementation so cannnot set - // suppress observers flag. - while ($this->hasChildNodes()) { - $this->removeChild($this->firstChild); + + if (is_string($n)) { + $n = $this->ownerDocument->createTextNode($n); } - # 6. If node is non-null, then insert node into parent before null with the - # suppress observers flag set. - // DEVIATION: There is no scripting in this implementation so cannnot set - // suppress observers flag. + if ($node !== null) { - $this->appendChild($node); + $node->appendChild($n); + } else { + $node = $n; } - # 7. If either addedNodes or removedNodes is not empty, then queue a tree - # mutation record for parent with addedNodes, removedNodes, null, and null. - // DEVIATION: There is no scripting in this implementation } - private function convertNodesToNode(array $nodes): \DOMNode { - # To convert nodes into a node, given nodes and document, run these steps: - # 1. Let node be null. - # 2. Replace each string in nodes with a new Text node whose data is the string - # and node document is document. - # 3. If nodes contains one node, then set node to nodes[0]. - # 4. Otherwise, set node to a new DocumentFragment node whose node document is - # document, and then append each node in nodes, if any, to it. - // The spec would have us iterate through the provided nodes and then iterate - // through them again to append. Let's optimize this a wee bit, shall we? - $document = ($this instanceof Document) ? $this : $this->ownerDocument; - $node = ($node->length > 1) ? $document->createDocumentFragment() : null; - foreach ($nodes as $n) { - // Can't do union types until PHP 8... OTL - if (!$n instanceof \DOMNode && !is_string($n)) { - $type = gettype($n); - if ($type === 'object') { - $type = get_class($n); - } - throw new Exception(Exception::ARGUMENT_TYPE_ERROR, 1, 'nodes', '[\DOMNode|string]', $n); - } - - $nn = (!is_string($n)) ? $n : $this->ownerDocument->createTextNode($n); - - if ($node !== null) { - $node->appendChild($nn); - } else { - $node = $nn; - } - } - - return $node; - } + return $node; } -} +} \ No newline at end of file diff --git a/lib/traits/ParentNodePolyfill.php b/lib/traits/ParentNodePolyfill.php new file mode 100644 index 0000000..50a3fb8 --- /dev/null +++ b/lib/traits/ParentNodePolyfill.php @@ -0,0 +1,77 @@ +childNodes as $child) { + if ($child instanceof Element) { + $count++; + } + } + + return $count; + } + + protected function __get_firstElementChild(): Element { + # The firstElementChild getter steps are to return the first child that is an + # element; otherwise null. + foreach ($this->childNodes as $child) { + if ($child instanceof Element) { + return $child; + } + } + return null; + } + + protected function __get_lastElementChild(): Element { + # The lastElementChild getter steps are to return the last child that is an + # element; otherwise null. + for ($i = $this->childNodes->length - 1; $i >= 0; $i--) { + $child = $this->childNodes->item($i); + if ($child instanceof Element) { + return $child; + } + } + + return null; + } + + + public function append(...$nodes): void { + # The append(nodes) method steps are: + # 1. Let node be the result of converting nodes into a node given nodes and + # this’s node document. + $node = $this->convertNodesToNode($nodes); + # 2. Append node to this. + $this->appendChild($node); + } + + public function prepend(...$nodes): void { + # The prepend(nodes) method steps are: + # + # 1. Let node be the result of converting nodes into a node given nodes and + # this’s node document. + $node = $this->convertNodesToNode($nodes); + # 2. Pre-insert node into this before this’s first child. + $this->insertBefore($node, $this->firstChild); + } + } +} else { + trait ParentNodePolyfill {} +}