diff --git a/src/SemanticTree.zig b/src/SemanticTree.zig index b60b3ea6..e0cd0d8a 100644 --- a/src/SemanticTree.zig +++ b/src/SemanticTree.zig @@ -45,7 +45,8 @@ pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) error{WriteFailed}! log.err(.app, "listener map failed", .{ .err = err }); return error.WriteFailed; }; - self.walk(self.dom_node, &xpath_buffer, null, &visitor, 1, listener_targets) catch |err| { + var css_cache: Element.CssCache = .empty; + self.walk(self.dom_node, &xpath_buffer, null, &visitor, 1, listener_targets, &css_cache) catch |err| { log.err(.app, "semantic tree json dump failed", .{ .err = err }); return error.WriteFailed; }; @@ -58,7 +59,8 @@ pub fn textStringify(self: @This(), writer: *std.Io.Writer) error{WriteFailed}!v log.err(.app, "listener map failed", .{ .err = err }); return error.WriteFailed; }; - self.walk(self.dom_node, &xpath_buffer, null, &visitor, 1, listener_targets) catch |err| { + var css_cache: Element.CssCache = .empty; + self.walk(self.dom_node, &xpath_buffer, null, &visitor, 1, listener_targets, &css_cache) catch |err| { log.err(.app, "semantic tree text dump failed", .{ .err = err }); return error.WriteFailed; }; @@ -82,7 +84,7 @@ const NodeData = struct { node_name: []const u8, }; -fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_name: ?[]const u8, visitor: anytype, index: usize, listener_targets: interactive.ListenerTargetMap) !void { +fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_name: ?[]const u8, visitor: anytype, index: usize, listener_targets: interactive.ListenerTargetMap, css_cache: ?*Element.CssCache) !void { // 1. Skip non-content nodes if (node.is(Element)) |el| { const tag = el.getTag(); @@ -92,7 +94,7 @@ fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_nam if (tag == .datalist or tag == .option or tag == .optgroup) return; // Check visibility using the engine's checkVisibility which handles CSS display: none - if (!el.checkVisibility(self.page)) { + if (!el.checkVisibilityCached(self.page, css_cache)) { return; } @@ -133,7 +135,7 @@ fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_nam } if (el.is(Element.Html)) |html_el| { - if (interactive.classifyInteractivity(self.page, el, html_el, listener_targets) != null) { + if (interactive.classifyInteractivity(self.page, el, html_el, listener_targets, css_cache) != null) { is_interactive = true; } } @@ -213,7 +215,7 @@ fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_nam } gop.value_ptr.* += 1; - try self.walk(child, xpath_buffer, name, visitor, gop.value_ptr.*, listener_targets); + try self.walk(child, xpath_buffer, name, visitor, gop.value_ptr.*, listener_targets, css_cache); } } diff --git a/src/browser/interactive.zig b/src/browser/interactive.zig index fa45257f..487032e9 100644 --- a/src/browser/interactive.zig +++ b/src/browser/interactive.zig @@ -133,6 +133,8 @@ pub fn collectInteractiveElements( // so classify and getListenerTypes are both O(1) per element. const listener_targets = try buildListenerTargetMap(page, arena); + var css_cache: Element.CssCache = .empty; + var results: std.ArrayList(InteractiveElement) = .empty; var tw = TreeWalker.Full.init(root, .{}); @@ -146,7 +148,7 @@ pub fn collectInteractiveElements( else => {}, } - const itype = classifyInteractivity(page, el, html_el, listener_targets) orelse continue; + const itype = classifyInteractivity(page, el, html_el, listener_targets, &css_cache) orelse continue; const listener_types = getListenerTypes( el.asEventTarget(), @@ -214,8 +216,9 @@ pub fn classifyInteractivity( el: *Element, html_el: *Element.Html, listener_targets: ListenerTargetMap, + cache: ?*Element.CssCache, ) ?InteractivityType { - if (el.hasPointerEventsNone(page)) return null; + if (el.hasPointerEventsNoneCached(page, cache)) return null; // 1. Native interactive by tag switch (el.getTag()) { diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 9fa62280..3703c82d 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -1044,24 +1044,23 @@ pub fn parentElement(self: *Element) ?*Element { const CSSStyleRule = @import("css/CSSStyleRule.zig"); const StyleSheetList = @import("css/StyleSheetList.zig"); -pub fn hasPointerEventsNone(self: *Element, page: *Page) bool { - const doc_sheets = page.document.getStyleSheets(page) catch null; - var current: ?*Element = self; - while (current) |el| { - if (checkCssProperty(el, page, doc_sheets, "pointer-events", &[_][]const u8{"none"})) return true; - current = el.parentElement(); - } - return false; -} +pub const CssProperties = struct { + display_none: bool = false, + visibility_hidden: bool = false, + opacity_zero: bool = false, + pointer_events_none: bool = false, +}; -fn checkCssProperty(el: *Element, page: *Page, doc_sheets: ?*StyleSheetList, property_name: []const u8, target_values: []const []const u8) bool { - if (el.getOrCreateStyle(page) catch null) |style| { - const val = style.asCSSStyleDeclaration().getPropertyValue(property_name, page); - for (target_values) |target| { - if (std.mem.eql(u8, val, target)) return true; - } +pub const CssCache = std.AutoHashMapUnmanaged(*Element, CssProperties); + +pub fn getCssProperties(el: *Element, page: *Page, doc_sheets: ?*StyleSheetList, cache: ?*CssCache) CssProperties { + if (cache) |c| { + if (c.get(el)) |props| return props; } + var props = CssProperties{}; + + // Check stylesheets first if (doc_sheets) |sheets| { for (0..sheets.length()) |i| { const sheet = sheets.item(i) orelse continue; @@ -1072,33 +1071,111 @@ fn checkCssProperty(el: *Element, page: *Page, doc_sheets: ?*StyleSheetList, pro const selector = style_rule.getSelectorText(); if (el.matches(selector, page) catch false) { const style = (style_rule.getStyle(page) catch continue).asCSSStyleDeclaration(); - const val = style.getPropertyValue(property_name, page); - for (target_values) |target| { - if (std.mem.eql(u8, val, target)) return true; + + const display = style.getPropertyValue("display", page); + if (std.mem.eql(u8, display, "none")) { + props.display_none = true; + } else if (display.len > 0) { + props.display_none = false; + } + + const visibility = style.getPropertyValue("visibility", page); + if (std.mem.eql(u8, visibility, "hidden") or std.mem.eql(u8, visibility, "collapse")) { + props.visibility_hidden = true; + } else if (visibility.len > 0) { + props.visibility_hidden = false; + } + + const opacity = style.getPropertyValue("opacity", page); + if (std.mem.eql(u8, opacity, "0")) { + props.opacity_zero = true; + } else if (opacity.len > 0) { + props.opacity_zero = false; + } + + const pointer_events = style.getPropertyValue("pointer-events", page); + if (std.mem.eql(u8, pointer_events, "none")) { + props.pointer_events_none = true; + } else if (pointer_events.len > 0) { + props.pointer_events_none = false; } } } } } } + + // Check inline styles overrides + if (el.getOrCreateStyle(page) catch null) |style| { + const decl = style.asCSSStyleDeclaration(); + const display = decl.getPropertyValue("display", page); + if (std.mem.eql(u8, display, "none")) { + props.display_none = true; + } else if (display.len > 0) { + props.display_none = false; + } + + const visibility = decl.getPropertyValue("visibility", page); + if (std.mem.eql(u8, visibility, "hidden") or std.mem.eql(u8, visibility, "collapse")) { + props.visibility_hidden = true; + } else if (visibility.len > 0) { + props.visibility_hidden = false; + } + + const opacity = decl.getPropertyValue("opacity", page); + if (std.mem.eql(u8, opacity, "0")) { + props.opacity_zero = true; + } else if (opacity.len > 0) { + props.opacity_zero = false; + } + + const pointer_events = decl.getPropertyValue("pointer-events", page); + if (std.mem.eql(u8, pointer_events, "none")) { + props.pointer_events_none = true; + } else if (pointer_events.len > 0) { + props.pointer_events_none = false; + } + } + + if (cache) |c| { + c.put(page.call_arena, el, props) catch {}; + } + + return props; +} + +pub fn hasPointerEventsNoneCached(self: *Element, page: *Page, cache: ?*CssCache) bool { + const doc_sheets = page.document.getStyleSheets(page) catch null; + var current: ?*Element = self; + while (current) |el| { + const props = getCssProperties(el, page, doc_sheets, cache); + if (props.pointer_events_none) return true; + current = el.parentElement(); + } return false; } -pub fn checkVisibility(self: *Element, page: *Page) bool { +pub fn hasPointerEventsNone(self: *Element, page: *Page) bool { + return self.hasPointerEventsNoneCached(page, null); +} + +pub fn checkVisibilityCached(self: *Element, page: *Page, cache: ?*CssCache) bool { const doc_sheets = page.document.getStyleSheets(page) catch null; var current: ?*Element = self; while (current) |el| { - if (checkCssProperty(el, page, doc_sheets, "display", &[_][]const u8{"none"})) return false; - if (checkCssProperty(el, page, doc_sheets, "visibility", &[_][]const u8{ "hidden", "collapse" })) return false; - if (checkCssProperty(el, page, doc_sheets, "opacity", &[_][]const u8{"0"})) return false; - + const props = getCssProperties(el, page, doc_sheets, cache); + if (props.display_none or props.visibility_hidden or props.opacity_zero) return false; current = el.parentElement(); } return true; } +pub fn checkVisibility(self: *Element, page: *Page) bool { + return self.checkVisibilityCached(page, null); +} + fn getElementDimensions(self: *Element, page: *Page) struct { width: f64, height: f64 } { var width: f64 = 5.0; var height: f64 = 5.0;