diff --git a/lib/Attr.php b/lib/Attr.php new file mode 100644 index 0000000..ccd0d10 --- /dev/null +++ b/lib/Attr.php @@ -0,0 +1,52 @@ +innerNode->localName; + } + + protected function __get_name(): string { + // PHP's DOM does this correctly already. + return $this->innerNode->name; + } + + protected function __get_namespaceURI(): string { + // PHP's DOM does this correctly already. + return $this->innerNode->namespaceURI; + } + + protected function __get_ownerElement(): Element { + // PHP's DOM does this correctly already. + return $this->innerNode->ownerDocument->getWrapperNode($this->innerNode->ownerElement); + } + + protected function __get_prefix(): string { + // PHP's DOM does this correctly already. + return $this->innerNode->prefix; + } + + protected function __get_specified(): bool { + # Useless, always returns true + return true; + } + + protected function __get_value(): string { + // PHP's DOM does this correctly already. + return $this->innerNode->value; + } + + protected function __set_value(string $value) { + // PHP's DOM does this correctly already. + $this->innerNode->value = $value; + } +} \ No newline at end of file diff --git a/lib/Comment.php b/lib/Comment.php index a3311a2..f84b7af 100644 --- a/lib/Comment.php +++ b/lib/Comment.php @@ -9,4 +9,8 @@ declare(strict_types=1); namespace MensBeam\HTML\DOM; -class Comment extends CharacterData {} \ No newline at end of file +class Comment extends CharacterData { + public function __construct(string $data = '') { + $this->innerNode = new \DOMComment($data); + } +} \ No newline at end of file diff --git a/lib/Document.php b/lib/Document.php index f62dcdd..c41a829 100644 --- a/lib/Document.php +++ b/lib/Document.php @@ -36,7 +36,14 @@ class Document extends Node { public function createDocumentFragment(): DocumentFragment { - return $this->innerNode->getWrapperNode($this->innerNode->createDocumentFragment()); + // DocumentFragment has a public constructor that creates an inner fragment + // without an associated document, so some jiggerypokery must be done instead. + $reflector = new \ReflectionClass(__NAMESPACE__ . '\\DocumentFragment'); + $fragment = $reflector->newInstanceWithoutConstructor(); + $property = new \ReflectionProperty($text, 'innerNode'); + $property->setAccessible(true); + $property->setValue($fragment, $this->innerNode->createDocumentFragment()); + return $fragment; } public function createElement(string $localName): Element { diff --git a/lib/DocumentFragment.php b/lib/DocumentFragment.php index 5e3cbca..dcb0ca2 100644 --- a/lib/DocumentFragment.php +++ b/lib/DocumentFragment.php @@ -15,7 +15,7 @@ class DocumentFragment extends Node { protected ?\WeakReference $host = null; - protected function __construct(\DOMDocumentFragment $fragment) { - parent::__construct($fragment); + public function __construct() { + $this->innerNode = new \DOMDocumentFragment(); } } diff --git a/lib/Element.php b/lib/Element.php index 0e24104..56cbdd2 100644 --- a/lib/Element.php +++ b/lib/Element.php @@ -19,7 +19,8 @@ class Element extends Node { // around because of the wrapper classes; So, use the null namespace internally // but print out the HTML namespace instead. $namespace = $this->innerNode->namespaceURI; - return ($namespace === null) ? Parser::HTML_NAMESPACE : $namespace; + $doc = $this->ownerDocument; + return (($doc instanceof Document && !$doc instanceof XMLDocument) && $namespace === null) ? Parser::HTML_NAMESPACE : $namespace; } diff --git a/lib/HTMLCollection.php b/lib/HTMLCollection.php new file mode 100644 index 0000000..3e51a04 --- /dev/null +++ b/lib/HTMLCollection.php @@ -0,0 +1,166 @@ + in IDL instead). +# +# A collection is an object that represents a list of nodes. A collection can be +# either live or static. Unless otherwise stated, a collection must be live. +# +# If a collection is live, then the attributes and methods on that object must +# operate on the actual underlying data, not a snapshot of the data. +# +# When a collection is created, a filter and a root are associated with it. +# +# The collection then represents a view of the subtree rooted at the +# collection’s root, containing only nodes that match the given filter. The view +# is linear. In the absence of specific requirements to the contrary, the nodes +# within the collection must be sorted in tree order. +class HTMLCollection implements \ArrayAccess, \Countable, \Iterator { + use MagicProperties; + + protected ?\Closure $filter = null; + protected int $_length = 0; + protected ?array $nodeArray = null; + protected int $position = 0; + + + protected function __get_length(): int { + # The length attribute must return the number of nodes represented by the + # collection. + return $this->count(); + } + + + protected function __construct(array|\Closure $arrayOrClosure = []) { + // In this implementation the root part of the creation is handled either before + // the NodeList is created (array) or within the filter (\Closure). + if ($arrayOrClosure === null) { + $arrayOrClosure = []; + } + + if (is_callable($arrayOrClosure)) { + $this->filter = $arrayOrClosure; + } else { + // Check types while also unpacking the iterable. + $array = []; + foreach ($arrayOrClosure as $i) { + if (!$i instanceof Element) { + $type = gettype($i); + if ($type === 'object') { + $type = get_class($i); + } + throw new Exception(Exception::ARGUMENT_TYPE_ERROR, 1, 'arrayOrClosure', 'array|\\Closure>', $type); + } + + $array[] = $i; + } + + $this->nodeArray = $array; + $this->_length = count($array); + } + } + + public function count(): int { + if ($this->nodeArray !== null) { + return $this->_length; + } + + $nodeArray = ($this->filter)(); + return count($nodeArray); + } + + public function current(): ?Element { + return $this->item($this->position); + } + + public function item(int $index): ?Element { + # The item(index) method must return the indexth node in the collection. If + # there is no indexth node in the collection, then the method must return null. + if ($index >= $this->count()) { + return null; + } + + $nodeArray = ($this->nodeArray !== null) ? $this->nodeArray : ($this->filter)(); + if (array_key_exists($index, $nodeArray)) { + return $nodeArray[$index]; + } + + return null; + } + + public function key(): int { + return $this->position; + } + + public function namedItem(string $name): ?Element { + # The namedItem(key) method steps are: + // The interface says to use "name" as the argument while the steps below show + // to use "key" *shrug*. + # 1. If key is the empty string, return null. + if ($name === '') { + return null; + } + + # 2. Return the first element in the collection for which at least one of the following is true: + # • it has an ID which is key; + # • it is in the HTML namespace and has a name attribute whose value is key; + # or null if there is no such element. + $nodeArray = ($this->nodeArray !== null) ? $this->nodeArray : ($this->filter)(); + foreach ($nodeArray as $element) { + if ($element->getAttribute('id') === $name || $element->namespaceURI === Parser::HTML_NAMESPACE && $element->getAttribute('name') === $name) { + return $element; + } + } + + return null; + } + + public function next(): void { + $this->position++; + } + + public function rewind(): void { + $this->position = 0; + } + + public function offsetExists($offset): bool { + if (is_int($offset)) { + $nodeArray = ($this->nodeArray !== null) ? $this->nodeArray : ($this->filter)(); + return array_key_exists($offset, $nodeArray); + } + + return ($this->namedItem($offset) !== null); + } + + public function offsetGet($offset): ?Element { + return (is_int($offset)) ? $this->item($offset) : $this->namedItem($offset); + } + + public function offsetSet($offset, $value): void { + // NodeLists are immutable; the spec is ambiguous as to what to do here. + // Browsers silently fail here, so that's what we're going to do. + } + + public function offsetUnset($offset): void { + // NodeLists are immutable; the spec is ambiguous as to what to do here. + // Browsers silently fail here, so that's what we're going to do. + } + + public function valid() { + $this->offsetExists($this->position); + } +} \ No newline at end of file diff --git a/lib/Node.php b/lib/Node.php index 277fcd7..4efed1c 100644 --- a/lib/Node.php +++ b/lib/Node.php @@ -332,7 +332,7 @@ abstract class Node { })->current() !== null); } - public function getRootNode(array $options): Node { + 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 diff --git a/lib/NodeList.php b/lib/NodeList.php index 64ba77f..84cd8de 100644 --- a/lib/NodeList.php +++ b/lib/NodeList.php @@ -58,7 +58,7 @@ class NodeList implements \ArrayAccess, \Countable, \Iterator { if ($type === 'object') { $type = get_class($i); } - throw new DOMException(DOMException::ARGUMENT_TYPE_ERROR, 1, 'arrayOrClosure', 'array|\\Closure>', $type); + throw new Exception(Exception::ARGUMENT_TYPE_ERROR, 1, 'arrayOrClosure', 'array|\\Closure>', $type); } $array[] = $i; diff --git a/lib/ProcessingInstruction.php b/lib/ProcessingInstruction.php index b4b7fbf..bcfb3c4 100644 --- a/lib/ProcessingInstruction.php +++ b/lib/ProcessingInstruction.php @@ -9,4 +9,8 @@ declare(strict_types=1); namespace MensBeam\HTML\DOM; -class ProcessingInstruction extends CharacterData {} \ No newline at end of file +class ProcessingInstruction extends CharacterData { + protected function __get_target(): string { + return $this->innerNode->target; + } +} \ No newline at end of file diff --git a/lib/Text.php b/lib/Text.php index efb9019..4943aba 100644 --- a/lib/Text.php +++ b/lib/Text.php @@ -10,7 +10,24 @@ namespace MensBeam\HTML\DOM; class Text extends CharacterData { + protected function __get_wholeText(): string { + // PHP's DOM does this correctly already. + return $this->innerNode->wholeText; + } + + public function __construct(string $data = '') { $this->innerNode = new \DOMText($data); } + + + public function splitText(int $offset): Text { + // PHP DOM mostly handles this correctly with the exception of not throwing an + // exception when the offset is greater than the length, so let's fix that. + if ($offset > $this->length) { + throw new DOMException(DOMException::INDEX_SIZE_ERROR); + } + + return $this->innerNode->ownerDocument->getWrapperNode($this->innerNode->splitText($offset)); + } } \ No newline at end of file