internal: Always correct SemanticTree context

Applies the frame-ownership pass to SemanticTree, copying what we did for
StyleManager (1). SemanticTree doesn't visit iframes, so the frame of the root
is the frame/frame._style_manager we need to target for all visited nodes.

Like #3536, it's up to the callers to (a) get the correct frame and (b) decide
what to do on a frameless-node.

(1) https://github.com/lightpanda-io/browser/pull/3536
This commit is contained in:
Karl Seguin committed 2026-09-17 12:13:53 +08:00
1 parent 32a43c1c97
commit 13638ff50b
7 files changed
+68 -47

No files matched your search

+12 -39
View File
@@ -38,13 +38,14 @@ const Self = @This();
dom_node: *Node,
registry: *NodeRegistry,
frame: *Frame,
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,
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| {
@@ -56,7 +57,6 @@ pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) error{WriteFailed}!
.xpath_buffer = &xpath_buffer,
.listener_targets = listener_targets,
.label_index = &label_index,
.owner_frame = self.dom_node.ownerFrame(self.frame) orelse self.frame,
};
self.walk(&ctx, self.dom_node, null, &visitor, 1, 0) catch |err| {
log.err(.app, "semantic tree json dump failed", .{ .err = err });
@@ -65,6 +65,7 @@ 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| {
@@ -76,7 +77,6 @@ pub fn textStringify(self: @This(), writer: *std.Io.Writer) error{WriteFailed}!v
.xpath_buffer = &xpath_buffer,
.listener_targets = listener_targets,
.label_index = &label_index,
.owner_frame = self.dom_node.ownerFrame(self.frame) orelse self.frame,
};
self.walk(&ctx, self.dom_node, null, &visitor, 1, 0) catch |err| {
log.err(.app, "semantic tree text dump failed", .{ .err = err });
@@ -108,7 +108,6 @@ const WalkContext = struct {
xpath_buffer: *std.ArrayList(u8),
listener_targets: interactive.ListenerTargetMap,
label_index: *Label.LabelByForIndex,
owner_frame: *Frame, // node's own frame, not the caller's (the dumped frame when the node's document has none)
};
fn walk(
@@ -132,7 +131,7 @@ fn walk(
// Hidden subtrees are never entered, so below the root only the
// element's own display matters.
const style_manager = &ctx.owner_frame._style_manager;
const style_manager = &self.frame._style_manager;
const hidden = if (current_depth == 0)
style_manager.isHidden(el, .{})
else
@@ -658,6 +657,7 @@ pub fn getNodeDetails(
registry: *NodeRegistry,
frame: *Frame,
) !NodeDetails {
assertOwns(frame, node);
const cdp_node = try registry.register(node);
const axn = AXNode.fromNode(node);
const role = try axn.getRole();
@@ -733,6 +733,13 @@ 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" {
@@ -759,40 +766,6 @@ test "SemanticTree backendDOMNodeId" {
try testing.expect(std.mem.indexOf(u8, json_str, "\"backendDOMNodeId\":") != null);
}
test "SemanticTree: styles come from the node's own frame" {
var registry: NodeRegistry = .init(testing.allocator);
defer registry.deinit();
// The caller's frame hides #inner; the frame that actually owns the walked
// subtree does not. A backendNodeId lookup can hand us a node from another
// frame, so the walk must not use the caller's stylesheets.
var page_a = try testing.pageTest("cdp/semantic_tree_frame_a.html", .{});
defer page_a.close();
var page_b = try testing.pageTest("cdp/semantic_tree_frame_b.html", .{});
defer page_b.close();
const frame_a = page_a.frame().?;
const frame_b = page_b.frame().?;
const target = (try frame_b.window._document.querySelector(.wrap("#target"), frame_b)).?.asNode();
const st: Self = .{
.dom_node = target,
.registry = &registry,
.frame = frame_a,
.arena = testing.arena_allocator,
.prune = false,
.interactive_only = false,
.max_depth = std.math.maxInt(u32) - 1,
};
var aw: std.Io.Writer.Allocating = .init(testing.allocator);
defer aw.deinit();
try st.textStringify(&aw.writer);
try testing.expect(std.mem.indexOf(u8, aw.written(), "inner-b") != null);
}
test "SemanticTree max_depth" {
var registry: NodeRegistry = .init(testing.allocator);
defer registry.deinit();