diff --git a/lib/CharacterData.php b/lib/CharacterData.php index 26dd191..1f708d9 100644 --- a/lib/CharacterData.php +++ b/lib/CharacterData.php @@ -10,6 +10,8 @@ namespace MensBeam\HTML\DOM; abstract class CharacterData extends Node { + use ChildNode; + protected function __get_data(): string { // PHP's DOM does this correctly already. return $this->innerNode->data; diff --git a/lib/ChildNode.php b/lib/ChildNode.php index 93e8a1b..a51bede 100644 --- a/lib/ChildNode.php +++ b/lib/ChildNode.php @@ -14,4 +14,127 @@ use MensBeam\HTML\DOM\Inner\{ trait ChildNode { + public function after(Node|string ...$nodes): void { + // After exists in PHP DOM, but it can insert incorrect nodes because of PHP + // DOM's incorrect (for HTML) pre-insertion validation. + + # The after(nodes) method steps are: + # + # 1. Let parent be this’s parent. + $inner = $this->innerNode; + $parent = $this->parentNode; + + # 2. If parent is null, then return. + if ($parent === null) { + return; + } + + # 3. Let viableNextSibling be this’s first following sibling not in nodes; + # otherwise null. + $n = $inner; + $viableNextSibling = null; + while ($n = $n->nextSibling) { + foreach ($nodes as $nodeOrString) { + if ($nodeOrString instanceof Node && $this->getInnerNode($nodeOrString) === $n) { + continue 2; + } + } + + $viableNextSibling = $n; + break; + } + + # 4. Let node be the result of converting nodes into a node, given nodes and this’s + # node document. + $node = $this->convertNodesToNode($nodes); + + # 5. Pre-insert node into parent before viableNextSibling. + $parent->insertBefore($node, ($viableNextSibling !== null) ? $inner->ownerDocument->getWrapperNode($viableNextSibling) : null); + } + + public function before(Node|string ...$nodes): void { + // Before exists in PHP DOM, but it can insert incorrect nodes because of PHP + // DOM's incorrect (for HTML) pre-insertion validation. + + # The before(nodes) method steps are: + # + # 1. Let parent be this’s parent. + $inner = $this->innerNode; + $parent = $this->parentNode; + + # 2. If parent is null, then return. + if ($parent === null) { + return; + } + + # 3. Let viablePreviousSibling be this’s first preceding sibling not in nodes; + # otherwise null. + $n = $inner; + $viablePreviousSibling = null; + while ($n = $n->previousSibling) { + foreach ($nodes as $nodeOrString) { + if ($nodeOrString instanceof Node && $this->getInnerNode($nodeOrString) === $n) { + continue 2; + } + } + + $viablePreviousSibling = $n; + break; + } + + # 4. Let node be the result of converting nodes into a node, given nodes and + # this’s node document. + $node = $this->convertNodesToNode($nodes); + + # 5. If viablePreviousSibling is null, then set it to parent’s first child; + # otherwise to viablePreviousSibling’s next sibling. + $viablePreviousSibling = ($viablePreviousSibling === null) ? $parent->firstChild : $inner->ownerDocument->getWrapperNode($viablePreviousSibling->nextSibling); + + # 6. Pre-insert node into parent before viablePreviousSibling. + $parent->insertBefore($node, $viablePreviousSibling); + } + + public function replaceWith(Node|string ...$nodes): void { + // Before exists in PHP DOM, but it can insert incorrect nodes because of PHP + // DOM's incorrect (for HTML) pre-insertion validation. + # The replaceWith(nodes) method steps are: + # + # 1. Let parent be this’s parent. + $inner = $this->innerNode; + $parent = $this->parentNode; + + # 2. If parent is null, then return. + if ($parent === null) { + return; + } + + # 3. Let viableNextSibling be this’s first following sibling not in nodes; + # otherwise null. + $n = $inner; + $viableNextSibling = null; + while ($n = $n->nextSibling) { + foreach ($nodes as $nodeOrString) { + if ($nodeOrString instanceof Node && $this->getInnerNode($nodeOrString) === $n) { + continue 2; + } + } + + $viableNextSibling = $n; + break; + } + + # 4. Let node be the result of converting nodes into a node, given nodes and + # this’s node document. + $node = $this->convertNodesToNode($nodes); + + # 5. If this’s parent is parent, replace this with node within parent. + # Note: This could have been inserted into node. + if ($this->parentNode === $parent) { + $parent->replaceChild($node, $this); + } + # 6. Otherwise, pre-insert node into parent before viableNextSibling. + else { + $parent->insertBefore($node, ($viableNextSibling !== null) ? $inner->ownerDocument->getWrapperNode($viableNextSibling) : null); + } + } } diff --git a/lib/Inner/Document.php b/lib/Inner/Document.php index 3ac4fde..26e55eb 100644 --- a/lib/Inner/Document.php +++ b/lib/Inner/Document.php @@ -112,7 +112,7 @@ class Document extends \DOMDocument { $className = 'HTMLTemplateElement'; } // This is done until we do element classes - elseif (in_array($name, [ 'a', 'abbr', 'address', 'area', 'article', 'aside', 'audio', 'b', 'base', 'bdi', 'bdo', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'cite', 'code', 'col', 'colgroup', 'content', 'data', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'i', 'iframe', 'img', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'link', 'main', 'map', 'mark', 'marquee', 'math', 'menu', 'menuitem', 'meta', 'meter', 'nav', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'picture', 'portal', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'script', 'section', 'select', 'shadow', 'slot', 'small', 'source', 'span', 'strong', 'style', 'sub', 'summary', 'sup', 'svg', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track', 'u', 'ul', 'var', 'video', 'wbr' ])) { + elseif (in_array($name, [ 'a', 'abbr', 'address', 'area', 'article', 'aside', 'audio', 'b', 'base', 'bdi', 'bdo', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'cite', 'code', 'col', 'colgroup', 'content', 'data', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'iframe', 'img', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'link', 'main', 'map', 'mark', 'marquee', 'math', 'menu', 'menuitem', 'meta', 'meter', 'nav', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'picture', 'portal', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'script', 'section', 'select', 'shadow', 'slot', 'small', 'source', 'span', 'strong', 'style', 'sub', 'summary', 'sup', 'svg', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track', 'u', 'ul', 'var', 'video', 'wbr' ])) { $className = 'HTMLElement'; } # If name is a valid custom element name, then return HTMLElement. diff --git a/lib/Node.php b/lib/Node.php index 4b020ad..268e4bb 100644 --- a/lib/Node.php +++ b/lib/Node.php @@ -981,6 +981,33 @@ abstract class Node { return false; } + protected function convertNodesToNode(array $nodes): Node { + # 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? + $doc = (!$this instanceof Document) ? $this->ownerDocument : $this; + $node = (count($nodes) !== 1) ? $doc->createDocumentFragment() : null; + foreach ($nodes as $k => $n) { + if (is_string($n)) { + $n = $doc->createTextNode($n); + } + + if ($node !== null) { + $node->appendChild($n); + } else { + $node = $n; + } + } + + return $node; + } + protected function getInnerDocument(): InnerDocument { return ($this instanceof Document) ? $this->innerNode : $this->innerNode->ownerDocument; } @@ -1231,18 +1258,9 @@ abstract class Node { // below walks through this node and temporarily replaces foreign descendants // with bullshit elements which are then replaced once the node is inserted. if ($element->namespaceURI === null && ($this instanceof DocumentFragment || $this->getRootNode() !== null) && $element->hasChildNodes()) { - // XPath can't easily match just unprefixed elements, so we have to do this the - // old fashioned way by walking the DOM. - $foreign = $this->walkInner($element, function(\DOMNode $n) { - if ($n instanceof \DOMElement && ($n->parentNode !== null && $n->parentNode->namespaceURI === null) && $n->namespaceURI !== null && $n->prefix === '') { - return self::WALK_ACCEPT | self::WALK_SKIP_CHILDREN; - } - - return self::WALK_REJECT; - }); - + $foreign = $element->ownerDocument->xpath->query('.//*[parent::*[namespace-uri()=""] and not(namespace-uri()="") and name()=local-name()]', $element); $this->bullshitReplacements = []; - if ($foreign->current() !== null) { + if ($foreign->length > 0) { $count = 0; $doc = $this->getInnerDocument(); foreach ($foreign as $f) { diff --git a/lib/ParentNode.php b/lib/ParentNode.php index 9542c2b..c151970 100644 --- a/lib/ParentNode.php +++ b/lib/ParentNode.php @@ -16,6 +16,64 @@ use Symfony\Component\CssSelector\CssSelectorConverter, trait ParentNode { + protected function __get_childElementCount(): int { + return $this->innerNode->childElementCount; + } + + protected function __get_children(): HTMLCollection { + $doc = $this->getInnerDocument(); + // HTMLCollections cannot be created from their constructors normally. + return Reflection::createFromProtectedConstructor(__NAMESPACE__ . '\\HTMLCollection', $doc, $doc->xpath->query('.//*', $this->innerNode)); + } + + protected function __get_firstElementChild(): ?Element { + $result = $this->innerNode->firstElementChild; + return ($result !== null) ? $this->getInnerDocument()->getWrapperNode($result) : null; + } + + protected function __get_lastElementChild(): ?Element { + $result = $this->innerNode->lastElementChild; + return ($result !== null) ? $this->getInnerDocument()->getWrapperNode($result) : null; + } + + + public function append(Node|string ...$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(Node|string ...$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(Node|string ...$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. Ensure pre-insertion validity of node into this before null. + $this->preInsertionValidity($node); + + # 3. Replace all with node within this. + while ($this->innerNode->hasChildNodes()) { + $this->innerNode->removeChild($this->innerNode->firstChild); + } + + $this->appendChild($node); + } + public function querySelector(string $selectors): ?Element { # The querySelector(selectors) method steps are to return the first result of # running scope-match a selectors string selectors against this, if the result @@ -42,11 +100,11 @@ trait ParentNode { */ public function walk(?\Closure $filter = null, bool $includeReferenceNode = false): \Generator { if ($this instanceof DocumentFragment || (!$this instanceof DocumentFragment && !$includeReferenceNode)) { - $node = $node->firstChild; + $node = $this->innerNode->firstChild; } if ($node !== null) { - $doc = (!$node instanceof InnerDocument) ? $node->ownerDocument : $node; + $doc = $this->getInnerDocument(); do { $next = $node->nextSibling; @@ -63,11 +121,11 @@ trait ParentNode { continue 2; case Node::WALK_REJECT: break; - default: return; + default: throw new DOMException(DOMException::SYNTAX_ERROR); } if ($node->parentNode !== null && $node->hasChildNodes()) { - yield from $node->walk($filter); + yield from $wrapperNode->walk($filter); } } while ($node = $next); } @@ -99,36 +157,4 @@ trait ParentNode { $nodeList = $this->getInnerDocument()->xpath->query($s, $this->innerNode); return $nodeList; } - - protected function walkInner(\DOMNode $node, ?\Closure $filter = null, bool $includeReferenceNode = false): \Generator { - if (!$node instanceof DocumentFragment && !$includeReferenceNode) { - $node = $node->firstChild; - } - - if ($node !== null) { - $doc = (!$node instanceof InnerDocument) ? $node->ownerDocument : $node; - - do { - $next = $node->nextSibling; - $result = ($filter === null) ? Node::WALK_ACCEPT : $filter($node); - - switch ($result) { - case Node::WALK_ACCEPT: - yield $node; - break; - case Node::WALK_ACCEPT | Node::WALK_SKIP_CHILDREN: - yield $node; - case Node::WALK_REJECT | Node::WALK_SKIP_CHILDREN: - continue 2; - case Node::WALK_REJECT: - break; - default: return; - } - - if ($node->parentNode !== null && $node->hasChildNodes()) { - yield from $this->walkInner($node, $filter); - } - } while ($node = $next); - } - } } diff --git a/tests/cases/TestChildNode.php b/tests/cases/TestChildNode.php new file mode 100644 index 0000000..4f48d6a --- /dev/null +++ b/tests/cases/TestChildNode.php @@ -0,0 +1,67 @@ +appendChild($d->createElement('html')); + $body = $d->documentElement->appendChild($d->createElement('body')); + $div = $body->appendChild($d->createElement('div')); + $o = $body->appendChild($d->createTextNode('ook')); + $div2 = $body->appendChild($d->createElement('div')); + + // On node with parent + $div->after($d->createElement('span'), $o, 'eek'); + $this->assertSame('
ookeek
', (string)$body); + $div->after($o); + $this->assertSame('
ookeek
', (string)$body); + + + // On node with no parent + $c = $d->createComment('ook'); + $this->assertNull($c->after($d->createTextNode('ook'))); + + // On node with parent + $br = $body->insertBefore($d->createElement('br'), $div); + $e = $d->createTextNode('eek'); + $div->before($d->createElement('span'), $o, 'eek', $e, $br); + $this->assertSame('ookeekeek
eek
', (string)$body); + $div->before($o); + $this->assertSame('eekeek
ook
eek
', (string)$body); + + // On node with no parent + $c = $d->createComment('ook'); + $this->assertNull($c->before($d->createTextNode('ook'))); + + // On node with parent + $s = $d->createElement('span'); + $br->replaceWith('ack', $o, $e, $s); + $this->assertSame('eekackookeek
eek
', (string)$body); + $s->replaceWith($o); + $this->assertSame('eekackeekook
eek
', (string)$body); + + // On node with no parent + $c = $d->createComment('ook'); + $this->assertNull($c->replaceWith($d->createTextNode('ook'))); + + // Parent within node + $o->replaceWith('poo', $o, $e); + $this->assertSame('eekackpooookeek
eek
', (string)$body); + } +} \ No newline at end of file diff --git a/tests/cases/TestElement.php b/tests/cases/TestElement.php index 75c074d..407adcd 100644 --- a/tests/cases/TestElement.php +++ b/tests/cases/TestElement.php @@ -422,10 +422,10 @@ class TestElement extends \PHPUnit\Framework\TestCase { * @covers \MensBeam\HTML\DOM\Node::postParsingTemplatesFix * @covers \MensBeam\HTML\DOM\Node::preInsertionBugFixes * @covers \MensBeam\HTML\DOM\Node::preInsertionValidity - * @covers \MensBeam\HTML\DOM\ParentNode::walkInner * @covers \MensBeam\HTML\DOM\Text::__construct * @covers \MensBeam\HTML\DOM\Inner\Document::__construct * @covers \MensBeam\HTML\DOM\Inner\Document::__get_wrapperNode + * @covers \MensBeam\HTML\DOM\Inner\Document::__get_xpath * @covers \MensBeam\HTML\DOM\Inner\Document::getWrapperNode * @covers \MensBeam\HTML\DOM\Inner\NodeCache::get * @covers \MensBeam\HTML\DOM\Inner\NodeCache::has diff --git a/tests/cases/TestNode.php b/tests/cases/TestNode.php index 3686bd3..52eaac5 100644 --- a/tests/cases/TestNode.php +++ b/tests/cases/TestNode.php @@ -66,11 +66,11 @@ class TestNode extends \PHPUnit\Framework\TestCase { * @covers \MensBeam\HTML\DOM\Node::postParsingTemplatesFix * @covers \MensBeam\HTML\DOM\Node::preInsertionBugFixes * @covers \MensBeam\HTML\DOM\Node::preInsertionValidity - * @covers \MensBeam\HTML\DOM\ParentNode::walkInner * @covers \MensBeam\HTML\DOM\ProcessingInstruction::__construct * @covers \MensBeam\HTML\DOM\Text::__construct * @covers \MensBeam\HTML\DOM\Inner\Document::__construct * @covers \MensBeam\HTML\DOM\Inner\Document::__get_wrapperNode + * @covers \MensBeam\HTML\DOM\Inner\Document::__get_xpath * @covers \MensBeam\HTML\DOM\Inner\Document::getWrapperNode * @covers \MensBeam\HTML\DOM\Inner\NodeCache::get * @covers \MensBeam\HTML\DOM\Inner\NodeCache::has diff --git a/tests/cases/TestParentNode.php b/tests/cases/TestParentNode.php index bc4078e..d404f3f 100644 --- a/tests/cases/TestParentNode.php +++ b/tests/cases/TestParentNode.php @@ -11,12 +11,148 @@ namespace MensBeam\HTML\DOM\TestCase; use MensBeam\HTML\DOM\{ Document, DOMException, + Element, Node }; /** @covers \MensBeam\HTML\DOM\ParentNode */ class TestParentNode extends \PHPUnit\Framework\TestCase { + /** + * @covers \MensBeam\HTML\DOM\ParentNode::append + * + * @covers \MensBeam\HTML\DOM\Document::__construct + * @covers \MensBeam\HTML\DOM\Document::createDocumentFragment + * @covers \MensBeam\HTML\DOM\Document::createElement + * @covers \MensBeam\HTML\DOM\Document::createTextNode + * @covers \MensBeam\HTML\DOM\Document::load + * @covers \MensBeam\HTML\DOM\DocumentFragment::__construct + * @covers \MensBeam\HTML\DOM\DOMImplementation::__construct + * @covers \MensBeam\HTML\DOM\Element::__construct + * @covers \MensBeam\HTML\DOM\Element::__get_innerHTML + * @covers \MensBeam\HTML\DOM\Node::__construct + * @covers \MensBeam\HTML\DOM\Node::__get_ownerDocument + * @covers \MensBeam\HTML\DOM\Node::appendChild + * @covers \MensBeam\HTML\DOM\Node::convertNodesToNode + * @covers \MensBeam\HTML\DOM\Node::getInnerDocument + * @covers \MensBeam\HTML\DOM\Node::getInnerNode + * @covers \MensBeam\HTML\DOM\Node::hasChildNodes + * @covers \MensBeam\HTML\DOM\Node::postInsertionBugFixes + * @covers \MensBeam\HTML\DOM\Node::postParsingTemplatesFix + * @covers \MensBeam\HTML\DOM\Node::preInsertionBugFixes + * @covers \MensBeam\HTML\DOM\Node::preInsertionValidity + * @covers \MensBeam\HTML\DOM\NonElementParentNode::getElementById + * @covers \MensBeam\HTML\DOM\Text::__construct + * @covers \MensBeam\HTML\DOM\Inner\Document::__construct + * @covers \MensBeam\HTML\DOM\Inner\Document::__get_wrapperNode + * @covers \MensBeam\HTML\DOM\Inner\Document::__get_xpath + * @covers \MensBeam\HTML\DOM\Inner\Document::getWrapperNode + * @covers \MensBeam\HTML\DOM\Inner\NodeCache::get + * @covers \MensBeam\HTML\DOM\Inner\NodeCache::has + * @covers \MensBeam\HTML\DOM\Inner\NodeCache::key + * @covers \MensBeam\HTML\DOM\Inner\NodeCache::set + * @covers \MensBeam\HTML\DOM\Inner\Reflection::createFromProtectedConstructor + * @covers \MensBeam\HTML\DOM\Inner\Reflection::getProtectedProperty + */ + public function testMethod_append(): void { + $d = new Document('
ook
eek
'); + $eek = $d->getElementById('eek'); + $eek->append('ook', $d->createElement('br')); + $eek->append('eek'); + $this->assertSame('eekook
eek', $eek->innerHTML); + } + + + /** + * @covers \MensBeam\HTML\DOM\ParentNode::prepend + * + * @covers \MensBeam\HTML\DOM\Document::__construct + * @covers \MensBeam\HTML\DOM\Document::createDocumentFragment + * @covers \MensBeam\HTML\DOM\Document::createElement + * @covers \MensBeam\HTML\DOM\Document::createTextNode + * @covers \MensBeam\HTML\DOM\Document::load + * @covers \MensBeam\HTML\DOM\DocumentFragment::__construct + * @covers \MensBeam\HTML\DOM\DOMImplementation::__construct + * @covers \MensBeam\HTML\DOM\Element::__construct + * @covers \MensBeam\HTML\DOM\Element::__get_innerHTML + * @covers \MensBeam\HTML\DOM\Node::__construct + * @covers \MensBeam\HTML\DOM\Node::__get_firstChild + * @covers \MensBeam\HTML\DOM\Node::__get_ownerDocument + * @covers \MensBeam\HTML\DOM\Node::appendChild + * @covers \MensBeam\HTML\DOM\Node::convertNodesToNode + * @covers \MensBeam\HTML\DOM\Node::getInnerDocument + * @covers \MensBeam\HTML\DOM\Node::getInnerNode + * @covers \MensBeam\HTML\DOM\Node::hasChildNodes + * @covers \MensBeam\HTML\DOM\Node::insertBefore + * @covers \MensBeam\HTML\DOM\Node::postInsertionBugFixes + * @covers \MensBeam\HTML\DOM\Node::postParsingTemplatesFix + * @covers \MensBeam\HTML\DOM\Node::preInsertionBugFixes + * @covers \MensBeam\HTML\DOM\Node::preInsertionValidity + * @covers \MensBeam\HTML\DOM\NonElementParentNode::getElementById + * @covers \MensBeam\HTML\DOM\Text::__construct + * @covers \MensBeam\HTML\DOM\Inner\Document::__construct + * @covers \MensBeam\HTML\DOM\Inner\Document::__get_wrapperNode + * @covers \MensBeam\HTML\DOM\Inner\Document::__get_xpath + * @covers \MensBeam\HTML\DOM\Inner\Document::getWrapperNode + * @covers \MensBeam\HTML\DOM\Inner\NodeCache::get + * @covers \MensBeam\HTML\DOM\Inner\NodeCache::has + * @covers \MensBeam\HTML\DOM\Inner\NodeCache::key + * @covers \MensBeam\HTML\DOM\Inner\NodeCache::set + * @covers \MensBeam\HTML\DOM\Inner\Reflection::createFromProtectedConstructor + * @covers \MensBeam\HTML\DOM\Inner\Reflection::getProtectedProperty + */ + public function testMethod_prepend(): void { + $d = new Document('
ook
eek
'); + $eek = $d->getElementById('eek'); + $eek->prepend('ook', $d->createElement('br')); + $this->assertSame('ook
eek', $eek->innerHTML); + } + + + /** + * @covers \MensBeam\HTML\DOM\ParentNode::replaceChildren + * + * @covers \MensBeam\HTML\DOM\Document::__construct + * @covers \MensBeam\HTML\DOM\Document::createDocumentFragment + * @covers \MensBeam\HTML\DOM\Document::createElement + * @covers \MensBeam\HTML\DOM\Document::createTextNode + * @covers \MensBeam\HTML\DOM\Document::load + * @covers \MensBeam\HTML\DOM\DocumentFragment::__construct + * @covers \MensBeam\HTML\DOM\DOMImplementation::__construct + * @covers \MensBeam\HTML\DOM\Element::__construct + * @covers \MensBeam\HTML\DOM\Element::__get_innerHTML + * @covers \MensBeam\HTML\DOM\Node::__construct + * @covers \MensBeam\HTML\DOM\Node::__get_ownerDocument + * @covers \MensBeam\HTML\DOM\Node::appendChild + * @covers \MensBeam\HTML\DOM\Node::convertNodesToNode + * @covers \MensBeam\HTML\DOM\Node::getInnerDocument + * @covers \MensBeam\HTML\DOM\Node::getInnerNode + * @covers \MensBeam\HTML\DOM\Node::hasChildNodes + * @covers \MensBeam\HTML\DOM\Node::postInsertionBugFixes + * @covers \MensBeam\HTML\DOM\Node::postParsingTemplatesFix + * @covers \MensBeam\HTML\DOM\Node::preInsertionBugFixes + * @covers \MensBeam\HTML\DOM\Node::preInsertionValidity + * @covers \MensBeam\HTML\DOM\NonElementParentNode::getElementById + * @covers \MensBeam\HTML\DOM\Text::__construct + * @covers \MensBeam\HTML\DOM\Inner\Document::__construct + * @covers \MensBeam\HTML\DOM\Inner\Document::__get_wrapperNode + * @covers \MensBeam\HTML\DOM\Inner\Document::__get_xpath + * @covers \MensBeam\HTML\DOM\Inner\Document::getWrapperNode + * @covers \MensBeam\HTML\DOM\Inner\NodeCache::get + * @covers \MensBeam\HTML\DOM\Inner\NodeCache::has + * @covers \MensBeam\HTML\DOM\Inner\NodeCache::key + * @covers \MensBeam\HTML\DOM\Inner\NodeCache::set + * @covers \MensBeam\HTML\DOM\Inner\Reflection::createFromProtectedConstructor + * @covers \MensBeam\HTML\DOM\Inner\Reflection::getProtectedProperty + */ + public function testMethod_replaceChildren(): void { + $d = new Document('
ook
eek
'); + $eek = $d->getElementById('eek'); + $eek->replaceChildren('ook', $d->createElement('br')); + $this->assertSame('ook
', $eek->innerHTML); + } + + /** * @covers \MensBeam\HTML\DOM\ParentNode::querySelector * @covers \MensBeam\HTML\DOM\ParentNode::querySelectorAll @@ -84,4 +220,129 @@ class TestParentNode extends \PHPUnit\Framework\TestCase { $d = new Document(); $d->querySelector('fail?'); } + + + /** + * @covers \MensBeam\HTML\DOM\ParentNode::walk + * + * @covers \MensBeam\HTML\DOM\Document::__construct + * @covers \MensBeam\HTML\DOM\Document::load + * @covers \MensBeam\HTML\DOM\DocumentType::__construct + * @covers \MensBeam\HTML\DOM\DOMImplementation::__construct + * @covers \MensBeam\HTML\DOM\Element::__construct + * @covers \MensBeam\HTML\DOM\Node::__construct + * @covers \MensBeam\HTML\DOM\Node::getInnerDocument + * @covers \MensBeam\HTML\DOM\Node::hasChildNodes + * @covers \MensBeam\HTML\DOM\Node::postParsingTemplatesFix + * @covers \MensBeam\HTML\DOM\Text::__construct + * @covers \MensBeam\HTML\DOM\Inner\Document::__construct + * @covers \MensBeam\HTML\DOM\Inner\Document::__get_xpath + * @covers \MensBeam\HTML\DOM\Inner\Document::getWrapperNode + * @covers \MensBeam\HTML\DOM\Inner\NodeCache::get + * @covers \MensBeam\HTML\DOM\Inner\NodeCache::has + * @covers \MensBeam\HTML\DOM\Inner\NodeCache::key + * @covers \MensBeam\HTML\DOM\Inner\NodeCache::set + * @covers \MensBeam\HTML\DOM\Inner\Reflection::createFromProtectedConstructor + * @covers \MensBeam\HTML\DOM\Inner\Reflection::setProtectedProperties + */ + public function testMethod_walk(): void { + $d = new Document(<< + + +
ook
+
+

Eek

+
+

Ook

+
+
+ + + HTML); + + // Empty filter -- walk over all nodes + $w = $d->walk(); + $this->assertSame($d->doctype, $w->current()); + foreach ($w as $node); + + // Simple accept on element and reject everything else filter + $w = $d->walk(function($n) { + return ($n instanceof Element) ? Node::WALK_ACCEPT : Node::WALK_REJECT; + }); + $this->assertSame($d->documentElement, $w->current()); + foreach ($w as $node); + + // Accept element but ignore children, simple reject otherwise + $w = $d->walk(function($n) { + return ($n instanceof Element) ? Node::WALK_ACCEPT | Node::WALK_SKIP_CHILDREN : Node::WALK_REJECT; + }); + $this->assertSame($d->documentElement, $w->current()); + $this->assertNull($w->next()); + } + + + /** + * @covers \MensBeam\HTML\DOM\ParentNode::walk + * + * @covers \MensBeam\HTML\DOM\Document::__construct + * @covers \MensBeam\HTML\DOM\Document::createElement + * @covers \MensBeam\HTML\DOM\DOMException::__construct + * @covers \MensBeam\HTML\DOM\DOMImplementation::__construct + * @covers \MensBeam\HTML\DOM\Element::__construct + * @covers \MensBeam\HTML\DOM\Node::__construct + * @covers \MensBeam\HTML\DOM\Node::appendChild + * @covers \MensBeam\HTML\DOM\Node::getInnerDocument + * @covers \MensBeam\HTML\DOM\Node::getInnerNode + * @covers \MensBeam\HTML\DOM\Node::getRootNode + * @covers \MensBeam\HTML\DOM\Node::postInsertionBugFixes + * @covers \MensBeam\HTML\DOM\Node::preInsertionBugFixes + * @covers \MensBeam\HTML\DOM\Node::preInsertionValidity + * @covers \MensBeam\HTML\DOM\Inner\Document::__construct + * @covers \MensBeam\HTML\DOM\Inner\Document::getWrapperNode + * @covers \MensBeam\HTML\DOM\Inner\NodeCache::get + * @covers \MensBeam\HTML\DOM\Inner\NodeCache::has + * @covers \MensBeam\HTML\DOM\Inner\NodeCache::key + * @covers \MensBeam\HTML\DOM\Inner\NodeCache::set + * @covers \MensBeam\HTML\DOM\Inner\Reflection::createFromProtectedConstructor + * @covers \MensBeam\HTML\DOM\Inner\Reflection::getProtectedProperty + */ + public function testMethod_walk__errors(): void { + $this->expectException(DOMException::class); + $this->expectExceptionCode(DOMException::SYNTAX_ERROR); + $d = new Document(); + $d->appendChild($d->createElement('html')); + $w = $d->walk(function($n) { + return 2112; + }); + $w->current(); + } + + + public function testProperty_childElementCount(): void { + $d = new Document('
ook
eek
'); + $this->assertEquals(2, $d->body->childElementCount); + } + + + public function testProperty_children(): void { + $d = new Document('
ook
eek
'); + $this->assertEquals(2, $d->body->children->length); + } + + + public function testProperty_firstElementChild(): void { + $d = new Document('ook
ook
eek
'); + $body = $d->body; + $this->assertSame($d->getElementById('ook'), $body->firstElementChild); + $this->assertNull($d->getElementById('eek')->firstElementChild); + } + + + public function testProperty_lastElementChild(): void { + $d = new Document('ook
ook
eek
ack
'); + $body = $d->body; + $this->assertSame($d->getElementById('ack'), $body->lastElementChild); + $this->assertNull($d->getElementById('eek')->lastElementChild); + } } \ No newline at end of file diff --git a/tests/phpunit.dist.xml b/tests/phpunit.dist.xml index 7d6d314..1917f6f 100644 --- a/tests/phpunit.dist.xml +++ b/tests/phpunit.dist.xml @@ -18,6 +18,7 @@ cases/TestAttr.php cases/TestCharacterData.php + cases/TestChildNode.php cases/TestDocument.php cases/TestDocumentOrElement.php cases/TestDOMImplementation.php