webapi: TreeWalker sibling traversal follows the spec algorithm

Fixes WPT /dom/traversal/TreeWalker-previousSiblingLastChildSkip.html
and 59 subtests of TreeWalker.html (701/761 -> 760/761):
TreeWalker.previousSibling()/nextSibling() only scanned the current
node's direct siblings, but the spec's "traverse siblings" algorithm
also:

- descends into a skipped (FILTER_SKIP) sibling's children - only a
  rejected sibling excludes its whole subtree - so from B2 with B1
  skipped, previousSibling() must return B1's last child;
- climbs to the parent when the siblings are exhausted and continues
  from the parent's siblings, stopping at the root or at an accepted
  parent.

Both directions now share the spec's traverseSiblings implementation.

Coverage:
- /dom/traversal/TreeWalker-previousSiblingLastChildSkip.html 0/1 -> 1/1
  (fully green)
- /dom/traversal/TreeWalker.html 701/761 -> 760/761

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Francis Bouvier
2026-07-11 16:05:28 +02:00
committed by Karl Seguin
parent df94585d0a
commit ee97f3b888

View File

@@ -174,27 +174,52 @@ pub fn lastChild(self: *DOMTreeWalker, frame: *Frame) !?*Node {
}
pub fn previousSibling(self: *DOMTreeWalker, frame: *Frame) !?*Node {
var node = self.previousSiblingOrNull(self._current);
while (node) |n| {
if (try self.acceptNode(n, frame) == NodeFilter.FILTER_ACCEPT) {
self._current = n;
return n;
}
node = self.previousSiblingOrNull(n);
}
return null;
return self.traverseSiblings(.previous, frame);
}
pub fn nextSibling(self: *DOMTreeWalker, frame: *Frame) !?*Node {
var node = self.nextSiblingOrNull(self._current);
while (node) |n| {
if (try self.acceptNode(n, frame) == NodeFilter.FILTER_ACCEPT) {
self._current = n;
return n;
return self.traverseSiblings(.next, frame);
}
// The spec's "traverse siblings" algorithm: a skipped (but not rejected)
// sibling's children are still candidates, and when the siblings run out the
// walk climbs to the parent and continues from its siblings, stopping at the
// root or at an accepted parent.
fn traverseSiblings(self: *DOMTreeWalker, comptime direction: enum { next, previous }, frame: *Frame) !?*Node {
var node = self._current;
if (node == self._root) return null;
while (true) {
var sibling: ?*Node = if (direction == .next)
self.nextSiblingOrNull(node)
else
self.previousSiblingOrNull(node);
while (sibling) |sib| {
node = sib;
const result = try self.acceptNode(node, frame);
if (result == NodeFilter.FILTER_ACCEPT) {
self._current = node;
return node;
}
sibling = if (direction == .next)
self.firstChildOrNull(node)
else
self.lastChildOrNull(node);
if (result == NodeFilter.FILTER_REJECT or sibling == null) {
sibling = if (direction == .next)
self.nextSiblingOrNull(node)
else
self.previousSiblingOrNull(node);
}
}
node = node.parentNode() orelse return null;
if (node == self._root) return null;
if (try self.acceptNode(node, frame) == NodeFilter.FILTER_ACCEPT) {
return null;
}
node = self.nextSiblingOrNull(n);
}
return null;
}
pub fn previousNode(self: *DOMTreeWalker, frame: *Frame) !?*Node {