Merge branch 'main' into css-improvements

This commit is contained in:
Adrià Arrufat
2026-03-20 09:46:31 +09:00
101 changed files with 3211 additions and 1203 deletions

View File

@@ -36,7 +36,9 @@ dom_node: *Node,
registry: *CDPNode.Registry,
page: *Page,
arena: std.mem.Allocator,
prune: bool = false,
prune: bool = true,
interactive_only: bool = false,
max_depth: u32 = std.math.maxInt(u32) - 1,
pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) error{WriteFailed}!void {
var visitor = JsonVisitor{ .jw = jw, .tree = self };
@@ -47,7 +49,7 @@ pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) error{WriteFailed}!
};
var visibility_cache: Element.VisibilityCache = .empty;
var pointer_events_cache: Element.PointerEventsCache = .empty;
self.walk(self.dom_node, &xpath_buffer, null, &visitor, 1, listener_targets, &visibility_cache, &pointer_events_cache) catch |err| {
self.walk(self.dom_node, &xpath_buffer, null, &visitor, 1, listener_targets, &visibility_cache, &pointer_events_cache, 0) catch |err| {
log.err(.app, "semantic tree json dump failed", .{ .err = err });
return error.WriteFailed;
};
@@ -62,7 +64,7 @@ pub fn textStringify(self: @This(), writer: *std.Io.Writer) error{WriteFailed}!v
};
var visibility_cache: Element.VisibilityCache = .empty;
var pointer_events_cache: Element.PointerEventsCache = .empty;
self.walk(self.dom_node, &xpath_buffer, null, &visitor, 1, listener_targets, &visibility_cache, &pointer_events_cache) catch |err| {
self.walk(self.dom_node, &xpath_buffer, null, &visitor, 1, listener_targets, &visibility_cache, &pointer_events_cache, 0) catch |err| {
log.err(.app, "semantic tree text dump failed", .{ .err = err });
return error.WriteFailed;
};
@@ -75,7 +77,7 @@ const OptionData = struct {
};
const NodeData = struct {
id: u32,
id: CDPNode.Id,
axn: AXNode,
role: []const u8,
name: ?[]const u8,
@@ -86,7 +88,9 @@ const NodeData = struct {
node_name: []const u8,
};
fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_name: ?[]const u8, visitor: anytype, index: usize, listener_targets: interactive.ListenerTargetMap, visibility_cache: ?*Element.VisibilityCache, pointer_events_cache: ?*Element.PointerEventsCache) !void {
fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_name: ?[]const u8, visitor: anytype, index: usize, listener_targets: interactive.ListenerTargetMap, visibility_cache: ?*Element.VisibilityCache, pointer_events_cache: ?*Element.PointerEventsCache, current_depth: u32) !void {
if (current_depth > self.max_depth) return;
// 1. Skip non-content nodes
if (node.is(Element)) |el| {
const tag = el.getTag();
@@ -178,7 +182,23 @@ fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_nam
};
var should_visit = true;
if (self.prune) {
if (self.interactive_only) {
var keep = false;
if (interactive.isInteractiveRole(role)) {
keep = true;
} else if (interactive.isContentRole(role)) {
if (name != null and name.?.len > 0) {
keep = true;
}
} else if (std.mem.eql(u8, role, "RootWebArea")) {
keep = true;
} else if (is_interactive) {
keep = true;
}
if (!keep) {
should_visit = false;
}
} else if (self.prune) {
if (structural and !is_interactive and !has_explicit_label) {
should_visit = false;
}
@@ -217,7 +237,7 @@ fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_nam
}
gop.value_ptr.* += 1;
try self.walk(child, xpath_buffer, name, visitor, gop.value_ptr.*, listener_targets, visibility_cache, pointer_events_cache);
try self.walk(child, xpath_buffer, name, visitor, gop.value_ptr.*, listener_targets, visibility_cache, pointer_events_cache, current_depth + 1);
}
}
@@ -393,36 +413,45 @@ const TextVisitor = struct {
depth: usize,
pub fn visit(self: *TextVisitor, node: *Node, data: *NodeData) !bool {
// Format: " [12] link: Hacker News (value)"
for (0..(self.depth * 2)) |_| {
for (0..self.depth) |_| {
try self.writer.writeByte(' ');
}
try self.writer.print("[{d}] {s}: ", .{ data.id, data.role });
var name_to_print: ?[]const u8 = null;
if (data.name) |n| {
if (n.len > 0) {
try self.writer.writeAll(n);
name_to_print = n;
}
} else if (node.is(CData.Text)) |text_node| {
const trimmed = std.mem.trim(u8, text_node.getWholeText(), " \t\r\n");
if (trimmed.len > 0) {
try self.writer.writeAll(trimmed);
name_to_print = trimmed;
}
}
const is_text_only = std.mem.eql(u8, data.role, "StaticText") or std.mem.eql(u8, data.role, "none") or std.mem.eql(u8, data.role, "generic");
try self.writer.print("{d}", .{data.id});
if (!is_text_only) {
try self.writer.print(" {s}", .{data.role});
}
if (name_to_print) |n| {
try self.writer.print(" '{s}'", .{n});
}
if (data.value) |v| {
if (v.len > 0) {
try self.writer.print(" (value: {s})", .{v});
try self.writer.print(" value='{s}'", .{v});
}
}
if (data.options) |options| {
try self.writer.writeAll(" options: [");
try self.writer.writeAll(" options=[");
for (options, 0..) |opt, i| {
if (i > 0) try self.writer.writeAll(", ");
if (i > 0) try self.writer.writeAll(",");
try self.writer.print("'{s}'", .{opt.value});
if (opt.selected) {
try self.writer.writeAll(" (selected)");
try self.writer.writeAll("*");
}
}
try self.writer.writeAll("]\n");
@@ -452,3 +481,56 @@ const TextVisitor = struct {
}
}
};
const testing = @import("testing.zig");
test "SemanticTree backendDOMNodeId" {
var registry: CDPNode.Registry = .init(testing.allocator);
defer registry.deinit();
var page = try testing.pageTest("cdp/registry1.html");
defer testing.reset();
defer page._session.removePage();
const st: Self = .{
.dom_node = page.window._document.asNode(),
.registry = &registry,
.page = page,
.arena = testing.arena_allocator,
.prune = false,
.interactive_only = false,
.max_depth = std.math.maxInt(u32) - 1,
};
const json_str = try std.json.Stringify.valueAlloc(testing.allocator, st, .{});
defer testing.allocator.free(json_str);
try testing.expect(std.mem.indexOf(u8, json_str, "\"backendDOMNodeId\":") != null);
}
test "SemanticTree max_depth" {
var registry: CDPNode.Registry = .init(testing.allocator);
defer registry.deinit();
var page = try testing.pageTest("cdp/registry1.html");
defer testing.reset();
defer page._session.removePage();
const st: Self = .{
.dom_node = page.window._document.asNode(),
.registry = &registry,
.page = page,
.arena = testing.arena_allocator,
.prune = false,
.interactive_only = false,
.max_depth = 1,
};
var aw: std.Io.Writer.Allocating = .init(testing.allocator);
defer aw.deinit();
try st.textStringify(&aw.writer);
const text_str = aw.written();
try testing.expect(std.mem.indexOf(u8, text_str, "other") == null);
}