Merge pull request #1325 from lightpanda-io/tokenlist_treewalker

support element.relList and improve TreeWalker
This commit is contained in:
Karl Seguin
2026-01-07 10:41:42 +08:00
committed by GitHub
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_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;

View File

@@ -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,

View File

@@ -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;
// 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)) {
node = c;
child = self.lastChildOrNull(node);
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;

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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 <a> elements
const namespace = element._namespace;
if (namespace != .html and namespace != .svg) {
return null;
}
return element.getRelList(page);
}
};
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 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");