SemanticTree: improve accessibility tree and name calculation

- Add more structural roles (banner, navigation, main, list, etc.).
- Implement fallback for accessible names (SVG titles, image alt text).
- Skip children for leaf-like semantic nodes to reduce redundancy.
- Disable pruning in the default semantic tree view.
This commit is contained in:
Adrià Arrufat
2026-03-09 21:04:47 +09:00
parent 61cba3f6eb
commit 85ebbe8759
3 changed files with 62 additions and 7 deletions

View File

@@ -385,9 +385,17 @@ const JsonVisitor = struct {
}; };
fn isStructuralRole(role: []const u8) bool { fn isStructuralRole(role: []const u8) bool {
// zig fmt: off
return std.mem.eql(u8, role, "none") or return std.mem.eql(u8, role, "none") or
std.mem.eql(u8, role, "generic") or std.mem.eql(u8, role, "generic") or
std.mem.eql(u8, role, "InlineTextBox"); std.mem.eql(u8, role, "InlineTextBox") or
std.mem.eql(u8, role, "banner") or
std.mem.eql(u8, role, "navigation") or
std.mem.eql(u8, role, "main") or
std.mem.eql(u8, role, "list") or
std.mem.eql(u8, role, "listitem") or
std.mem.eql(u8, role, "region");
// zig fmt: on
} }
const TextVisitor = struct { const TextVisitor = struct {
@@ -436,6 +444,17 @@ const TextVisitor = struct {
try self.writer.writeByte('\n'); try self.writer.writeByte('\n');
self.depth += 1; self.depth += 1;
// If this is a leaf-like semantic node and we already have a name,
// skip children to avoid redundant StaticText or noise.
const is_leaf_semantic = std.mem.eql(u8, data.role, "link") or
std.mem.eql(u8, data.role, "button") or
std.mem.eql(u8, data.role, "heading") or
std.mem.eql(u8, data.role, "code");
if (is_leaf_semantic and data.name != null and data.name.?.len > 0) {
return false;
}
return true; return true;
} }

View File

@@ -888,10 +888,12 @@ fn writeName(axnode: AXNode, w: anytype, page: *Page) !?AXSource {
=> {}, => {},
else => { else => {
// write text content if exists. // write text content if exists.
var buf = std.Io.Writer.Allocating.init(page.call_arena); var buf: std.Io.Writer.Allocating = .init(page.call_arena);
try el.getInnerText(&buf.writer); try writeAccessibleNameFallback(node, &buf.writer, page);
try writeString(buf.written(), w); if (buf.written().len > 0) {
return .contents; try writeString(buf.written(), w);
return .contents;
}
}, },
} }
@@ -915,6 +917,40 @@ fn writeName(axnode: AXNode, w: anytype, page: *Page) !?AXSource {
}; };
} }
fn writeAccessibleNameFallback(node: *DOMNode, writer: *std.Io.Writer, page: *Page) !void {
var it = node.childrenIterator();
while (it.next()) |child| {
switch (child._type) {
.cdata => |cd| switch (cd._type) {
.text => |*text| try writer.writeAll(text.getWholeText()),
else => {},
},
.element => |el| {
if (el.getTag() == .img) {
if (el.getAttributeSafe(.wrap("alt"))) |alt| {
try writer.writeAll(alt);
try writer.writeByte(' ');
}
} else if (el.getTag() == .svg) {
// Try to find a <title> inside SVG
var sit = child.childrenIterator();
while (sit.next()) |s_child| {
if (s_child.is(DOMNode.Element)) |s_el| {
if (std.mem.eql(u8, s_el.getTagNameLower(), "title")) {
try writeAccessibleNameFallback(s_child, writer, page);
try writer.writeByte(' ');
}
}
}
} else {
try writeAccessibleNameFallback(child, writer, page);
}
},
else => {},
}
}
}
fn isHidden(elt: *DOMNode.Element) bool { fn isHidden(elt: *DOMNode.Element) bool {
if (elt.getAttributeSafe(comptime .wrap("aria-hidden"))) |value| { if (elt.getAttributeSafe(comptime .wrap("aria-hidden"))) |value| {
if (std.mem.eql(u8, value, "true")) { if (std.mem.eql(u8, value, "true")) {

View File

@@ -113,12 +113,12 @@ pub fn fetch(app: *App, url: [:0]const u8, opts: FetchOpts) !void {
var registry = CDPNode.Registry.init(app.allocator); var registry = CDPNode.Registry.init(app.allocator);
defer registry.deinit(); defer registry.deinit();
const st = SemanticTree{ const st: SemanticTree = .{
.dom_node = page.window._document.asNode(), .dom_node = page.window._document.asNode(),
.registry = &registry, .registry = &registry,
.page = page, .page = page,
.arena = page.call_arena, .arena = page.call_arena,
.prune = true, .prune = false,
}; };
if (mode == .semantic_tree) { if (mode == .semantic_tree) {