diff --git a/src/SemanticTree.zig b/src/SemanticTree.zig index ad1e92581..aab3576ce 100644 --- a/src/SemanticTree.zig +++ b/src/SemanticTree.zig @@ -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 = ®istry, - .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(), ®istry, 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 = ®istry, - .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(), ®istry, frame, .{ .prune = false, .max_depth = 1 }); var aw: std.Io.Writer.Allocating = .init(testing.allocator); defer aw.deinit(); diff --git a/src/browser/tools.zig b/src/browser/tools.zig index 5f90c164a..7a938a852 100644 --- a/src/browser/tools.zig +++ b/src/browser/tools.zig @@ -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); } diff --git a/src/lightpanda.zig b/src/lightpanda.zig index c26d734f3..e51039aff 100644 --- a/src/lightpanda.zig +++ b/src/lightpanda.zig @@ -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 = ®istry, - .frame = frame, - .arena = frame.call_arena, + const st: SemanticTree = try .init(frame.call_arena, state.root, ®istry, frame, .{ .prune = (mode == .semantic_tree_text), - }; + }); if (mode == .semantic_tree) { try std.json.Stringify.value(st, .{}, writer); diff --git a/src/server/cdp/domains/lp.zig b/src/server/cdp/domains/lp.zig index adb3903a3..e61a7bae7 100644 --- a/src/server/cdp/domains/lp.zig +++ b/src/server/cdp/domains/lp.zig @@ -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,