diff --git a/lib/Attr.php b/lib/Attr.php index 2a14cfe..be3870e 100644 --- a/lib/Attr.php +++ b/lib/Attr.php @@ -18,9 +18,10 @@ class Attr extends Node { } protected function __get_name(): string { - // PHP's DOM does this correctly already. + // PHP's DOM incorrectly returns the local name instead of the qualified name + // per the specification. // Need to uncoerce string if necessary. - $name = $this->innerNode->name; + $name = $this->innerNode->nodeName; return (!str_contains(needle: 'U', haystack: $name)) ? $name : $this->uncoerceName($name); } diff --git a/lib/HTMLCollection.php b/lib/HTMLCollection.php index 4748363..6ae41f3 100644 --- a/lib/HTMLCollection.php +++ b/lib/HTMLCollection.php @@ -45,4 +45,8 @@ class HTMLCollection extends Collection { public function offsetGet($offset): ?Element { return (is_int($offset)) ? $this->item($offset) : $this->namedItem($offset); } + + public function offsetExists($offset): bool { + return (((is_int($offset)) ? $this->item($offset) : $this->namedItem($offset)) !== null); + } } \ No newline at end of file diff --git a/lib/NamedNodeMap.php b/lib/NamedNodeMap.php index c31c635..b1a8a3b 100644 --- a/lib/NamedNodeMap.php +++ b/lib/NamedNodeMap.php @@ -7,10 +7,16 @@ declare(strict_types=1); namespace MensBeam\HTML\DOM; -use MensBeam\HTML\DOM\Inner\Document as InnerDocument; +use MensBeam\HTML\DOM\Inner\{ + Document as InnerDocument, + Reflection +}; +use MensBeam\HTML\Parser\NameCoercion; class NamedNodeMap extends Collection { + use NameCoercion; + # A NamedNodeMap has an associated element (an element). protected Element $element; @@ -43,6 +49,53 @@ class NamedNodeMap extends Collection { return parent::item($index); } + public function offsetGet($offset): ?Attr { + if (is_int($offset)) { + return $this->item($offset); + } + + # A NamedNodeMap object’s supported property names are the return value of running + # these steps: + # 1. Let names be the qualified names of the attributes in this NamedNodeMap object’s + # attribute list, with duplicates omitted, in order. + // The spec is extremely vague as to what to do here, but it seems to expect + // this to be some sort of live private property that the class will poll to + // check for valid property names when trying to access them. This is + // inefficient. Going to do basically the same thing but not return a list of + // every one. It will just search the element's attribute list instead using the + // same process. + + # 2. If this NamedNodeMap object’s element is in the HTML namespace and its node + # document is an HTML document, then for each name in names: + # 1. Let lowercaseName be name, in ASCII lowercase. + # 2. If lowercaseName is not equal to name, remove name from names. + # 3. Return names. + + $innerElement = Reflection::getProtectedProperty($this->element, 'innerNode'); + $innerDocument = $innerElement->ownerDocument; + $attributes = $innerElement->attributes; + if ($attributes->length > 0) { + $coercedOffset = $this->coerceName($offset); + + foreach ($attributes as $attr) { + $name = $attr->nodeName; + if ($this->element->namespaceURI === Node::HTML_NAMESPACE && $name !== strtolower($name)) { + continue; + } + + if ($name === $offset || $name === $coercedOffset) { + return $innerDocument->getWrapperNode($attr); + } + } + } + + return null; + } + + public function offsetExists($offset): bool { + return (((is_int($offset)) ? $this->item($offset) : $this->getNamedItem($offset)) !== null); + } + public function removeNamedItem(string $qualifiedName): ?Attr { return $this->removeNamedItemNS(null, $qualifiedName); } diff --git a/tests/cases/TestAttr.php b/tests/cases/TestAttr.php index 8665faa..8a5bfb7 100644 --- a/tests/cases/TestAttr.php +++ b/tests/cases/TestAttr.php @@ -27,7 +27,7 @@ class TestAttr extends \PHPUnit\Framework\TestCase { // Coerced name $this->assertSame('poop💩', $body->getAttributeNode('poop💩')->name); // Foreign attribute name - $this->assertSame('xlink', $svg->getAttributeNodeNS(Node::XMLNS_NAMESPACE, 'xlink')->name); + $this->assertSame('xmlns:xlink', $svg->getAttributeNodeNS(Node::XMLNS_NAMESPACE, 'xlink')->name); } public function testProperty_prefix(): void { diff --git a/tests/cases/TestCollection.php b/tests/cases/TestCollection.php new file mode 100644 index 0000000..8295061 --- /dev/null +++ b/tests/cases/TestCollection.php @@ -0,0 +1,94 @@ +




'); + $body = $d->body; + $children = $body->childNodes; + foreach ($children as $key => $child) { + $this->assertTrue($child instanceof HTMLElement); + } + } + + + /** + * @covers \MensBeam\HTML\DOM\Collection::offsetGet + * @covers \MensBeam\HTML\DOM\Collection::offsetUnset + * + * @covers \MensBeam\HTML\DOM\Attr::__get_value + * @covers \MensBeam\HTML\DOM\Collection::item + * @covers \MensBeam\HTML\DOM\Document::__construct + * @covers \MensBeam\HTML\DOM\Document::__get_body + * @covers \MensBeam\HTML\DOM\Document::load + * @covers \MensBeam\HTML\DOM\DOMImplementation::__construct + * @covers \MensBeam\HTML\DOM\Element::__get_attributes + * @covers \MensBeam\HTML\DOM\NamedNodeMap::__construct + * @covers \MensBeam\HTML\DOM\NamedNodeMap::item + * @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\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 + */ + public function testMethod_offsetSet_offsetUnset(): void { + $d = new Document(''); + $body = $d->body; + $attributes = $body->attributes; + $attributes[0] = 'eek'; + $this->assertSame('ook', $attributes[0]->value); + unset($attributes[2]); + $this->assertSame('ook', $attributes[2]->value); + } +} \ No newline at end of file diff --git a/tests/cases/TestNamedNodeMap.php b/tests/cases/TestNamedNodeMap.php new file mode 100644 index 0000000..80a33da --- /dev/null +++ b/tests/cases/TestNamedNodeMap.php @@ -0,0 +1,31 @@ +', 'UTF-8'); + $body = $d->body; + $body->setAttributeNS(Node::XMLNS_NAMESPACE, 'xmlns:href', Node::HTML_NAMESPACE); + $body->setAttributeNS('https://poop💩.poop', 'poop💩:poop💩', 'poop💩'); + + $attributes = $body->attributes; + $this->assertSame('ook', $attributes['a']->value); + $this->assertSame(Node::HTML_NAMESPACE, $attributes['xmlns:href']->value); + $this->assertSame('poop💩', $attributes['poop💩:poop💩']->value); + } +} \ No newline at end of file diff --git a/tests/cases/TestNonDocumentTypeChildNode.php b/tests/cases/TestNonDocumentTypeChildNode.php index 2a74b5e..158b405 100644 --- a/tests/cases/TestNonDocumentTypeChildNode.php +++ b/tests/cases/TestNonDocumentTypeChildNode.php @@ -27,6 +27,8 @@ class TestNonDocumentTypeChildNode extends \PHPUnit\Framework\TestCase { $br2 = $body->appendChild($d->createElement('br')); $this->assertSame($br2, $br->nextElementSibling); + $this->assertNull($br2->nextElementSibling); $this->assertSame($br, $ook->previousElementSibling); + $this->assertNull($br->previousElementSibling); } } \ No newline at end of file diff --git a/tests/phpunit.dist.xml b/tests/phpunit.dist.xml index 5a77177..76b14c8 100644 --- a/tests/phpunit.dist.xml +++ b/tests/phpunit.dist.xml @@ -19,12 +19,14 @@ cases/TestAttr.php cases/TestCharacterData.php cases/TestChildNode.php + cases/TestCollection.php cases/TestDocument.php cases/TestDocumentOrElement.php cases/TestDOMImplementation.php cases/TestDOMTokenList.php cases/TestElement.php cases/TestInnerDocument.php + cases/TestNamedNodeMap.php cases/TestNode.php cases/TestNonDocumentTypeChildNode.php cases/TestParentNode.php