mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 12:44:43 +00:00
SemanticTree: Implement compound component metadata
This commit is contained in:
@@ -53,12 +53,19 @@ pub fn textStringify(self: @This(), writer: *std.Io.Writer) error{WriteFailed}!v
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const OptionData = struct {
|
||||||
|
value: []const u8,
|
||||||
|
text: []const u8,
|
||||||
|
selected: bool,
|
||||||
|
};
|
||||||
|
|
||||||
const NodeData = struct {
|
const NodeData = struct {
|
||||||
id: u32,
|
id: u32,
|
||||||
axn: AXNode,
|
axn: AXNode,
|
||||||
role: []const u8,
|
role: []const u8,
|
||||||
name: ?[]const u8,
|
name: ?[]const u8,
|
||||||
value: ?[]const u8,
|
value: ?[]const u8,
|
||||||
|
options: ?[]OptionData = null,
|
||||||
xpath: []const u8,
|
xpath: []const u8,
|
||||||
is_interactive: bool,
|
is_interactive: bool,
|
||||||
node_name: []const u8,
|
node_name: []const u8,
|
||||||
@@ -70,6 +77,9 @@ fn walk(self: @This(), node: *Node, parent_xpath: []const u8, visitor: anytype)
|
|||||||
const tag = el.getTag();
|
const tag = el.getTag();
|
||||||
if (tag.isMetadata() or tag == .svg) return;
|
if (tag.isMetadata() or tag == .svg) return;
|
||||||
|
|
||||||
|
// We handle options/optgroups natively inside their parents, skip them in the general walk
|
||||||
|
if (tag == .datalist or tag == .option or tag == .optgroup) return;
|
||||||
|
|
||||||
// CSS display: none visibility check (inline style only for now)
|
// CSS display: none visibility check (inline style only for now)
|
||||||
if (el.getAttributeSafe(comptime lp.String.wrap("style"))) |style| {
|
if (el.getAttributeSafe(comptime lp.String.wrap("style"))) |style| {
|
||||||
if (std.mem.indexOf(u8, style, "display: none") != null or
|
if (std.mem.indexOf(u8, style, "display: none") != null or
|
||||||
@@ -98,6 +108,7 @@ fn walk(self: @This(), node: *Node, parent_xpath: []const u8, visitor: anytype)
|
|||||||
|
|
||||||
var is_interactive = false;
|
var is_interactive = false;
|
||||||
var value: ?[]const u8 = null;
|
var value: ?[]const u8 = null;
|
||||||
|
var options: ?[]OptionData = null;
|
||||||
var node_name: []const u8 = "text";
|
var node_name: []const u8 = "text";
|
||||||
|
|
||||||
if (node.is(Element)) |el| {
|
if (node.is(Element)) |el| {
|
||||||
@@ -131,10 +142,14 @@ fn walk(self: @This(), node: *Node, parent_xpath: []const u8, visitor: anytype)
|
|||||||
|
|
||||||
if (el.is(Element.Html.Input)) |input| {
|
if (el.is(Element.Html.Input)) |input| {
|
||||||
value = input.getValue();
|
value = input.getValue();
|
||||||
|
if (el.getAttributeSafe(comptime lp.String.wrap("list"))) |list_id| {
|
||||||
|
options = try extractDataListOptions(list_id, self.page, self.arena);
|
||||||
|
}
|
||||||
} else if (el.is(Element.Html.TextArea)) |textarea| {
|
} else if (el.is(Element.Html.TextArea)) |textarea| {
|
||||||
value = textarea.getValue();
|
value = textarea.getValue();
|
||||||
} else if (el.is(Element.Html.Select)) |select| {
|
} else if (el.is(Element.Html.Select)) |select| {
|
||||||
value = select.getValue(self.page);
|
value = select.getValue(self.page);
|
||||||
|
options = try extractSelectOptions(el.asNode(), self.page, self.arena);
|
||||||
}
|
}
|
||||||
} else if (node._type == .document or node._type == .document_fragment) {
|
} else if (node._type == .document or node._type == .document_fragment) {
|
||||||
node_name = "root";
|
node_name = "root";
|
||||||
@@ -151,6 +166,7 @@ fn walk(self: @This(), node: *Node, parent_xpath: []const u8, visitor: anytype)
|
|||||||
.role = role,
|
.role = role,
|
||||||
.name = name,
|
.name = name,
|
||||||
.value = value,
|
.value = value,
|
||||||
|
.options = options,
|
||||||
.xpath = xpath,
|
.xpath = xpath,
|
||||||
.is_interactive = is_interactive,
|
.is_interactive = is_interactive,
|
||||||
.node_name = node_name,
|
.node_name = node_name,
|
||||||
@@ -178,13 +194,22 @@ fn walk(self: @This(), node: *Node, parent_xpath: []const u8, visitor: anytype)
|
|||||||
}
|
}
|
||||||
|
|
||||||
var did_visit = false;
|
var did_visit = false;
|
||||||
|
var should_walk_children = true;
|
||||||
if (should_visit) {
|
if (should_visit) {
|
||||||
did_visit = try visitor.visit(node, &data);
|
should_walk_children = try visitor.visit(node, &data);
|
||||||
|
did_visit = true; // Always true if should_visit was true, because visit() executed and opened structures
|
||||||
|
} else {
|
||||||
|
// If we skip the node, we must NOT tell the visitor to close it later
|
||||||
|
did_visit = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var it = node.childrenIterator();
|
if (should_walk_children) {
|
||||||
while (it.next()) |child| {
|
// If we are printing this node normally OR skipping it and unrolling its children,
|
||||||
try self.walk(child, xpath, visitor);
|
// we walk the children iterator.
|
||||||
|
var it = node.childrenIterator();
|
||||||
|
while (it.next()) |child| {
|
||||||
|
try self.walk(child, xpath, visitor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (did_visit) {
|
if (did_visit) {
|
||||||
@@ -192,6 +217,45 @@ fn walk(self: @This(), node: *Node, parent_xpath: []const u8, visitor: anytype)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn extractSelectOptions(node: *Node, page: *Page, arena: std.mem.Allocator) ![]OptionData {
|
||||||
|
var options = std.ArrayListUnmanaged(OptionData){};
|
||||||
|
var it = node.childrenIterator();
|
||||||
|
while (it.next()) |child| {
|
||||||
|
if (child.is(Element)) |el| {
|
||||||
|
if (el.getTag() == .option) {
|
||||||
|
if (el.is(Element.Html.Option)) |opt| {
|
||||||
|
const text = opt.getText();
|
||||||
|
const value = opt.getValue(page);
|
||||||
|
const selected = opt.getSelected();
|
||||||
|
try options.append(arena, .{ .text = text, .value = value, .selected = selected });
|
||||||
|
}
|
||||||
|
} else if (el.getTag() == .optgroup) {
|
||||||
|
var group_it = child.childrenIterator();
|
||||||
|
while (group_it.next()) |group_child| {
|
||||||
|
if (group_child.is(Element.Html.Option)) |opt| {
|
||||||
|
const text = opt.getText();
|
||||||
|
const value = opt.getValue(page);
|
||||||
|
const selected = opt.getSelected();
|
||||||
|
try options.append(arena, .{ .text = text, .value = value, .selected = selected });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return options.toOwnedSlice(arena);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extractDataListOptions(list_id: []const u8, page: *Page, arena: std.mem.Allocator) !?[]OptionData {
|
||||||
|
const doc = page.document.asNode();
|
||||||
|
const datalist = @import("browser/webapi/selector/Selector.zig").querySelector(doc, try std.fmt.allocPrint(arena, "#{s}", .{list_id}), page) catch null;
|
||||||
|
if (datalist) |dl| {
|
||||||
|
if (dl.getTag() == .datalist) {
|
||||||
|
return try extractSelectOptions(dl.asNode(), page, arena);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
fn getXPathSegment(self: @This(), node: *Node) ![]const u8 {
|
fn getXPathSegment(self: @This(), node: *Node) ![]const u8 {
|
||||||
if (node.is(Element)) |el| {
|
if (node.is(Element)) |el| {
|
||||||
const tag = el.getTagNameLower();
|
const tag = el.getTagNameLower();
|
||||||
@@ -276,6 +340,22 @@ const JsonVisitor = struct {
|
|||||||
}
|
}
|
||||||
try self.jw.endObject();
|
try self.jw.endObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.options) |options| {
|
||||||
|
try self.jw.objectField("options");
|
||||||
|
try self.jw.beginArray();
|
||||||
|
for (options) |opt| {
|
||||||
|
try self.jw.beginObject();
|
||||||
|
try self.jw.objectField("value");
|
||||||
|
try self.jw.write(opt.value);
|
||||||
|
try self.jw.objectField("text");
|
||||||
|
try self.jw.write(opt.text);
|
||||||
|
try self.jw.objectField("selected");
|
||||||
|
try self.jw.write(opt.selected);
|
||||||
|
try self.jw.endObject();
|
||||||
|
}
|
||||||
|
try self.jw.endArray();
|
||||||
|
}
|
||||||
} else if (node.is(CData.Text) != null) {
|
} else if (node.is(CData.Text) != null) {
|
||||||
const text_node = node.is(CData.Text).?;
|
const text_node = node.is(CData.Text).?;
|
||||||
try self.jw.objectField("nodeType");
|
try self.jw.objectField("nodeType");
|
||||||
@@ -289,6 +369,12 @@ const JsonVisitor = struct {
|
|||||||
|
|
||||||
try self.jw.objectField("children");
|
try self.jw.objectField("children");
|
||||||
try self.jw.beginArray();
|
try self.jw.beginArray();
|
||||||
|
|
||||||
|
if (data.options != null) {
|
||||||
|
// Signal to not walk children, as we handled them natively
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,12 +420,28 @@ const TextVisitor = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.options) |options| {
|
||||||
|
try self.writer.writeAll(" options: [");
|
||||||
|
for (options, 0..) |opt, i| {
|
||||||
|
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("]\n");
|
||||||
|
self.depth += 1;
|
||||||
|
return false; // Native handling complete, do not walk children
|
||||||
|
}
|
||||||
|
|
||||||
try self.writer.writeByte('\n');
|
try self.writer.writeByte('\n');
|
||||||
self.depth += 1;
|
self.depth += 1;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn leave(self: *TextVisitor) !void {
|
pub fn leave(self: *TextVisitor) !void {
|
||||||
self.depth -= 1;
|
if (self.depth > 0) {
|
||||||
|
self.depth -= 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user