support element.relList and improve TreeWalker

This commit is contained in:
Karl Seguin
2026-01-07 10:34:47 +08:00
parent 90ee919f45
commit 6af9d12f71
7 changed files with 157 additions and 18 deletions

View File

@@ -93,6 +93,7 @@ _attribute_named_node_map_lookup: std.AutoHashMapUnmanaged(usize, *Element.Attri
_element_styles: Element.StyleLookup = .{}, _element_styles: Element.StyleLookup = .{},
_element_datasets: Element.DatasetLookup = .{}, _element_datasets: Element.DatasetLookup = .{},
_element_class_lists: Element.ClassListLookup = .{}, _element_class_lists: Element.ClassListLookup = .{},
_element_rel_lists: Element.RelListLookup = .{},
_element_shadow_roots: Element.ShadowRootLookup = .{}, _element_shadow_roots: Element.ShadowRootLookup = .{},
_element_assigned_slots: Element.AssignedSlotLookup = .{}, _element_assigned_slots: Element.AssignedSlotLookup = .{},
@@ -263,6 +264,7 @@ fn reset(self: *Page, comptime initializing: bool) !void {
self._element_styles = .{}; self._element_styles = .{};
self._element_datasets = .{}; self._element_datasets = .{};
self._element_class_lists = .{}; self._element_class_lists = .{};
self._element_rel_lists = .{};
self._element_shadow_roots = .{}; self._element_shadow_roots = .{};
self._element_assigned_slots = .{}; self._element_assigned_slots = .{};
self._notified_network_idle = .init; self._notified_network_idle = .init;

View File

@@ -41,6 +41,14 @@ pub fn isArray(self: Value) bool {
return self.js_val.isArray(); 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 { pub fn toString(self: Value, allocator: Allocator) ![]const u8 {
return self.context.valueToString(self.js_val, .{ .allocator = allocator }); 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() }; 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 { pub fn toObject(self: Value) js.Object {
return .{ return .{
.context = self.context, .context = self.context,

View File

@@ -79,25 +79,81 @@ pub fn parentNode(self: *DOMTreeWalker) !?*Node {
pub fn firstChild(self: *DOMTreeWalker) !?*Node { pub fn firstChild(self: *DOMTreeWalker) !?*Node {
var node = self._current.firstChild(); var node = self._current.firstChild();
while (node) |n| { 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; self._current = n;
return 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; return null;
} }
pub fn lastChild(self: *DOMTreeWalker) !?*Node { pub fn lastChild(self: *DOMTreeWalker) !?*Node {
var node = self._current.lastChild(); var node = self._current.lastChild();
while (node) |n| { 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; self._current = n;
return 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; return null;
} }
@@ -131,15 +187,39 @@ pub fn previousNode(self: *DOMTreeWalker) !?*Node {
var sibling = self.previousSiblingOrNull(node); var sibling = self.previousSiblingOrNull(node);
while (sibling) |sib| { while (sibling) |sib| {
node = sib; node = sib;
var child = self.lastChildOrNull(node);
while (child) |c| { // Check if this sibling is rejected before descending into it
if (self.isInSubtree(c)) { const sib_result = try self.acceptNode(node);
node = c; if (sib_result == NodeFilter.FILTER_REJECT) {
child = self.lastChildOrNull(node); // Skip this sibling and its descendants entirely
} else { sibling = self.previousSiblingOrNull(node);
break; 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) { if (try self.acceptNode(node) == NodeFilter.FILTER_ACCEPT) {
self._current = node; self._current = node;
return node; return node;

View File

@@ -301,14 +301,26 @@ pub fn createEvent(_: *const Document, event_type: []const u8, page: *Page) !*@i
return error.NotSupported; return error.NotSupported;
} }
pub fn createTreeWalker(_: *const Document, root: *Node, what_to_show: ?u32, filter: ?DOMTreeWalker.FilterOpts, page: *Page) !*DOMTreeWalker { pub fn createTreeWalker(_: *const Document, root: *Node, what_to_show: ?js.Value, filter: ?DOMTreeWalker.FilterOpts, page: *Page) !*DOMTreeWalker {
const show = what_to_show orelse NodeFilter.SHOW_ALL; return DOMTreeWalker.init(root, try whatToShow(what_to_show), filter, page);
return DOMTreeWalker.init(root, show, filter, page);
} }
pub fn createNodeIterator(_: *const Document, root: *Node, what_to_show: ?u32, filter: ?DOMNodeIterator.FilterOpts, page: *Page) !*DOMNodeIterator { pub fn createNodeIterator(_: *const Document, root: *Node, what_to_show: ?js.Value, filter: ?DOMNodeIterator.FilterOpts, page: *Page) !*DOMNodeIterator {
const show = what_to_show orelse NodeFilter.SHOW_ALL; return DOMNodeIterator.init(root, try whatToShow(what_to_show), filter, page);
return DOMNodeIterator.init(root, 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 { pub fn getReadyState(self: *const Document) []const u8 {

View File

@@ -44,6 +44,7 @@ const Element = @This();
pub const DatasetLookup = std.AutoHashMapUnmanaged(*Element, *DOMStringMap); pub const DatasetLookup = std.AutoHashMapUnmanaged(*Element, *DOMStringMap);
pub const StyleLookup = std.AutoHashMapUnmanaged(*Element, *CSSStyleProperties); pub const StyleLookup = std.AutoHashMapUnmanaged(*Element, *CSSStyleProperties);
pub const ClassListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTokenList); 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 ShadowRootLookup = std.AutoHashMapUnmanaged(*Element, *ShadowRoot);
pub const AssignedSlotLookup = std.AutoHashMapUnmanaged(*Element, *Html.Slot); 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.*; 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 { pub fn getDataset(self: *Element, page: *Page) !*DOMStringMap {
const gop = try page._element_datasets.getOrPut(page.arena, self); const gop = try page._element_datasets.getOrPut(page.arena, self);
if (!gop.found_existing) { if (!gop.found_existing) {

View File

@@ -220,7 +220,18 @@ pub const JsApi = struct {
pub const hash = bridge.accessor(Anchor.getHash, Anchor.setHash, .{}); pub const hash = bridge.accessor(Anchor.getHash, Anchor.setHash, .{});
pub const @"type" = bridge.accessor(Anchor.getType, Anchor.setType, .{}); pub const @"type" = bridge.accessor(Anchor.getType, Anchor.setType, .{});
pub const text = bridge.accessor(Anchor.getText, Anchor.setText, .{}); 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, .{}); 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 <a> elements
const namespace = element._namespace;
if (namespace != .html and namespace != .svg) {
return null;
}
return element.getRelList(page);
}
}; };
const testing = @import("../../../../testing.zig"); const testing = @import("../../../../testing.zig");

View File

@@ -68,6 +68,16 @@ pub const JsApi = struct {
pub const rel = bridge.accessor(Link.getRel, Link.setRel, .{}); pub const rel = bridge.accessor(Link.getRel, Link.setRel, .{});
pub const href = bridge.accessor(Link.getHref, Link.setHref, .{}); 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 <link> elements, not SVG or MathML
if (element._namespace != .html) {
return null;
}
return element.getRelList(page);
}
}; };
const testing = @import("../../../../testing.zig"); const testing = @import("../../../../testing.zig");