From 2e07af4edb964019ec5985b6a6712fb604e2b07c Mon Sep 17 00:00:00 2001 From: Dustin Wilson Date: Wed, 13 Oct 2021 16:48:38 -0500 Subject: [PATCH] Added ChildNode::before, started on a bit of documentation --- README.md | 18 ++++++- lib/Attr.php | 10 +++- lib/Comment.php | 8 +++ lib/Document.php | 8 +++ lib/DocumentFragment.php | 8 +++ lib/Element.php | 7 +++ lib/Node.php | 23 ++++++++ lib/ProcessingInstruction.php | 8 +++ lib/Text.php | 8 +++ lib/traits/{Node.php => BaseNode.php} | 8 +-- lib/traits/ChildNode.php | 54 ++++++++++++++++++- lib/traits/LeafNode.php | 2 +- lib/traits/ParentNode.php | 2 +- .../cases/{TestNode.php => TestBaseNode.php} | 8 +-- tests/cases/TestChildNode.php | 51 +++++++++++++----- tests/cases/TestDocument.php | 2 +- tests/cases/TestParentNode.php | 2 +- tests/phpunit.dist.xml | 2 +- 18 files changed, 200 insertions(+), 29 deletions(-) create mode 100644 lib/Node.php rename lib/traits/{Node.php => BaseNode.php} (94%) rename tests/cases/{TestNode.php => TestBaseNode.php} (84%) diff --git a/README.md b/README.md index 36caeaa..9e75f16 100644 --- a/README.md +++ b/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. \ No newline at end of file +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`][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. \ No newline at end of file diff --git a/lib/Attr.php b/lib/Attr.php index d6a90dd..33ea74e 100644 --- a/lib/Attr.php +++ b/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; } diff --git a/lib/Comment.php b/lib/Comment.php index dce9487..637b4b7 100644 --- a/lib/Comment.php +++ b/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; } diff --git a/lib/Document.php b/lib/Document.php index de43254..df3eb86 100644 --- a/lib/Document.php +++ b/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; diff --git a/lib/DocumentFragment.php b/lib/DocumentFragment.php index 57a43aa..3d874c0 100644 --- a/lib/DocumentFragment.php +++ b/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 { diff --git a/lib/Element.php b/lib/Element.php index 172540f..3250550 100644 --- a/lib/Element.php +++ b/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'); diff --git a/lib/Node.php b/lib/Node.php new file mode 100644 index 0000000..cdeac0b --- /dev/null +++ b/lib/Node.php @@ -0,0 +1,23 @@ +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); + } } diff --git a/lib/traits/LeafNode.php b/lib/traits/LeafNode.php index 50c4be2..f732028 100644 --- a/lib/traits/LeafNode.php +++ b/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); diff --git a/lib/traits/ParentNode.php b/lib/traits/ParentNode.php index eccd622..4c4e611 100644 --- a/lib/traits/ParentNode.php +++ b/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 { diff --git a/tests/cases/TestNode.php b/tests/cases/TestBaseNode.php similarity index 84% rename from tests/cases/TestNode.php rename to tests/cases/TestBaseNode.php index f630055..93e1c3b 100644 --- a/tests/cases/TestNode.php +++ b/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'); diff --git a/tests/cases/TestChildNode.php b/tests/cases/TestChildNode.php index 4618e76..579ad28 100644 --- a/tests/cases/TestChildNode.php +++ b/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('
ookeek
', (string)$d->body); $div->after($o); + $this->assertSame('
ookeek
', (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('ookeek
eek
', (string)$d->body); + $div->before($o); + $this->assertSame('eek
ook
eek
', (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(); } } \ No newline at end of file diff --git a/tests/cases/TestDocument.php b/tests/cases/TestDocument.php index d764119..b3e6f34 100644 --- a/tests/cases/TestDocument.php +++ b/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 diff --git a/tests/cases/TestParentNode.php b/tests/cases/TestParentNode.php index 0cc2fb3..138f857 100644 --- a/tests/cases/TestParentNode.php +++ b/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 { diff --git a/tests/phpunit.dist.xml b/tests/phpunit.dist.xml index 5713573..89b5218 100644 --- a/tests/phpunit.dist.xml +++ b/tests/phpunit.dist.xml @@ -16,6 +16,7 @@ + cases/TestBaseNode.php cases/TestChildNode.php cases/TestDocument.php cases/TestDocumentFragment.php @@ -24,7 +25,6 @@ cases/TestElementMap.php cases/TestLeafNode.php cases/TestMoonwalk.php - cases/TestNode.php cases/TestParentNode.php cases/TestTokenList.php cases/TestWalk.php