Browse Source

Added ChildNode::before, started on a bit of documentation

wrapper-classes
Dustin Wilson 3 years ago
parent
commit
2e07af4edb
  1. 18
      README.md
  2. 10
      lib/Attr.php
  3. 8
      lib/Comment.php
  4. 8
      lib/Document.php
  5. 8
      lib/DocumentFragment.php
  6. 7
      lib/Element.php
  7. 23
      lib/Node.php
  8. 8
      lib/ProcessingInstruction.php
  9. 8
      lib/Text.php
  10. 8
      lib/traits/BaseNode.php
  11. 54
      lib/traits/ChildNode.php
  12. 2
      lib/traits/LeafNode.php
  13. 2
      lib/traits/ParentNode.php
  14. 8
      tests/cases/TestBaseNode.php
  15. 51
      tests/cases/TestChildNode.php
  16. 2
      tests/cases/TestDocument.php
  17. 2
      tests/cases/TestParentNode.php
  18. 2
      tests/phpunit.dist.xml

18
README.md

@ -1,3 +1,19 @@
[a]: https://dom.spec.whatwg.org/#htmlcollection
[b]: https://webidl.spec.whatwg.org/#idl-sequence
# HTML DOM #
Modern DOM library written in PHP for HTML documents.
Modern DOM library written in PHP for HTML documents.
## Usage ##
Coming soon
## 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 limtations. 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 a thing anymore either. 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 PHP's DOM would normally and array for anything that cannot be hacked to use `DOMNodeList` to keep things consistent.

10
lib/Attr.php

@ -10,5 +10,13 @@ namespace MensBeam\HTML\DOM;
class Attr extends \DOMAttr {
use Node;
use BaseNode;
// Should be in Node, but traits cannot have contants
public const DOCUMENT_POSITION_DISCONNECTED = 0x01;
public const DOCUMENT_POSITION_PRECEDING = 0x02;
public const DOCUMENT_POSITION_FOLLOWING = 0x04;
public const DOCUMENT_POSITION_CONTAINS = 0x08;
public const DOCUMENT_POSITION_CONTAINED_BY = 0x10;
public const DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = 0x20;
}

8
lib/Comment.php

@ -10,4 +10,12 @@ namespace MensBeam\HTML\DOM;
class Comment extends \DOMComment {
use ChildNode, LeafNode, Moonwalk, ToString;
// Should be in Node, but traits cannot have contants
public const DOCUMENT_POSITION_DISCONNECTED = 0x01;
public const DOCUMENT_POSITION_PRECEDING = 0x02;
public const DOCUMENT_POSITION_FOLLOWING = 0x04;
public const DOCUMENT_POSITION_CONTAINS = 0x08;
public const DOCUMENT_POSITION_CONTAINED_BY = 0x10;
public const DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = 0x20;
}

8
lib/Document.php

