Merge pull request #3553 from lightpanda-io/semantic-tree-init

SemanticTree: resolve the owner frame once in init
This commit is contained in:
Karl Seguin authored and GitHub committed 2026-09-17 18:29:46 +08:00
commit 243ff0b8be
4 files changed
+43 -66

No files matched your search

+33 -38
View File
@@ -40,12 +40,33 @@ dom_node: *Node,
registry: *NodeRegistry,
frame: *Frame, // we never visit iframes, every node we visit is in the same frame as dom_node
arena: std.mem.Allocator,
prune: bool = true,
interactive_only: bool = false,
max_depth: u32 = std.math.maxInt(u32) - 1,
prune: bool,
interactive_only: bool,
max_depth: u32,
pub const Opts = struct {
prune: bool = true,
interactive_only: bool = false,
max_depth: u32 = std.math.maxInt(u32) - 1,
};
/// `frame` only seeds the owner lookup; the tree is walked with the frame
/// that owns `node`. A node whose document has no frame (DOMParser, XHR, or
/// a frame that has since navigated away) has no styles or layout to
/// describe.
pub fn init(arena: std.mem.Allocator, node: *Node, registry: *NodeRegistry, frame: *Frame, opts: Opts) error{FramelessNode}!Self {
return .{
.dom_node = node,
.registry = registry,
.frame = node.ownerFrame(frame) orelse return error.FramelessNode,
.arena = arena,
.prune = opts.prune,
.interactive_only = opts.interactive_only,
.max_depth = opts.max_depth,
};
}
pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) error{WriteFailed}!void {
assertOwns(self.frame, self.dom_node);
var visitor = JsonVisitor{ .jw = jw, .tree = self };
var xpath_buffer: std.ArrayList(u8) = .empty;
const listener_targets = interactive.buildListenerTargetMap(self.frame, self.arena) catch |err| {
@@ -65,7 +86,6 @@ pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) error{WriteFailed}!
}
pub fn textStringify(self: @This(), writer: *std.Io.Writer) error{WriteFailed}!void {
assertOwns(self.frame, self.dom_node);
var visitor = TextVisitor{ .writer = writer, .tree = self, .depth = 0 };
var xpath_buffer: std.ArrayList(u8) = .empty;
const listener_targets = interactive.buildListenerTargetMap(self.frame, self.arena) catch |err| {
@@ -651,14 +671,12 @@ const NodeDetails = struct {
}
};
pub fn getNodeDetails(
arena: std.mem.Allocator,
node: *Node,
registry: *NodeRegistry,
frame: *Frame,
) !NodeDetails {
assertOwns(frame, node);
const cdp_node = try registry.register(node);
pub fn nodeDetails(self: Self) !NodeDetails {
const arena = self.arena;
const node = self.dom_node;
const frame = self.frame;
const cdp_node = try self.registry.register(node);
const axn = AXNode.fromNode(node);
const role = try axn.getRole();
var labels: Label.LabelByForIndex = .{};
@@ -733,13 +751,6 @@ pub fn getNodeDetails(
};
}
fn assertOwns(frame: *const Frame, node: *const Node) void {
if (comptime lp.IS_DEBUG == false) {
return;
}
std.debug.assert(node.ownerFrame(frame) == frame);
}
const testing = @import("testing.zig");
test "SemanticTree backendDOMNodeId" {
@@ -750,15 +761,7 @@ test "SemanticTree backendDOMNodeId" {
defer page.close();
const frame = page.frame().?;
const st: Self = .{
.dom_node = frame.window._document.asNode(),
.registry = &registry,
.frame = frame,
.arena = testing.arena_allocator,
.prune = false,
.interactive_only = false,
.max_depth = std.math.maxInt(u32) - 1,
};
const st: Self = try .init(testing.arena_allocator, frame.window._document.asNode(), &registry, frame, .{ .prune = false });
const json_str = try std.json.Stringify.valueAlloc(testing.allocator, st, .{});
defer testing.allocator.free(json_str);
@@ -774,15 +777,7 @@ test "SemanticTree max_depth" {
defer page.close();
const frame = page.frame().?;
const st: Self = .{
.dom_node = frame.window._document.asNode(),
.registry = &registry,
.frame = frame,
.arena = testing.arena_allocator,
.prune = false,
.interactive_only = false,
.max_depth = 1,
};
const st: Self = try .init(testing.arena_allocator, frame.window._document.asNode(), &registry, frame, .{ .prune = false, .max_depth = 1 });
var aw: std.Io.Writer.Allocating = .init(testing.allocator);
defer aw.deinit();
+4 -12
View File
@@ -1430,16 +1430,9 @@ fn execTree(arena: std.mem.Allocator, session: *lp.Session, registry: *NodeRegis
const page = try ensurePage(session, registry, args.url, args.timeout);
const root_node = (try resolveOptionalNode(registry, args.backendNodeId)) orelse page.document.asNode();
const frame = root_node.ownerFrame(page) orelse return ToolError.NodeNotFound;
const st = lp.SemanticTree{
.dom_node = root_node,
.registry = registry,
.frame = frame,
.arena = arena,
.prune = true,
const st = lp.SemanticTree.init(arena, root_node, registry, page, .{
.max_depth = args.maxDepth orelse std.math.maxInt(u32) - 1,
};
}) catch return ToolError.NodeNotFound;
var aw: std.Io.Writer.Allocating = .init(arena);
st.textStringify(&aw.writer) catch return ToolError.InternalError;
@@ -1454,9 +1447,8 @@ fn execNodeDetails(arena: std.mem.Allocator, session: *lp.Session, registry: *No
const node = registry.lookup_by_id.get(args.backendNodeId) orelse
return ToolError.NodeNotFound;
const frame = node.dom.ownerFrame(page) orelse return ToolError.NodeNotFound;
const details = lp.SemanticTree.getNodeDetails(arena, node.dom, registry, frame) catch
return ToolError.InternalError;
const st = lp.SemanticTree.init(arena, node.dom, registry, page, .{}) catch return ToolError.NodeNotFound;
const details = st.nodeDetails() catch return ToolError.InternalError;
return renderJson(arena, &details);
}
+2 -6
View File
@@ -495,13 +495,9 @@ fn dumpContent(app: *App, mode: Config.DumpFormat, opts: FetchOpts, frame: *Fram
var registry = NodeRegistry.init(app.allocator);
defer registry.deinit();
const st: SemanticTree = .{
.dom_node = state.root,
.registry = &registry,
.frame = frame,
.arena = frame.call_arena,
const st: SemanticTree = try .init(frame.call_arena, state.root, &registry, frame, .{
.prune = (mode == .semantic_tree_text),
};
});
if (mode == .semantic_tree) {
try std.json.Stringify.value(st, .{}, writer);
+4 -10
View File
@@ -133,17 +133,12 @@ fn getSemanticTree(cmd: anytype) !void {
(bc.node_registry.lookup_by_id.get(nodeId) orelse return error.InvalidNodeId).dom
else
root.document.asNode();
const frame = dom_node.ownerFrame(root) orelse return error.InvalidNodeId;
var st = SemanticTree{
.dom_node = dom_node,
.registry = &bc.node_registry,
.frame = frame,
.arena = cmd.arena,
const st = SemanticTree.init(cmd.arena, dom_node, &bc.node_registry, root, .{
.prune = params.prune orelse true,
.interactive_only = params.interactiveOnly orelse false,
.max_depth = params.maxDepth orelse std.math.maxInt(u32) - 1,
};
}) catch return error.InvalidNodeId;
if (params.format) |format| {
if (format == .text) {
@@ -274,9 +269,8 @@ fn getNodeDetails(cmd: anytype) !void {
const root = bc.mainFrame() orelse return error.FrameNotLoaded;
const node = (bc.node_registry.lookup_by_id.get(params.backendNodeId) orelse return error.InvalidNodeId).dom;
const frame = node.ownerFrame(root) orelse return error.InvalidNodeId;
const details = SemanticTree.getNodeDetails(cmd.arena, node, &bc.node_registry, frame) catch return error.InternalError;
const st = SemanticTree.init(cmd.arena, node, &bc.node_registry, root, .{}) catch return error.InvalidNodeId;
const details = st.nodeDetails() catch return error.InternalError;
return cmd.sendResult(.{
.nodeDetails = details,