Browse Source

Started Node::cloneNode tests, updated README with limitations

wrapper-classes
Dustin Wilson 3 years ago
parent
commit
1ffe918af1
  1. 20
      README.md
  2. 8
      lib/DOMImplementation.php
  3. 15
      lib/Document.php
  4. 2
      lib/HTMLTemplateElement.php
  5. 2
      lib/InnerNode/Document.php
  6. 11
      lib/InnerNode/Reflection.php
  7. 17
      lib/Node.php
  8. 63
      tests/cases/TestNode.php

20
README.md

@ -56,11 +56,15 @@ Coming soon
$d->loadDOM(new \DOMDocument());
```
## Limitations ##
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. 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.
## Differences from Specification ##
The primary aim of this library is accuracy. However, due either to limitations imposed by PHP's DOM or by assumptions made by the specification that aren't applicable to a PHP library some changes have needed to be made. These are as follows:
1. Any mention of scripting or anything necessary because of scripting (such as the `ElementCreationOptions` options dictionary on `Document::createElement`) will not be implemented.
2. The specification is written entirely with browsers in mind and aren't concerned with the DOM's being used outside of the browser. In browser there is always a document created by parsing serialized markup, and the DOM spec always assumes such. This is impossible in the way this PHP library is intended to be used. The default when creating a new `Document` is to set its content type to "application/xml". This isn't ideal when creating an HTML document entirely through the DOM, so this implementation will instead default to "text/html" unless using `XMLDocument`.
3. Per the specification an actual HTML document cannot be created outside of the parser itself unless created via `DOMImplementation::createHTMLDocument`. Also, per the spec `DOMImplementation` cannot be instantiated via its constructor. This would require in this library's use case first creating a document then creating an HTML document via its implementation. This is impractical, so in this library (like PHP DOM itself) a `DOMImplementation` can be instantiated independent of a document.
4. The specification shows `Document` as being able to be instantated through its constructor and shows `XMLDocument` as inheriting from `Document`. In browsers `XMLDocument` cannot be instantiated through its constructor. We will follow the specification here and allow it.
5. CDATA section nodes, text nodes, and document fragments per the specification can be instantiated 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.
6. 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.
7. All of the `Range` APIs will also not be implemented due to the sheer complexity of creating them in userland and how it adds undue difficulty to node manipulation in the "core" DOM. Numerous operations reference in excrutiating detail what to do with Ranges when manipulating nodes and would have to be added here to be compliant or mostly so.
8. Aside from `HTMLElement`, `HTMLTemplateElement`, `MathMLElement`, and `SVGElement` none of the specific derived element classes (such as `HTMLAnchorElement` or `SVGSVGElement`) are implemented. The focus on this library will be on the core DOM before moving onto those. They may or may not be implemented in the future.

8
lib/DOMImplementation.php

@ -18,8 +18,8 @@ class DOMImplementation {
protected \WeakReference $document;
protected function __construct(Document $document) {
$this->document = \WeakReference::create($document);
public function __construct(?Document $document = null) {
$this->document = \WeakReference::create($document ?? new Document());
}
@ -70,7 +70,7 @@ class DOMImplementation {
$contentType = 'application/xml';
}
Reflection::setProtectedProperty($document, '_contentType', $contentType);
Reflection::setProtectedProperties($document, ['_contentType' => $contentType ]);
# 8. Return document.
return $document;
@ -85,7 +85,7 @@ class DOMImplementation {
if (!preg_match(InnerDocument::QNAME_PRODUCTION_REGEX, $qualifiedName)) {
throw new DOMException(DOMException::INVALID_CHARACTER);
}
# 2. Return a new doctype, with qualifiedName as its name, publicId as its
# public ID, and systemId as its system ID, and with its node document set to
# the associated document of this.

15
lib/Document.php

@ -17,6 +17,7 @@ use MensBeam\HTML\Parser;
class Document extends Node {
use DocumentOrElement, ParentNode;
protected string $_characterSet = 'UTF-8';
protected string $_compatMode = 'CSS1Compat';
protected string $_contentType = 'text/html';
protected DOMImplementation $_implementation;
@ -36,6 +37,14 @@ class Document extends Node {
}, true)->current();
}
protected function __get_charset(): string {
return $this->_characterSet;
}
protected function __get_characterSet(): string {
return $this->_characterSet;
}
protected function __get_compatMode(): string {
return $this->_compatMode;
}
@ -56,6 +65,10 @@ class Document extends Node {
return $this->_implementation;
}
protected function __get_inputEncoding(): string {
return $this->_characterSet;
}
protected function __get_URL(): string {
return $this->_URL;
}
@ -63,7 +76,7 @@ class Document extends Node {
public function __construct() {
parent::__construct(new InnerDocument($this));
$this->_implementation = Reflection::createFromProtectedConstructor(__NAMESPACE__ . '\\DOMImplementation', $this);
$this->_implementation = new DOMImplementation($this);
}

2
lib/HTMLTemplateElement.php

@ -25,6 +25,6 @@ class HTMLTemplateElement extends HTMLElement {
parent::__construct($element);
$this->_content = $this->ownerDocument->createDocumentFragment();
Reflection::setProtectedProperty($this->_content, 'host', \WeakReference::create($this));
Reflection::setProtectedProperties($this->_content, [ 'host' => \WeakReference::create($this) ]);
}
}

2
lib/InnerNode/Document.php

@ -101,7 +101,7 @@ class Document extends \DOMDocument {
// We need to work around a PHP DOM bug where doctype nodes aren't associated
// with a document until they're appended.
if ($className === 'DocumentType') {
Reflection::setProtectedProperty($wrapperNode, '_ownerDocument', $this->_wrapperNode);
Reflection::setProtectedProperties($wrapperNode, [ '_ownerDocument' => $this->_wrapperNode ]);
}
$this->nodeMap->set($wrapperNode, $node);

11
lib/InnerNode/Reflection.php

@ -26,10 +26,13 @@ class Reflection {
return $property->getValue($instance);
}
public static function setProtectedProperty(mixed $instance, string $propertyName, mixed $value): void {
public static function setProtectedProperties(mixed $instance, array $properties): void {
$reflector = new \ReflectionClass($instance::class);
$property = new \ReflectionProperty($instance, $propertyName);
$property->setAccessible(true);
$property->setValue($instance, $value);
foreach ($properties as $propertyName => $value) {
$property = new \ReflectionProperty($instance, $propertyName);
$property->setAccessible(true);
$property->setValue($instance, $value);
}
}
}

17
lib/Node.php

@ -324,7 +324,22 @@ abstract class Node {
public function cloneNode(?bool $deep = false): Node {
// PHP's DOM does this correctly already.
$newInner = $this->innerNode->cloneNode($deep);
return $newInner->ownerDocument->getWrapperNode($newInner);
// Documents have some userland properties to transfer
if ($this instanceof Document) {
$newDoc = $this->innerNode->getWrapperNode($newInner);
if ($this->characterSet !== 'UTF-8' || $this->compatMode !== 'CSS1Compat' || $this->contentType !== 'text/html' || $this->URL !== '') {
Reflection::setProtectedProperties($newDoc, [
'_characterSet' => $this->characterSet,
'_compatMode' => $this->compatMode,
'_contentType' => $this->contentType,
'_URL' => $this->URL
]);
}
return $newDoc;
}
return $this->innerNode->ownerDocument->getWrapperNode($newInner);
}
public function compareDocumentPosition(Node $other): int {

63
tests/cases/TestNode.php

@ -18,6 +18,51 @@ use MensBeam\HTML\Parser;
/** @covers \MensBeam\HTML\DOM\Document */
class TestNode extends \PHPUnit\Framework\TestCase {
/**
* @covers \MensBeam\HTML\DOM\Node::cloneNode
*
* @covers \MensBeam\HTML\DOM\Attr::__construct
* @covers \MensBeam\HTML\DOM\Comment::__construct
* @covers \MensBeam\HTML\DOM\CDATASection::__construct
* @covers \MensBeam\HTML\DOM\Document::__construct
* @covers \MensBeam\HTML\DOM\Document::createAttribute
* @covers \MensBeam\HTML\DOM\Document::createAttributeNS
* @covers \MensBeam\HTML\DOM\Document::createComment
* @covers \MensBeam\HTML\DOM\Document::createDocumentFragment
* @covers \MensBeam\HTML\DOM\Document::createElement
* @covers \MensBeam\HTML\DOM\Document::createElementNS
* @covers \MensBeam\HTML\DOM\Document::createProcessingInstruction
* @covers \MensBeam\HTML\DOM\Document::createTextNode
* @covers \MensBeam\HTML\DOM\DocumentOrElement::validateAndExtract
* @covers \MensBeam\HTML\DOM\DocumentFragment::__construct
* @covers \MensBeam\HTML\DOM\DocumentType::__construct
* @covers \MensBeam\HTML\DOM\DocumentType::__get_ownerDocument
* @covers \MensBeam\HTML\DOM\DOMImplementation::__construct
* @covers \MensBeam\HTML\DOM\DOMImplementation::createDocumentType
* @covers \MensBeam\HTML\DOM\Element::__construct
* @covers \MensBeam\HTML\DOM\Node::__construct
* @covers \MensBeam\HTML\DOM\ProcessingInstruction::__construct
* @covers \MensBeam\HTML\DOM\Text::__construct
* @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\Reflection::setProtectedProperties
* @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 testMethod_cloneNode() {
$d = new Document();
// Node::cloneNode on Document
$d2 = $d->cloneNode(true);
$this->assertSame(Document::class, $d2::class);
}
/**
* @covers \MensBeam\HTML\DOM\Node::__get_childNodes
*
@ -194,7 +239,7 @@ class TestNode extends \PHPUnit\Framework\TestCase {
* @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\Reflection::setProtectedProperty
* @covers \MensBeam\HTML\DOM\InnerNode\Reflection::setProtectedProperties
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::get
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::has
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::key
@ -238,7 +283,7 @@ class TestNode extends \PHPUnit\Framework\TestCase {
* @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\Reflection::setProtectedProperty
* @covers \MensBeam\HTML\DOM\InnerNode\Reflection::setProtectedProperties
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::get
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::has
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::key
@ -293,7 +338,7 @@ class TestNode extends \PHPUnit\Framework\TestCase {
* @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\Reflection::setProtectedProperty
* @covers \MensBeam\HTML\DOM\InnerNode\Reflection::setProtectedProperties
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::get
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::has
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::key
@ -375,7 +420,7 @@ class TestNode extends \PHPUnit\Framework\TestCase {
* @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\Reflection::setProtectedProperty
* @covers \MensBeam\HTML\DOM\InnerNode\Reflection::setProtectedProperties
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::get
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::has
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::key
@ -436,7 +481,7 @@ class TestNode extends \PHPUnit\Framework\TestCase {
* @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\Reflection::setProtectedProperty
* @covers \MensBeam\HTML\DOM\InnerNode\Reflection::setProtectedProperties
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::get
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::has
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::key
@ -525,7 +570,7 @@ class TestNode extends \PHPUnit\Framework\TestCase {
* @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\Reflection::setProtectedProperty
* @covers \MensBeam\HTML\DOM\InnerNode\Reflection::setProtectedProperties
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::get
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::has
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::key
@ -587,7 +632,7 @@ class TestNode extends \PHPUnit\Framework\TestCase {
* @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\Reflection::setProtectedProperty
* @covers \MensBeam\HTML\DOM\InnerNode\Reflection::setProtectedProperties
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::get
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::has
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::key
@ -664,7 +709,7 @@ class TestNode extends \PHPUnit\Framework\TestCase {
* @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\Reflection::setProtectedProperty
* @covers \MensBeam\HTML\DOM\InnerNode\Reflection::setProtectedProperties
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::get
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::has
* @covers \MensBeam\HTML\DOM\InnerNode\NodeMap::key
@ -718,11 +763,13 @@ class TestNode extends \PHPUnit\Framework\TestCase {
$this->assertSame('ook', $frag->textContent);
$frag->textContent = 'eek';
$this->assertSame('eek', $frag->textContent);
$this->assertEquals(1, $frag->childNodes->length);
// Node::textContent on element
$this->assertSame('ook', $body->textContent);
$body->textContent = 'eek';
$this->assertSame('eek', $body->textContent);
$this->assertEquals(1, $body->childNodes->length);
// Node::textContent on processing instruction
$this->assertSame('eek', $pi->textContent);

Loading…
Cancel
Save