@ -18,6 +18,14 @@ use MensBeam\HTML\Parser\{
class Document extends \DOMDocument {
use DocumentOrElement, MagicProperties, ParentNode, Walk;
// Should be in Node, but traits cannot have contants
public const DOCUMENT_POSITION_DISCONNECTED = 0x01;
public const DOCUMENT_POSITION_PRECEDING = 0x02;
public const DOCUMENT_POSITION_FOLLOWING = 0x04;
public const DOCUMENT_POSITION_CONTAINS = 0x08;
public const DOCUMENT_POSITION_CONTAINED_BY = 0x10;
public const DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = 0x20;
protected ?Element $_body = null;
/** Nonstandard */
protected ?string $_documentEncoding = null;

8
lib/DocumentFragment.php

@ -13,6 +13,14 @@ use MensBeam\Framework\MagicProperties;
class DocumentFragment extends \DOMDocumentFragment {
use MagicProperties, ParentNode, Walk;
// Should be in Node, but traits cannot have contants
public const DOCUMENT_POSITION_DISCONNECTED = 0x01;
public const DOCUMENT_POSITION_PRECEDING = 0x02;
public const DOCUMENT_POSITION_FOLLOWING = 0x04;
public const DOCUMENT_POSITION_CONTAINS = 0x08;
public const DOCUMENT_POSITION_CONTAINED_BY = 0x10;
public const DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = 0x20;
protected ?\WeakReference $_host = null;
protected function __get_host(): ?HTMLTemplateElement {

7
lib/Element.php

@ -14,6 +14,13 @@ use MensBeam\HTML\Parser;
class Element extends \DOMElement {
use ChildNode, DocumentOrElement, MagicProperties, Moonwalk, ParentNode, ToString, Walk;
// Should be in Node, but traits cannot have contants
public const DOCUMENT_POSITION_DISCONNECTED = 0x01;
public const DOCUMENT_POSITION_PRECEDING = 0x02;
public const DOCUMENT_POSITION_FOLLOWING = 0x04;
public const DOCUMENT_POSITION_CONTAINS = 0x08;
public const DOCUMENT_POSITION_CONTAINED_BY = 0x10;
public const DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = 0x20;
protected function __get_classList(): TokenList {
return new TokenList($this, 'class');

23
lib/Node.php

@ -0,0 +1,23 @@
<?php
/**
* @license MIT
* Copyright 2017, Dustin Wilson, J. King et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\HTML\DOM;
/**
* Exists not because other elements can inherit from it but so constants either
* defined below or inherited from \DOMNode may be accessed from it as expected
* in code.
*/
class Node extends \DOMNode {
public const DOCUMENT_POSITION_DISCONNECTED = 0x01;
public const DOCUMENT_POSITION_PRECEDING = 0x02;
public const DOCUMENT_POSITION_FOLLOWING = 0x04;
public const DOCUMENT_POSITION_CONTAINS = 0x08;
public const DOCUMENT_POSITION_CONTAINED_BY = 0x10;
public const DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = 0x20;
}

8
lib/ProcessingInstruction.php

@ -10,4 +10,12 @@ namespace MensBeam\HTML\DOM;
class ProcessingInstruction extends \DOMProcessingInstruction {
use ChildNode, LeafNode, Moonwalk, ToString;
// Should be in Node, but traits cannot have contants
public const DOCUMENT_POSITION_DISCONNECTED = 0x01;
public const DOCUMENT_POSITION_PRECEDING = 0x02;
public const DOCUMENT_POSITION_FOLLOWING = 0x04;
public const DOCUMENT_POSITION_CONTAINS = 0x08;
public const DOCUMENT_POSITION_CONTAINED_BY = 0x10;
public const DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = 0x20;
}

8
lib/Text.php

@ -10,4 +10,12 @@ namespace MensBeam\HTML\DOM;
class Text extends \DOMText {
use ChildNode, LeafNode, Moonwalk, ToString;
// Should be in Node, but traits cannot have contants
public const DOCUMENT_POSITION_DISCONNECTED = 0x01;
public const DOCUMENT_POSITION_PRECEDING = 0x02;
public const DOCUMENT_POSITION_FOLLOWING = 0x04;
public const DOCUMENT_POSITION_CONTAINS = 0x08;
public const DOCUMENT_POSITION_CONTAINED_BY = 0x10;
public const DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = 0x20;
}

8
lib/traits/Node.php → lib/traits/BaseNode.php

@ -9,9 +9,11 @@ declare(strict_types=1);
namespace MensBeam\HTML\DOM;
// Extensions to PHP's DOM cannot inherit from an extended Node parent, so a
// trait is the next best thing...
trait Node {
/**
* Not in standard. Exists because extensions to PHP DOM cannot inherit from a
* \DOMNode descendant, so a trait is the next best thing.
*/
trait BaseNode {
// Disable C14N
public function C14N($exclusive = null, $with_comments = null, ?array $xpath = null, ?array $ns_prefixes = null): bool {
throw new DOMException(DOMException::NOT_SUPPORTED, __CLASS__ . ' is meant for XML and buggy; use Document::saveHTML or cast to a string');

54
lib/traits/ChildNode.php

@ -12,8 +12,10 @@ namespace MensBeam\HTML\DOM;
# 4.2.8. Mixin ChildNode
trait ChildNode {
public function after(...$nodes): void {
// After exists in PHP DOM, but it can insert incorrect nodes because of PHP
// DOM's incorrect (for HTML) pre-insertion validation.
// PHP's declaration for \DOMCharacterData::after doesn't include the
// DOMNode|string typing for the nodes that it should, so type checking will
// \DOMNode|string typing for the nodes that it should, so type checking will
// need to be done manually.
foreach ($nodes as $node) {
if (!$node instanceof \DOMNode && !is_string($node)) {
@ -57,4 +59,54 @@ trait ChildNode {
# 5. Pre-insert node into parent before viableNextSibling.
$parent->insertBefore($node, $viableNextSibling);
}
public function before(...$nodes): void {
// Before exists in PHP DOM, but it can insert incorrect nodes because of PHP
// DOM's incorrect (for HTML) pre-insertion validation.
// PHP's declaration for \DOMCharacterData::after doesn't include the
// \DOMNode|string typing for the nodes that it should, so type checking will
// need to be done manually.
foreach ($nodes as $node) {
if (!$node instanceof \DOMNode && !is_string($node)) {
$type = gettype($node);
if ($type === 'object') {
$type = get_class($node);
}
throw new Exception(Exception::ARGUMENT_TYPE_ERROR, 1, 'nodes', '\DOMNode|string', $type);
}
}
# The before(nodes) method steps are:
#
# 1. Let parent be this’s parent.
$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 = $this;
$viablePreviousSibling = null;
while ($n = $n->previousSibling) {
foreach ($nodes as $nodeOrString) {
if ($nodeOrString instanceof \DOMNode && $nodeOrString->isSameNode($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 : $viablePreviousSibling->nextSibling;
# 6. Pre-insert node into parent before viablePreviousSibling.
$parent->insertBefore($node, $viablePreviousSibling);
}
}

2
lib/traits/LeafNode.php

@ -13,7 +13,7 @@ namespace MensBeam\HTML\DOM;
* the insertion methods disabled.
*/
trait LeafNode {
use Node;
use BaseNode;
public function appendChild($node) {
throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR);

2
lib/traits/ParentNode.php

@ -11,7 +11,7 @@ namespace MensBeam\HTML\DOM;
# 4.2.6. Mixin ParentNode
trait ParentNode {
use Node;
use BaseNode;
protected function __get_children(): \DOMNodeList {

8
tests/cases/TestNode.php → tests/cases/TestBaseNode.php

@ -16,7 +16,7 @@ use MensBeam\HTML\DOM\{
/** @covers \MensBeam\HTML\DOM\Node */
class TestNode extends \PHPUnit\Framework\TestCase {
class TestBaseNode extends \PHPUnit\Framework\TestCase {
public function provideDisabledMethods(): iterable {
return [
[ function() {
@ -32,8 +32,8 @@ class TestNode extends \PHPUnit\Framework\TestCase {
/**
* @dataProvider provideDisabledMethods
* @covers \MensBeam\HTML\DOM\Node::C14N
* @covers \MensBeam\HTML\DOM\Node::C14NFile
* @covers \MensBeam\HTML\DOM\BaseNode::C14N
* @covers \MensBeam\HTML\DOM\BaseNode::C14NFile
*/
public function testDisabledMethods(\Closure $closure): void {
$this->expectException(DOMException::class);
@ -42,7 +42,7 @@ class TestNode extends \PHPUnit\Framework\TestCase {
}
/** @covers \MensBeam\HTML\DOM\Node::getRootNode */
/** @covers \MensBeam\HTML\DOM\BaseNode::getRootNode */
public function testGetRootNode(): void {
$d = new Document();
$t = $d->createElement('template');

51
tests/cases/TestChildNode.php

@ -18,9 +18,10 @@ use MensBeam\HTML\DOM\{
class TestChildNode extends \PHPUnit\Framework\TestCase {
/**
* @covers \MensBeam\HTML\DOM\ChildNode::after
* @covers \MensBeam\HTML\DOM\Node::convertNodesToNode
* @covers \MensBeam\HTML\DOM\ChildNode::before
* @covers \MensBeam\HTML\DOM\BaseNode::convertNodesToNode
*/
public function testAfter(): void {
public function testAfterBefore(): void {
$d = new Document();
$d->appendChild($d->createElement('html'));
$d->documentElement->appendChild($d->createElement('body'));
@ -32,33 +33,55 @@ class TestChildNode extends \PHPUnit\Framework\TestCase {
$div->after($d->createElement('span'), $o, 'eek');
$this->assertSame('<body><div></div><span></span>ookeek<div></div></body>', (string)$d->body);
$div->after($o);
$this->assertSame('<body><div></div>ook<span></span>eek<div></div></body>', (string)$d->body);
// On node with no parent
$c = $d->createComment('ook');
$this->assertNull($c->after($d->createTextNode('ook')));
// On node with parent
$br = $d->body->insertBefore($d->createElement('br'), $div);
$div->before($d->createElement('span'), $o, 'eek', $br);
$this->assertSame('<body><span></span>ookeek<br><div></div><span></span>eek<div></div></body>', (string)$d->body);
$div->before($o);
$this->assertSame('<body><span></span>eek<br>ook<div></div><span></span>eek<div></div></body>', (string)$d->body);
// On node with no parent
$c = $d->createComment('ook');
$this->assertNull($c->before($d->createTextNode('ook')));
}
public function provideAfterFailure(): array {
public function provideAfterBeforeFailures(): array {
$d = new Document();
$d->appendChild($d->createElement('html'));
$d->documentElement->appendChild($d->createElement('body'));
$div = $d->body->appendChild($d->createElement('div'));
return [
[ false ],
[ new \DateTime() ],
[ function() use($div) {
$div->after(false);
} ],
[ function() use($div) {
$div->before(false);
} ],
[ function() use($div) {
$div->after(new \DateTime);
} ],
[ function() use($div) {
$div->before(new \DateTime);
} ],
];
}
/**
* @dataProvider provideAfterFailure
* @dataProvider provideAfterBeforeFailures
* @covers \MensBeam\HTML\DOM\ChildNode::after
* @covers \MensBeam\HTML\DOM\ChildNode::before
*/
public function testAfterFailure($object): void {
public function testAfterBeforeFailures(\Closure $closure): void {
$this->expectException(Exception::class);
$this->expectExceptionCode(Exception::ARGUMENT_TYPE_ERROR);
$d = new Document();
$d->appendChild($d->createElement('html'));
$d->documentElement->appendChild($d->createElement('body'));
$div = $d->body->appendChild($d->createElement('div'));
$o = $d->body->appendChild($d->createTextNode('ook'));
$div2 = $d->body->appendChild($d->createElement('div'));
$div->after($object);
$closure();
}
}

2
tests/cases/TestDocument.php

@ -133,7 +133,7 @@ class TestDocument extends \PHPUnit\Framework\TestCase {
* @covers \MensBeam\HTML\DOM\Document::preInsertionValidity
* @covers \MensBeam\HTML\DOM\Document::replaceTemplates
* @covers \MensBeam\HTML\DOM\Document::__get_quirksMode
* @covers \MensBeam\HTML\DOM\Node::getRootNode
* @covers \MensBeam\HTML\DOM\BaseNode::getRootNode
*/
public function testDocumentCreation(): void {
// Test null source

2
tests/cases/TestParentNode.php

@ -129,7 +129,7 @@ class TestParentNode extends \PHPUnit\Framework\TestCase {
/**
* @dataProvider providePreInsertionValidationFailures
* @covers \MensBeam\HTML\DOM\DOMException::__construct
* @covers \MensBeam\HTML\DOM\Node::getRootNode
* @covers \MensBeam\HTML\DOM\BaseNode::getRootNode
* @covers \MensBeam\HTML\DOM\ParentNode::preInsertionValidity
*/
public function testPreInsertionValidationFailures(\Closure $closure, int $errorCode = DOMException::HIERARCHY_REQUEST_ERROR): void {

2
tests/phpunit.dist.xml

@ -16,6 +16,7 @@
</coverage>
<testsuites>
<testsuite name="DOM">
<file>cases/TestBaseNode.php</file>
<file>cases/TestChildNode.php</file>
<file>cases/TestDocument.php</file>
<file>cases/TestDocumentFragment.php</file>
@ -24,7 +25,6 @@
<file>cases/TestElementMap.php</file>
<file>cases/TestLeafNode.php</file>
<file>cases/TestMoonwalk.php</file>
<file>cases/TestNode.php</file>
<file>cases/TestParentNode.php</file>
<file>cases/TestTokenList.php</file>
<file>cases/TestWalk.php</file>

Loading…
Cancel
Save