Added in BaseNode::compareDocumentPosition

• BaseNode::compareDocumentPosition is not tested yet
• To facilitate preceding/following support Walk::walkShallow now has a 
startingNode parameter.
This commit is contained in:
Dustin Wilson 2021-10-14 16:51:37 -05:00
parent 7faaef3441
commit d7cab23226
4 changed files with 123 additions and 9 deletions

View file

@ -27,7 +27,7 @@ class DOMException extends Exception {
public function __construct(int $code, ...$args) {
self::$messages = array_replace(parent::$messages, [
3 => 'Hierarchy request error; supplied node is not allowed here',
3 => 'Hierarchy request error',
4 => 'Supplied node does not belong to this document',
5 => 'Invalid character',
7 => 'Modification not allowed here',

View file

@ -14,6 +14,9 @@ namespace MensBeam\HTML\DOM;
* \DOMNode descendant, so a trait is the next best thing.
*/
trait BaseNode {
private static ?int $rand = null;
// Disable C14N
public function C14N($exclusive = null, $with_comments = null, ?array $xpath = null, ?array $ns_prefixes = null): bool {
throw new DOMException(DOMException::NOT_SUPPORTED, __METHOD__ . ' is meant for XML and buggy; use Document::saveHTML or cast to a string');
@ -24,6 +27,97 @@ trait BaseNode {
throw new DOMException(DOMException::NOT_SUPPORTED, __METHOD__ . ' is meant for XML and buggy; use Document::saveHTMLFile');
}
public function compareDocumentPosition(\DOMNode $other): int {
# The compareDocumentPosition(other) method steps are:
#
# 1. If this is other, then return zero.
if ($this->isSameNode($other)) {
return 0;
}
# 2. Let node1 be other and node2 be this.
$node1 = $other;
$node2 = $this;
# 3. Let attr1 and attr2 be null.
$attr1 = $attr2 = null;
# 4. If node1 is an attribute, then set attr1 to node1 and node1 to attr1s
# element.
if ($node1 instanceof Attr) {
$attr1 = $node1;
$node1 = $attr1->ownerElement;
}
# 5. If node2 is an attribute, then:
if ($node2 instanceof Attr) {
# 1. Set attr2 to node2 and node2 to attr2s element.
$attr2 = $node2;
$node2 = $attr2->ownerElement;
# 2. If attr1 and node1 are non-null, and node2 is node1, then:
if ($attr1 !== null && $node1 !== null && $node2 === $node1) {
# 1. For each attr in node2s attribute list:
foreach ($node2->attributes as $attr) {
# 1. If attr equals attr1, then return the result of adding DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC and DOCUMENT_POSITION_PRECEDING.
if ($attr->isSameNode($attr1)) {
return Node::DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC + Node::DOCUMENT_POSITION_PRECEDING;
}
# 2. If attr equals attr2, then return the result of adding DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC and DOCUMENT_POSITION_FOLLOWING.
if ($attr->isSameNode($attr2)) {
return Node::DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC + Node::DOCUMENT_POSITION_FOLLOWING;
}
}
}
}
# 6. If node1 or node2 is null, or node1s root is not node2s root, then return the
# result of adding DOCUMENT_POSITION_DISCONNECTED,
# DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC, and either
# DOCUMENT_POSITION_PRECEDING or DOCUMENT_POSITION_FOLLOWING, with the constraint
# that this is to be consistent, together.
#
# NOTE: Whether to return DOCUMENT_POSITION_PRECEDING or
# DOCUMENT_POSITION_FOLLOWING is typically implemented via pointer comparison.
# In JavaScript implementations a cached Math.random() value can be used.
if (self::$rand === null) {
self::$rand = rand(0, 1);
}
if ($node1 === null || $node2 === null || !$node1->getRootNode()->isSameNode($node2->getRootNode())) {
return Node::DOCUMENT_POSITION_DISCONNECTED + Node::DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC + (($rand === 0) ? DOCUMENT_POSITION_PRECEDING : DOCUMENT_POSITION_FOLLOWING);
}
# 7. If node1 is an ancestor of node2 and attr1 is null, or node1 is node2 and attr2
# is non-null, then return the result of adding DOCUMENT_POSITION_CONTAINS to
# DOCUMENT_POSITION_PRECEDING.
if (($node1->isSameNode($node2) && $attr2 !== null) || ($attr1 === null && $node2->moonwalk(function($n) use($node1) {
return ($n->isSameNode($node1));
})->current() !== null)) {
return Node::DOCUMENT_POSITION_CONTAINS + Node::DOCUMENT_POSITION_PRECEDING;
}
# 8. If node1 is a descendant of node2 and attr2 is null, or node1 is node2 and attr1
# is non-null, then return the result of adding DOCUMENT_POSITION_CONTAINED_BY to
# DOCUMENT_POSITION_FOLLOWING.
if (($node1 === $node2 && $attr1 !== null) || ($attr2 === null && $node2->walk(function($n) use($node1) {
return ($n->isSameNode($node1));
})->current() !== null)) {
return Node::DOCUMENT_POSITION_CONTAINED_BY + Node::DOCUMENT_POSITION_FOLLOWING;
}
# 9. If node1 is preceding node2, then return DOCUMENT_POSITION_PRECEDING.
if ($node2->parentNode->walkShallow(function($n) use($node1) {
return ($n->isSameNode($node1));
}, $node2, true)) {
return Node::DOCUMENT_POSITION_PRECEDING;
}
# 10. Return DOCUMENT_POSITION_FOLLOWING.
return Node::DOCUMENT_POSITION_FOLLOWING;
}
// Disable getLineNo
public function getLineNo(): int {
throw new DOMException(DOMException::NOT_SUPPORTED, __METHOD__ . ' is not in the standard, is buggy, and useless');

View file

@ -47,12 +47,21 @@ trait Walk {
*
* @param ?\Closure $filter - An optional callback function used to filter; if not provided the generator will
* just yield every node.
* @param Comment|Element|ProcessingInstruction|Text|null $startingNode - An optional node to begin from.
* @param bool $backwards - An optional setting that if true makes the generator instead walk backwards
* through the child nodes.
*/
public function walkShallow(?\Closure $filter = null, bool $backwards = false): \Generator {
public function walkShallow(?\Closure $filter = null, Comment|Element|ProcessingInstruction|Text|null $startingNode = null, bool $backwards = false): \Generator {
$node = (!$this instanceof TemplateElement) ? $this : $this->content;
$node = (!$backwards) ? $node->firstChild : $node->lastChild;
if ($startingNode === null) {
$node = (!$backwards) ? $node->firstChild : $node->lastChild;
} else {
if ($startingNode->parentNode === null || !$startingNode->parentNode->isSameNode($node)) {
throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR);
}
$node = $startingNode;
}
if ($node !== null) {
do {

View file

@ -10,6 +10,7 @@ namespace MensBeam\HTML\DOM\TestCase;
use MensBeam\HTML\DOM\{
Document,
DOMException,
Element
};
@ -49,21 +50,31 @@ class TestWalk extends \PHPUnit\Framework\TestCase {
/** @covers \MensBeam\HTML\DOM\Walk::walkShallow */
public function testWalkShallowBackwards(): void {
public function testWalkShallow(): void {
// Test walking backwards
$d = new Document();
$d->appendChild($d->createElement('html'));
$d->documentElement->appendChild($d->createElement('body'));
$d->body->innerHTML = '<span class="one">O</span><span class="two">O</span><span class="three">O</span><span class="four">K</span>';
$iterator = $d->body->walkShallow(null, true);
$iterator = $d->body->walkShallow(null, $d->body->firstChild->nextSibling, true);
$count = 0;
foreach ($iterator as $i) {
$count++;
if ($i->getAttribute('class') === 'two') {
break;
}
}
$this->assertSame(3, $count);
$this->assertSame(2, $count);
}
/** @covers \MensBeam\HTML\DOM\Walk::walkShallow */
public function testWalkShallowFailure(): void {
$this->expectException(DOMException::class);
$this->expectExceptionCode(DOMException::HIERARCHY_REQUEST_ERROR);
// Test walking backwards
$d = new Document();
$d->appendChild($d->createElement('html'));
$d->documentElement->appendChild($d->createElement('body'));
$d->body->innerHTML = '<span class="one">O</span><span class="two">O</span><span class="three">O</span><span class="four">K</span>';
$d->body->walkShallow(null, $d->createElement('fail'), true)->current();
}
}