Browse Source

More tests, started adding name coercion back in

wrapper-classes
Dustin Wilson 3 years ago
parent
commit
d24f569991
  1. 66
      README.md
  2. 12
      lib/Attr.php
  3. 6
      lib/CDATASection.php
  4. 9
      lib/DOMImplementation.php
  5. 63
      lib/Document.php
  6. 5
      lib/DocumentFragment.php
  7. 2
      lib/DocumentType.php
  8. 2
      lib/HTMLTemplateElement.php
  9. 22
      lib/InnerNode/Document.php
  10. 2
      lib/InnerNode/NodeMap.php
  11. 41
      lib/Node.php
  12. 5
      lib/Text.php
  13. 236
      tests/cases/TestNode.php

66
README.md

@ -6,7 +6,7 @@
# HTML DOM #
Modern DOM library written in PHP for HTML documents. This implementation is a userland extension of PHP's built-in DOM. It exists because PHP's DOM is inaccurate, inadequate for use with HTML, and buggy. This implementation attempts to fix as much as possible the inaccuracies of the PHP DOM, add in features necessary for modern HTML development, and circumvent most of the bugs without recreating the entirety of the DOM specification in userland. There is another PHP DOM library, [`phpgt/dom`][c], which does implement more of the DOM in userland; it, however, doesn't address many of PHP DOM's bugs and is incredibly slow (see [Limitations \#5][e]).
Modern DOM library written in PHP for HTML documents. This implementation is a userland extension of PHP's built-in DOM. It exists because PHP's DOM is inaccurate, inadequate for use with any HTML, and buggy. This implementation aims to fix as much as possible the inaccuracies of the PHP DOM, add in features necessary for modern HTML development, and circumvent most of the bugs.
## Usage ##
@ -61,64 +61,6 @@ Coming soon
The primary aim of this library is accuracy. If the document model differs from what the specification mandates, this is probably a bug. However, we are also constrained by PHP, which imposes various limitations. These are as follows:
1. Due to PHP's DOM being designed for XML 1.0 Second Edition, element and attribute names which are illegal in XML 1.0 Second Edition are mangled as recommended by the specification.
2. Due to a PHP bug which severely degrades performance with large documents and in consideration of existing PHP software, HTML elements are placed in the null namespace rather than in the HTML namespace.
3. While `DOMDocumentType` can be extended and registered by PHP's `DOMDocument::registerNodeClass` `DOMImplementation` cannot; this means that doctypes created with `DOMImplementation::createDocumentType` can't ever be a registered class. Therefore, doctypes remain as `DOMDocumentType` in this library and retain the same limitations as ones in PHP's DOM.
4. The DOM specification mentions that [`HTMLCollection`][a] has to be kept around for backwards compatibility in browsers, but any new implementations should use [`sequence<T>`][b] instead which is essentially just a typed array object of some kind. Any methods should also return a copy of an object instead of a reference to the platform object, meaning the bane of any web developer's existence -- live lists -- shouldn't be in any new additions to the DOM. Since this implementation is not a fully userland PHP implementation of the DOM but instead an extension of it, this implementation will use `DOMNodeList` where the specification says to use an `HTMLCollection` and an array where the specification says to use a `sequence<T>`. In addition, if the specification states to return a static `NodeList` this implementation will use `MensBeam\HTML\DOM\NodeList` instead; this is because `DOMNodeList` is always live in PHP.
5. Aside from `HTMLTemplateElement` there are no other specific element classes such as `HTMLAnchorElement`, `HTMLDivElement`, etc. and therefore are no DOM methods and properties that are specific to those elements. Implementing them is possible, but we weighed it against its utility as each specific element slows down the DOM seemingly exponentially especially when parsing serialized HTML because each element has to be converted to the specific variety manually and recursively. For instance, when parsing the [WHATWG's single page HTML specification][d] (which is an absurdly enormous HTML document on the very edge of what we should be able to parse) in our tests it takes around 6.5 seconds; with specific element classes it instead takes *15 minutes*. [`phpgt/dom`][c] mitigates this by only converting when querying for elements, but it's still slow. We decided not to go this route.
6. PHP's DOM has an `DOMDocument::adoptNode` method, but it returns an error saying it isn't implemented. `Document::adoptNode` doesn't work exactly like the specification because we cannot override the signature from the original method to make the `$node` argument a reference so that the original object variable is replaced, too. Otherwise, it works as it should; just be mindful of this unfortunate difference.
7. This implementation will not implement the `NodeIterator` and `TreeWalker` APIs. They are horribly conceived and impractical APIs that few people actually use because it's literally easier to write recursive loops to walk through the DOM than it is to use those APIs. They have instead been replaced with the `ChildNode::moonwalk`, `ParentNode::walk`, `ChildNode::walkFollowing`, and `ChildNode::walkPreceding` generators.
8. Readonly properties inherited from PHP DOM cannot be overridden in this implementation and therefore might produce incorrect data. In many cases an additional standard property exists, but in most cases the property is simply useless for HTML so does absolutely nothing. Below are the properties that will show invalid or useless data along with suggested replacements if any:
<table>
<thead>
<tr>
<th>Property</th>
<th>Replacement(s)</th>
</tr>
</thead>
<tbody>
<tr>
<th colspan="2"><code>Document</code></th>
</tr>
<tr>
<td><code>Document::documentURI</code></td>
<td><code>Document::URL</code></td>
</tr>
<tr>
<td><code>Document::actualEncoding</code></td>
<td><code>Document::characterSet</code>, <code>Document::charset</code>, <code>Document::inputEncoding</code></td>
</tr>
<tr>
<td><code>Document::encoding</code></td>
<td><code>Document::characterSet</code>, <code>Document::charset</code>, <code>Document::inputEncoding</code></td>
</tr>
<tr>
<td><code>Document::preserveWhitespace</code></td>
<td></td>
</tr>
<tr>
<td><code>Document::recover</code></td>
<td></td>
</tr>
<tr>
<td><code>Document::resolveExternals</code></td>
<td></td>
</tr>
<tr>
<td><code>Document::standalone</code></td>
<td></td>
</tr>
<tr>
<td><code>Document::strictErrorChecking</code></td>
<td></td>
</tr>
<tr>
<td><code>Document::substituteEntities</code></td>
<td></td>
</tr>
<tr>
<td><code>Document::validateOnParse</code></td>
<td></td>
</tr>
</tbody>
</table>
2. CDATA section nodes, text nodes, and document fragments per the specification can be created by their constructors independent of the `Document::createCDATASectionNode`, `Document::createTextNode`, and `Document::createDocumentFragment` methods respectively. This is not possible currently with this library and probably never will be due to the difficulty of implementing it and the awkwardness of their being different from every other node type in this respect.
3. This implementation will not implement the `NodeIterator` and `TreeWalker` APIs. They are horribly conceived and impractical APIs that few people actually use because it's literally easier to write recursive loops to walk through the DOM than it is to use those APIs. They have instead been replaced with the `ChildNode::moonwalk`, `ParentNode::walk`, `ChildNode::walkFollowing`, and `ChildNode::walkPreceding` generators.
4. Aside from `HTMLElement`, `HTMLTemplateElement`, `MathMLElement`, and `SVGElement` none of the specific derived element classes will yet be implemented.

12
lib/Attr.php

@ -12,12 +12,16 @@ namespace MensBeam\HTML\DOM;
class Attr extends Node {
protected function __get_localName(): string {
// PHP's DOM does this correctly already.
return $this->innerNode->localName;
// Need to uncoerce string if necessary.
$localName = $this->innerNode->localName;
return (!str_contains(needle: 'U', haystack: $localName)) ? $localName : $this->uncoerceName($localName);
}
protected function __get_name(): string {
// PHP's DOM does this correctly already.
return $this->innerNode->name;
// Need to uncoerce string if necessary.
$name = $this->innerNode->name;
return (!str_contains(needle: 'U', haystack: $name)) ? $name : $this->uncoerceName($name);
}
protected function __get_namespaceURI(): string {
@ -33,7 +37,9 @@ class Attr extends Node {
protected function __get_prefix(): string {
// PHP's DOM does this correctly already.
return $this->innerNode->prefix;
// Need to uncoerce string if necessary.
$prefix = $this->innerNode->prefix;
return (!str_contains(needle: 'U', haystack: $prefix)) ? $prefix : $this->uncoerceName($prefix);
}
protected function __get_specified(): bool {

6
lib/CDATASection.php

@ -9,8 +9,4 @@ declare(strict_types=1);
namespace MensBeam\HTML\DOM;
class CDATASection extends Text {
public function __construct(string $data = '') {
$this->innerNode = new \DOMCDATASection($data);
}
}
class CDATASection extends Text {}

9
lib/DOMImplementation.php

@ -7,8 +7,11 @@
declare(strict_types=1);
namespace MensBeam\HTML\DOM;
use MensBeam\HTML\DOM\InnerNode\Reflection,
MensBeam\HTML\DOM\Parser;
use MensBeam\HTML\DOM\InnerNode\{
Document as InnerDocument,
Reflection
};
use MensBeam\HTML\DOM\Parser;
class DOMImplementation {
@ -79,7 +82,7 @@ class DOMImplementation {
# 1. Validate qualifiedName.
# To validate a qualifiedName, throw an "InvalidCharacterError" DOMException if
# qualifiedName does not match the QName production.
if (!preg_match('/^([A-Z_a-z\x{C0}-\x{D6}\x{D8}-\x{F6}\x{F8}-\x{2FF}\x{370}-\x{37D}\x{37F}-\x{1FFF}\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}][A-Z_a-z\x{C0}-\x{D6}\x{D8}-\x{F6}\x{F8}-\x{2FF}\x{370}-\x{37D}\x{37F}-\x{1FFF}\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}-\.0-9\x{B7}\x{0300}-\x{036F}\x{203F}-\x{2040}]*:)?[A-Z_a-z\x{C0}-\x{D6}\x{D8}-\x{F6}\x{F8}-\x{2FF}\x{370}-\x{37D}\x{37F}-\x{1FFF}\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}][A-Z_a-z\x{C0}-\x{D6}\x{D8}-\x{F6}\x{F8}-\x{2FF}\x{370}-\x{37D}\x{37F}-\x{1FFF}\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}-\.0-9\x{B7}\x{0300}-\x{036F}\x{203F}-\x{2040}]*$/Su', $qualifiedName)) {
if (!preg_match(InnerDocument::QNAME_PRODUCTION_REGEX, $qualifiedName)) {
throw new DOMException(DOMException::INVALID_CHARACTER);
}

63
lib/Document.php

@ -53,30 +53,59 @@ class Document extends Node {
}
public function createCDATASection(): CDATASection {
return $this->innerNode->getWrapperNode($this->innerNode->createCDATASection($data));
}
public function createComment(): Comment {
return $this->innerNode->getWrapperNode($this->innerNode->createComment($data));
}
public function createDocumentFragment(): DocumentFragment {
// 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;
return $this->innerNode->getWrapperNode($this->innerNode->createDocumentFragment());
}
public function createElement(string $localName): Element {
return $this->innerNode->getWrapperNode($this->innerNode->createElement($localName));
# 1. If localName does not match the Name production, then throw an
# "InvalidCharacterError" DOMException.
if (!preg_match(InnerDocument::NAME_PRODUCTION_REGEX, $localName)) {
throw new DOMException(DOMException::INVALID_CHARACTER);
}
# 2. If this is an HTML document, then set localName to localName in ASCII
# lowercase.
if ($this instanceof Document && !$this instanceof XMLElement) {
$localName = strtolower($localName);
}
# 3. Let is be null.
# 4. If options is a dictionary and options["is"] exists, then set is to it.
// DEVIATION: There's no scripting in this implementation
# 5. Let namespace be the HTML namespace, if this is an HTML document or this’s
# content type is "application/xhtml+xml"; otherwise null.
// PHP's DOM has numerous bugs when setting the HTML namespace. Externally,
// everything will show as HTML namespace, but internally will be null.
# 6. Return the result of creating an element given this, localName, namespace,
# null, is, and with the synchronous custom elements flag set.
try {
$element = $this->innerNode->createElementNS(null, $localName);
} catch (\DOMException $e) {
// The element name is invalid for XML
// Replace any offending characters with "UHHHHHH" where H are the
// uppercase hexadecimal digits of the character's code point
$element = $this->innerNode->createElementNS(null, $this->coerceName($localName));
}
return $this->innerNode->getWrapperNode($element);
}
public function createProcessingInstruction(string $target, string $data): ProcessingInstruction {
return $this->innerNode->getWrapperNode($this->innerNode->createProcessingInstruction($target, $data));
}
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;
return $this->innerNode->getWrapperNode($this->innerNode->createTextNode($data));
}
public function importNode(\DOMNode|Node $node, bool $deep = false): Node {

5
lib/DocumentFragment.php

@ -13,9 +13,4 @@ class DocumentFragment extends Node {
use ParentNode;
protected ?\WeakReference $host = null;
public function __construct() {
$this->innerNode = new \DOMDocumentFragment();
}
}

2
lib/DocumentType.php

@ -10,6 +10,8 @@ namespace MensBeam\HTML\DOM;
class DocumentType extends Node {
use ChildNode;
protected function __get_name(): string {
// Return an empty string if a space because this implementation gets around a
// PHP DOM limitation by substituting an empty string for a space.

2
lib/HTMLTemplateElement.php

@ -11,7 +11,7 @@ use MensBeam\HTML\DOM\InnerNode\Element as InnerElement,
MensBeam\HTML\DOM\InnerNode\Reflection;
class HTMLTemplateElement extends Element {
class HTMLTemplateElement extends HTMLElement {
protected DocumentFragment $_content;
// Templates can contain content in both light and shadow, so its content

22
lib/InnerNode/Document.php

@ -18,6 +18,9 @@ use MensBeam\HTML\Parser;
class Document extends \DOMDocument {
use MagicProperties;
public const NAME_PRODUCTION_REGEX = '/^[:A-Z_a-z\x{C0}-\x{D6}\x{D8}-\x{F6}\x{F8}-\x{2FF}\x{370}-\x{37D}\x{37F}-\x{1FFF}\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}][:A-Z_a-z\x{C0}-\x{D6}\x{D8}-\x{F6}\x{F8}-\x{2FF}\x{370}-\x{37D}\x{37F}-\x{1FFF}\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}-\.0-9\x{B7}\x{0300}-\x{036F}\x{203F}-\x{2040}]*$/Su';
public const QNAME_PRODUCTION_REGEX = '/^([A-Z_a-z\x{C0}-\x{D6}\x{D8}-\x{F6}\x{F8}-\x{2FF}\x{370}-\x{37D}\x{37F}-\x{1FFF}\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}][A-Z_a-z\x{C0}-\x{D6}\x{D8}-\x{F6}\x{F8}-\x{2FF}\x{370}-\x{37D}\x{37F}-\x{1FFF}\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}-\.0-9\x{B7}\x{0300}-\x{036F}\x{203F}-\x{2040}]*:)?[A-Z_a-z\x{C0}-\x{D6}\x{D8}-\x{F6}\x{F8}-\x{2FF}\x{370}-\x{37D}\x{37F}-\x{1FFF}\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}][A-Z_a-z\x{C0}-\x{D6}\x{D8}-\x{F6}\x{F8}-\x{2FF}\x{370}-\x{37D}\x{37F}-\x{1FFF}\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}-\.0-9\x{B7}\x{0300}-\x{036F}\x{203F}-\x{2040}]*$/Su';
protected NodeMap $nodeMap;
protected \WeakReference $_wrapperNode;
@ -70,7 +73,7 @@ class Document extends \DOMDocument {
} elseif ($node instanceof \DOMDocumentFragment) {
$className = 'DocumentFragment';
} elseif ($node instanceof \DOMDocumentType) {
$className = 'DOMDocumentType';
$className = 'DocumentType';
} elseif ($node instanceof \DOMElement) {
$namespace = $node->namespaceURI;
if ($namespace === null) {
@ -94,21 +97,8 @@ class Document extends \DOMDocument {
$className = 'XMLDocument';
}
// If the class is to be a CDATASection, DocumentFragment, or Text then the
// object needs to be created differently because they have public constructors,
// unlike other nodes.
if ($className === 'CDATASection' || $className === 'DocumentFragment' || $className === 'Text') {
$reflector = new \ReflectionClass(self::$parentNamespace . "\\$className");
$wrapperNode = $reflector->newInstanceWithoutConstructor();
$property = new \ReflectionProperty($wrapperNode, 'innerNode');
$property->setAccessible(true);
$property->setValue($wrapperNode, $node);
return $wrapperNode;
} else {
$wrapperNode = Reflection::createFromProtectedConstructor(self::$parentNamespace . "\\$className", $node);
}
$this->nodeMap->set($wrapperNode, $this);
$wrapperNode = Reflection::createFromProtectedConstructor(self::$parentNamespace . "\\$className", $node);
$this->nodeMap->set($wrapperNode, $node);
return $wrapperNode;
}
}

2
lib/InnerNode/NodeMap.php

@ -29,7 +29,7 @@ class NodeMap {
return true;
}
public function get(\DOMNode|WrapperNode $node): ?\DOMNode {
public function get(\DOMNode|WrapperNode $node): \DOMNode|WrapperNode|null {
$key = $this->key($node);
if ($key === false) {
return null;

41
lib/Node.php

@ -8,11 +8,12 @@
declare(strict_types=1);
namespace MensBeam\HTML\DOM;
use MensBeam\Framework\MagicProperties,
MensBeam\HTML\DOM\InnerNode\Reflection;
MensBeam\HTML\DOM\InnerNode\Reflection,
MensBeam\HTML\Parser\NameCoercion;
abstract class Node {
use MagicProperties;
use MagicProperties, NameCoercion;
public const ELEMENT_NODE = 1;
public const ATTRIBUTE_NODE = 2;
@ -99,8 +100,12 @@ abstract class Node {
protected function __get_firstChild(): ?Node {
// PHP's DOM does this correctly already.
if (!$value = $this->innerNode->firstChild) {
return null;
}
$doc = ($this instanceof Document) ? $this->innerNode : $this->innerNode->ownerDocument;
return $doc->getWrapperNode($this->innerNode->firstChild);
return $doc->getWrapperNode($value);
}
protected function __get_isConnected(): bool {
@ -112,17 +117,32 @@ abstract class Node {
protected function __get_lastChild(): ?Node {
// PHP's DOM does this correctly already.
return $this->innerNode->ownerDocument->getWrapperNode($this->innerNode->lastChild);
if (!$value = $this->innerNode->lastChild) {
return null;
}
$doc = ($this instanceof Document) ? $this->innerNode : $this->innerNode->ownerDocument;
return $doc->getWrapperNode($value);
}
protected function __get_previousSibling(): ?Node {
// PHP's DOM does this correctly already.
return $this->innerNode->ownerDocument->getWrapperNode($this->innerNode->previousSibling);
if (!$value = $this->innerNode->previousSibling) {
return null;
}
$doc = ($this instanceof Document) ? $this->innerNode : $this->innerNode->ownerDocument;
return $doc->getWrapperNode($value);
}
protected function __get_nextSibling(): ?Node {
// PHP's DOM does this correctly already.
return $this->innerNode->ownerDocument->getWrapperNode($this->innerNode->nextSibling);
if (!$value = $this->innerNode->nextSibling) {
return null;
}
$doc = ($this instanceof Document) ? $this->innerNode : $this->innerNode->ownerDocument;
return $doc->getWrapperNode($value);
}
protected function __get_nodeName(): string {
@ -132,7 +152,9 @@ abstract class Node {
# ↪ Element
# Its HTML-uppercased qualified name.
if ($this instanceof Element) {
return strtoupper($this->innerNode->nodeName);
$nodeName = $this->innerNode->nodeName;
// Uncoerce names if necessary
return strtoupper(!str_contains(needle: 'U', haystack: $nodeName) ? $nodeName : $this->uncoerceName($nodeName));
}
// PHP's DOM mostly does this correctly with the exception of Element, so let's
@ -209,7 +231,8 @@ abstract class Node {
return null;
}
return $parent->ownerDocument->getWrapperNode($parent);
$doc = ($this instanceof Document) ? $this->innerNode : $this->innerNode->ownerDocument;
return $doc->getWrapperNode($parent);
}
protected function __get_textContent(): string {
@ -333,7 +356,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

5
lib/Text.php

@ -16,11 +16,6 @@ class Text extends CharacterData {
}
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.

236
tests/cases/TestNode.php

@ -17,7 +17,30 @@ use MensBeam\HTML\Parser;
/** @covers \MensBeam\HTML\DOM\Document */
class TestNode extends \PHPUnit\Framework\TestCase {
/** @covers \MensBeam\HTML\DOM\Node::__get_childNodes */
/**
* @covers \MensBeam\HTML\DOM\Node::__get_childNodes
*
* @covers \MensBeam\HTML\DOM\Document::__construct
* @covers \MensBeam\HTML\DOM\Document::createElement
* @covers \MensBeam\HTML\DOM\Document::createTextNode
* @covers \MensBeam\HTML\DOM\DOMImplementation::__construct
* @covers \MensBeam\HTML\DOM\Node::appendChild
* @covers \MensBeam\HTML\DOM\Node::preInsertionValidity
* @covers \MensBeam\HTML\DOM\NodeList::__construct
* @covers \MensBeam\HTML\DOM\NodeList::count
* @covers \MensBeam\HTML\DOM\NodeList::__get_length
* @covers \MensBeam\HTML\DOM\NodeList::item
* @covers \MensBeam\HTML\DOM\NodeList::offsetGet
* @covers \MensBeam\HTML\DOM\InnerNode\Document::__construct
* @covers \MensBeam\HTML\DOM\InnerNode\Document::getWrapperNode
* @covers \MensBeam\HTML\DOM\InnerNode\Document::__get_wrapperNode
* @covers \MensBeam\HTML\DOM\InnerNode\Reflection::createFromProtectedConstructor
* @covers \MensBeam\HTML\DOM\InnerNode\Reflection::getProtectedProperty
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::get
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::has
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::key
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::set
*/
public function testProperty_childNodes() {
$d = new Document();
$d->appendChild($d->createElement('html'));
@ -34,7 +57,216 @@ class TestNode extends \PHPUnit\Framework\TestCase {
// Node::childNodes on Text
$childNodes = $d->body->lastChild->childNodes;
$this->assertEquals(0, $childNodes->length);
// Try it again to test caching
// Try it again to test caching in coverage; no reason to assert
$childNodes = $d->body->lastChild->childNodes;
}
/**
* @covers \MensBeam\HTML\DOM\Node::__get_firstChild
*
* @covers \MensBeam\HTML\DOM\Document::__construct
* @covers \MensBeam\HTML\DOM\Document::createElement
* @covers \MensBeam\HTML\DOM\DOMImplementation::__construct
* @covers \MensBeam\HTML\DOM\Node::appendChild
* @covers \MensBeam\HTML\DOM\Node::preInsertionValidity
* @covers \MensBeam\HTML\DOM\InnerNode\Document::__construct
* @covers \MensBeam\HTML\DOM\InnerNode\Document::getWrapperNode
* @covers \MensBeam\HTML\DOM\InnerNode\Document::__get_wrapperNode
* @covers \MensBeam\HTML\DOM\InnerNode\Reflection::createFromProtectedConstructor
* @covers \MensBeam\HTML\DOM\InnerNode\Reflection::getProtectedProperty
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::get
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::has
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::key
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::set
*/
public function testProperty_firstChild() {
$d = new Document();
$d->appendChild($d->createElement('html'));
$de = $d->documentElement;
$de->appendChild($d->createElement('body'));
// Node::firstChild on Document
$this->assertSame($de, $d->firstChild);
// Node::firstChild on document element
$this->assertSame($d->body, $de->firstChild);
// Node::firstChild on empty node
$this->assertNull($d->body->firstChild);
}
/**
* @covers \MensBeam\HTML\DOM\Node::__get_isConnected
*
* @covers \MensBeam\HTML\DOM\ChildNode::moonwalk
* @covers \MensBeam\HTML\DOM\Document::__construct
* @covers \MensBeam\HTML\DOM\Document::createElement
* @covers \MensBeam\HTML\DOM\DOMImplementation::__construct
* @covers \MensBeam\HTML\DOM\Node::appendChild
* @covers \MensBeam\HTML\DOM\Node::getRootNode
* @covers \MensBeam\HTML\DOM\Node::preInsertionValidity
* @covers \MensBeam\HTML\DOM\InnerNode\Document::__construct
* @covers \MensBeam\HTML\DOM\InnerNode\Document::getWrapperNode
* @covers \MensBeam\HTML\DOM\InnerNode\Document::__get_wrapperNode
* @covers \MensBeam\HTML\DOM\InnerNode\Reflection::createFromProtectedConstructor
* @covers \MensBeam\HTML\DOM\InnerNode\Reflection::getProtectedProperty
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::get
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::has
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::key
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::set
*/
public function testProperty_isConnected() {
$d = new Document();
$d->appendChild($d->createElement('html'));
$b = $d->createElement('body');
$this->assertTrue($d->documentElement->isConnected);
$this->assertFalse($b->isConnected);
}
/**
* @covers \MensBeam\HTML\DOM\Node::__get_lastChild
*
* @covers \MensBeam\HTML\DOM\Document::__construct
* @covers \MensBeam\HTML\DOM\Document::createElement
* @covers \MensBeam\HTML\DOM\Document::createTextNode
* @covers \MensBeam\HTML\DOM\DOMImplementation::__construct
* @covers \MensBeam\HTML\DOM\Node::appendChild
* @covers \MensBeam\HTML\DOM\Node::preInsertionValidity
* @covers \MensBeam\HTML\DOM\InnerNode\Document::__construct
* @covers \MensBeam\HTML\DOM\InnerNode\Document::getWrapperNode
* @covers \MensBeam\HTML\DOM\InnerNode\Document::__get_wrapperNode
* @covers \MensBeam\HTML\DOM\InnerNode\Reflection::createFromProtectedConstructor
* @covers \MensBeam\HTML\DOM\InnerNode\Reflection::getProtectedProperty
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::get
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::has
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::key
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::set
*/
public function testProperty_lastChild() {
$d = new Document();
$d->appendChild($d->createElement('html'));
$de = $d->documentElement;
$de->appendChild($d->createElement('body'));
$b = $d->body;
$b->appendChild($d->createElement('span'));
$o = $b->appendChild($d->createTextNode('ook'));
// Node::lastChild on Document
$this->assertSame($de, $d->lastChild);
// Node::lastChild on document element
$this->assertSame($d->body, $de->lastChild);
// Node::lastChild on element with multiple children
$this->assertSame($o, $b->lastChild);
// Node::lastChild on text node
$this->assertNull($o->lastChild);
}
/**
* @covers \MensBeam\HTML\DOM\Node::__get_previousSibling
*
* @covers \MensBeam\HTML\DOM\Document::__construct
* @covers \MensBeam\HTML\DOM\Document::createElement
* @covers \MensBeam\HTML\DOM\Document::createTextNode
* @covers \MensBeam\HTML\DOM\DOMImplementation::__construct
* @covers \MensBeam\HTML\DOM\DOMImplementation::createDocumentType
* @covers \MensBeam\HTML\DOM\Node::appendChild
* @covers \MensBeam\HTML\DOM\Node::preInsertionValidity
* @covers \MensBeam\HTML\DOM\InnerNode\Document::__construct
* @covers \MensBeam\HTML\DOM\InnerNode\Document::getWrapperNode
* @covers \MensBeam\HTML\DOM\InnerNode\Document::__get_wrapperNode
* @covers \MensBeam\HTML\DOM\InnerNode\Reflection::createFromProtectedConstructor
* @covers \MensBeam\HTML\DOM\InnerNode\Reflection::getProtectedProperty
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::get
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::has
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::key
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::set
*/
public function testProperty_previousSibling() {
$d = new Document();
$dt = $d->appendChild($d->implementation->createDocumentType('html', '', ''));
$d->appendChild($d->createElement('html'));
$de = $d->documentElement;
$de->appendChild($d->createElement('body'));
$b = $d->body;
$s = $b->appendChild($d->createElement('span'));
$o = $b->appendChild($d->createTextNode('ook'));
// Node::previousSibling on document element
$this->assertSame($dt, $de->previousSibling);
// Node::previousSibling on element with multiple children
$this->assertSame($s, $o->previousSibling);
// Node::previousSibling on first child of body
$this->assertNull($b->firstChild->previousSibling);
}
/**
* @covers \MensBeam\HTML\DOM\Node::__get_nextSibling
*
* @covers \MensBeam\HTML\DOM\Document::__construct
* @covers \MensBeam\HTML\DOM\Document::createElement
* @covers \MensBeam\HTML\DOM\Document::createTextNode
* @covers \MensBeam\HTML\DOM\DOMImplementation::__construct
* @covers \MensBeam\HTML\DOM\DOMImplementation::createDocumentType
* @covers \MensBeam\HTML\DOM\Node::appendChild
* @covers \MensBeam\HTML\DOM\Node::preInsertionValidity
* @covers \MensBeam\HTML\DOM\InnerNode\Document::__construct
* @covers \MensBeam\HTML\DOM\InnerNode\Document::getWrapperNode
* @covers \MensBeam\HTML\DOM\InnerNode\Document::__get_wrapperNode
* @covers \MensBeam\HTML\DOM\InnerNode\Reflection::createFromProtectedConstructor
* @covers \MensBeam\HTML\DOM\InnerNode\Reflection::getProtectedProperty
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::get
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::has
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::key
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::set
*/
public function testProperty_nextSibling() {
$d = new Document();
$dt = $d->appendChild($d->implementation->createDocumentType('html', '', ''));
$d->appendChild($d->createElement('html'));
$de = $d->documentElement;
$de->appendChild($d->createElement('body'));
$b = $d->body;
$s = $b->appendChild($d->createElement('span'));
$o = $b->appendChild($d->createTextNode('ook'));
// Node::nextSibling on doctype
$this->assertSame($de, $dt->nextSibling);
// Node::nextSibling on element with multiple children
$this->assertSame($o, $s->nextSibling);
// Node::nextSibling on last child of body
$this->assertNull($b->lastChild->nextSibling);
}
/**
* @covers \MensBeam\HTML\DOM\Node::__get_nodeName
*
* @covers \MensBeam\HTML\DOM\Document::__construct
* @covers \MensBeam\HTML\DOM\Document::createElement
* @covers \MensBeam\HTML\DOM\Document::createTextNode
* @covers \MensBeam\HTML\DOM\DOMImplementation::__construct
* @covers \MensBeam\HTML\DOM\DOMImplementation::createDocumentType
* @covers \MensBeam\HTML\DOM\InnerNode\Document::__construct
* @covers \MensBeam\HTML\DOM\InnerNode\Document::getWrapperNode
* @covers \MensBeam\HTML\DOM\InnerNode\Document::__get_wrapperNode
* @covers \MensBeam\HTML\DOM\InnerNode\Reflection::createFromProtectedConstructor
* @covers \MensBeam\HTML\DOM\InnerNode\Reflection::getProtectedProperty
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::get
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::has
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::key
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::set
*/
public function testProperty_nodeName() {
$d = new Document();
$dt = $d->appendChild($d->implementation->createDocumentType('html', '', ''));
// Node::nodeName on element
$this->assertSame('HTML', $d->createElement('html')->nodeName);
// Node::nodeName on element with coerced name
$this->assertSame('POOP💩', $d->createElement('poop💩')->nodeName);
}
}
Loading…
Cancel
Save