From 6af9d12f71208d221057f8dee929846f3cfe0d3a Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 7 Jan 2026 10:34:47 +0800 Subject: [PATCH] support element.relList and improve TreeWalker --- src/browser/Page.zig | 2 + src/browser/js/Value.zig | 12 +++ src/browser/webapi/DOMTreeWalker.zig | 104 ++++++++++++++++++--- src/browser/webapi/Document.zig | 24 +++-- src/browser/webapi/Element.zig | 12 +++ src/browser/webapi/element/html/Anchor.zig | 11 +++ src/browser/webapi/element/html/Link.zig | 10 ++ 7 files changed, 157 insertions(+), 18 deletions(-) diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 64e6eef3..8df24ec4 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -93,6 +93,7 @@ _attribute_named_node_map_lookup: std.AutoHashMapUnmanaged(usize, *Element.Attri _element_styles: Element.StyleLookup = .{}, _element_datasets: Element.DatasetLookup = .{}, _element_class_lists: Element.ClassListLookup = .{}, +_element_rel_lists: Element.RelListLookup = .{}, _element_shadow_roots: Element.ShadowRootLookup = .{}, _element_assigned_slots: Element.AssignedSlotLookup = .{}, @@ -263,6 +264,7 @@ fn reset(self: *Page, comptime initializing: bool) !void { self._element_styles = .{}; self._element_datasets = .{}; self._element_class_lists = .{}; + self._element_rel_lists = .{}; self._element_shadow_roots = .{}; self._element_assigned_slots = .{}; self._notified_network_idle = .init; diff --git a/src/browser/js/Value.zig b/src/browser/js/Value.zig index 329fb128..75e2495d 100644 --- a/src/browser/js/Value.zig +++ b/src/browser/js/Value.zig @@ -41,6 +41,14 @@ pub fn isArray(self: Value) bool { return self.js_val.isArray(); } +pub fn isNull(self: Value) bool { + return self.js_val.isNull(); +} + +pub fn isUndefined(self: Value) bool { + return self.js_val.isUndefined(); +} + pub fn toString(self: Value, allocator: Allocator) ![]const u8 { return self.context.valueToString(self.js_val, .{ .allocator = allocator }); } @@ -61,6 +69,10 @@ pub fn persist(self: Value) !Value { return Value{ .context = context, .js_val = persisted.toValue() }; } +pub fn toZig(self: Value, comptime T: type) !T { + return self.context.jsValueToZig(T, self.js_val); +} + pub fn toObject(self: Value) js.Object { return .{ .context = self.context, diff --git a/src/browser/webapi/DOMTreeWalker.zig b/src/browser/webapi/DOMTreeWalker.zig index 88ca271a..6cae910e 100644 --- a/src/browser/webapi/DOMTreeWalker.zig +++ b/src/browser/webapi/DOMTreeWalker.zig @@ -79,25 +79,81 @@ pub fn parentNode(self: *DOMTreeWalker) !?*Node { pub fn firstChild(self: *DOMTreeWalker) !?*Node { var node = self._current.firstChild(); + while (node) |n| { - if (try self.acceptNode(n) == NodeFilter.FILTER_ACCEPT) { + const filter_result = try self.acceptNode(n); + + if (filter_result == NodeFilter.FILTER_ACCEPT) { self._current = n; return n; } - node = self.nextSiblingOrNull(n); + + if (filter_result == NodeFilter.FILTER_SKIP) { + // Descend into children of this skipped node + if (n.firstChild()) |child| { + node = child; + continue; + } + } + + // REJECT or SKIP with no children - find next sibling, walking up if necessary + var current_node = n; + while (true) { + if (current_node.nextSibling()) |sibling| { + node = sibling; + break; + } + + // No sibling, go up to parent + const parent = current_node._parent orelse return null; + if (parent == self._current) { + // We've exhausted all children of self._current + return null; + } + current_node = parent; + } } + return null; } pub fn lastChild(self: *DOMTreeWalker) !?*Node { var node = self._current.lastChild(); + while (node) |n| { - if (try self.acceptNode(n) == NodeFilter.FILTER_ACCEPT) { + const filter_result = try self.acceptNode(n); + + if (filter_result == NodeFilter.FILTER_ACCEPT) { self._current = n; return n; } - node = self.previousSiblingOrNull(n); + + if (filter_result == NodeFilter.FILTER_SKIP) { + // Descend into children of this skipped node + if (n.lastChild()) |child| { + node = child; + continue; + } + } + + // REJECT or SKIP with no children - find previous sibling, walking up if necessary + var current_node = n; + while (true) { + if (current_node.previousSibling()) |sibling| { + node = sibling; + break; + } + + // No sibling, go up to parent + const parent = current_node._parent orelse return null; + if (parent == self._current) { + // We've exhausted all children of self._current + return null; + } + current_node = parent; + } } + return null; } @@ -131,15 +187,39 @@ pub fn previousNode(self: *DOMTreeWalker) !?*Node { var sibling = self.previousSiblingOrNull(node); while (sibling) |sib| { node = sib; - var child = self.lastChildOrNull(node); - while (child) |c| { - if (self.isInSubtree(c)) { - node = c; - child = self.lastChildOrNull(node); - } else { - break; - } + + // Check if this sibling is rejected before descending into it + const sib_result = try self.acceptNode(node); + if (sib_result == NodeFilter.FILTER_REJECT) { + // Skip this sibling and its descendants entirely + sibling = self.previousSiblingOrNull(node); + continue; } + + // Descend to the deepest last child, but respect FILTER_REJECT + while (true) { + var child = self.lastChildOrNull(node); + + // Find the rightmost non-rejected child + while (child) |c| { + if (!self.isInSubtree(c)) break; + + const filter_result = try self.acceptNode(c); + if (filter_result == NodeFilter.FILTER_REJECT) { + // Skip this child and try its previous sibling + child = self.previousSiblingOrNull(c); + } else { + // ACCEPT or SKIP - use this child + break; + } + } + + if (child == null) break; // No acceptable children + + // Descend into this child + node = child.?; + } + if (try self.acceptNode(node) == NodeFilter.FILTER_ACCEPT) { self._current = node; return node; diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index 6124e23b..8dafc21f 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -301,14 +301,26 @@ pub fn createEvent(_: *const Document, event_type: []const u8, page: *Page) !*@i return error.NotSupported; } -pub fn createTreeWalker(_: *const Document, root: *Node, what_to_show: ?u32, filter: ?DOMTreeWalker.FilterOpts, page: *Page) !*DOMTreeWalker { - const show = what_to_show orelse NodeFilter.SHOW_ALL; - return DOMTreeWalker.init(root, show, filter, page); +pub fn createTreeWalker(_: *const Document, root: *Node, what_to_show: ?js.Value, filter: ?DOMTreeWalker.FilterOpts, page: *Page) !*DOMTreeWalker { + return DOMTreeWalker.init(root, try whatToShow(what_to_show), filter, page); } -pub fn createNodeIterator(_: *const Document, root: *Node, what_to_show: ?u32, filter: ?DOMNodeIterator.FilterOpts, page: *Page) !*DOMNodeIterator { - const show = what_to_show orelse NodeFilter.SHOW_ALL; - return DOMNodeIterator.init(root, show, filter, page); +pub fn createNodeIterator(_: *const Document, root: *Node, what_to_show: ?js.Value, filter: ?DOMNodeIterator.FilterOpts, page: *Page) !*DOMNodeIterator { + return DOMNodeIterator.init(root, try whatToShow(what_to_show), filter, page); +} + +fn whatToShow(value_: ?js.Value) !u32 { + const value = value_ orelse return 4294967295; // show all when undefined + if (value.isUndefined()) { + // undefined explicitly passed + return 4294967295; + } + + if (value.isNull()) { + return 0; + } + + return value.toZig(u32); } pub fn getReadyState(self: *const Document) []const u8 { diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 4de815e5..98bbbf7b 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -44,6 +44,7 @@ const Element = @This(); pub const DatasetLookup = std.AutoHashMapUnmanaged(*Element, *DOMStringMap); pub const StyleLookup = std.AutoHashMapUnmanaged(*Element, *CSSStyleProperties); pub const ClassListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTokenList); +pub const RelListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTokenList); pub const ShadowRootLookup = std.AutoHashMapUnmanaged(*Element, *ShadowRoot); pub const AssignedSlotLookup = std.AutoHashMapUnmanaged(*Element, *Html.Slot); @@ -550,6 +551,17 @@ pub fn getClassList(self: *Element, page: *Page) !*collections.DOMTokenList { return gop.value_ptr.*; } +pub fn getRelList(self: *Element, page: *Page) !*collections.DOMTokenList { + const gop = try page._element_rel_lists.getOrPut(page.arena, self); + if (!gop.found_existing) { + gop.value_ptr.* = try page._factory.create(collections.DOMTokenList{ + ._element = self, + ._attribute_name = "rel", + }); + } + return gop.value_ptr.*; +} + pub fn getDataset(self: *Element, page: *Page) !*DOMStringMap { const gop = try page._element_datasets.getOrPut(page.arena, self); if (!gop.found_existing) { diff --git a/src/browser/webapi/element/html/Anchor.zig b/src/browser/webapi/element/html/Anchor.zig index 44e24823..1c427ecd 100644 --- a/src/browser/webapi/element/html/Anchor.zig +++ b/src/browser/webapi/element/html/Anchor.zig @@ -220,7 +220,18 @@ pub const JsApi = struct { pub const hash = bridge.accessor(Anchor.getHash, Anchor.setHash, .{}); pub const @"type" = bridge.accessor(Anchor.getType, Anchor.setType, .{}); pub const text = bridge.accessor(Anchor.getText, Anchor.setText, .{}); + pub const relList = bridge.accessor(_getRelList, null, .{ .null_as_undefined = true }); pub const toString = bridge.function(Anchor.getHref, .{}); + + fn _getRelList(self: *Anchor, page: *Page) !?*@import("../../collections.zig").DOMTokenList { + const element = self.asElement(); + // relList is only valid for HTML and SVG elements + const namespace = element._namespace; + if (namespace != .html and namespace != .svg) { + return null; + } + return element.getRelList(page); + } }; const testing = @import("../../../../testing.zig"); diff --git a/src/browser/webapi/element/html/Link.zig b/src/browser/webapi/element/html/Link.zig index f8607ca8..6c4ca29a 100644 --- a/src/browser/webapi/element/html/Link.zig +++ b/src/browser/webapi/element/html/Link.zig @@ -68,6 +68,16 @@ pub const JsApi = struct { pub const rel = bridge.accessor(Link.getRel, Link.setRel, .{}); pub const href = bridge.accessor(Link.getHref, Link.setHref, .{}); + pub const relList = bridge.accessor(_getRelList, null, .{ .null_as_undefined = true }); + + fn _getRelList(self: *Link, page: *Page) !?*@import("../../collections.zig").DOMTokenList { + const element = self.asElement(); + // relList is only valid for HTML elements, not SVG or MathML + if (element._namespace != .html) { + return null; + } + return element.getRelList(page); + } }; const testing = @import("../../../../testing.zig");