From ee97f3b888cb4ce5e7f4a22716375d71bb383bd4 Mon Sep 17 00:00:00 2001 From: Francis Bouvier Date: Sat, 11 Jul 2026 16:05:28 +0200 Subject: [PATCH] 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 --- src/browser/webapi/DOMTreeWalker.zig | 57 ++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/src/browser/webapi/DOMTreeWalker.zig b/src/browser/webapi/DOMTreeWalker.zig index 6c5d616a4..71e7b0507 100644 --- a/src/browser/webapi/DOMTreeWalker.zig +++ b/src/browser/webapi/DOMTreeWalker.zig @@ -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 {