mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-28 15:40:04 +00:00
Merge pull request #1797 from lightpanda-io/css-improvements
Implement CSSOM and Enhanced Visibility Filtering
This commit is contained in:
@@ -155,6 +155,11 @@ pub fn reset(_: *const ArenaPool, allocator: Allocator, retain: usize) void {
|
||||
_ = arena.reset(.{ .retain_with_limit = retain });
|
||||
}
|
||||
|
||||
pub fn resetRetain(_: *const ArenaPool, allocator: Allocator) void {
|
||||
const arena: *std.heap.ArenaAllocator = @ptrCast(@alignCast(allocator.ptr));
|
||||
_ = arena.reset(.retain_capacity);
|
||||
}
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
test "arena pool - basic acquire and use" {
|
||||
|
||||
@@ -47,7 +47,15 @@ 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, 0) catch |err| {
|
||||
var visibility_cache: Element.VisibilityCache = .empty;
|
||||
var pointer_events_cache: Element.PointerEventsCache = .empty;
|
||||
var ctx: WalkContext = .{
|
||||
.xpath_buffer = &xpath_buffer,
|
||||
.listener_targets = listener_targets,
|
||||
.visibility_cache = &visibility_cache,
|
||||
.pointer_events_cache = &pointer_events_cache,
|
||||
};
|
||||
self.walk(&ctx, self.dom_node, null, &visitor, 1, 0) catch |err| {
|
||||
log.err(.app, "semantic tree json dump failed", .{ .err = err });
|
||||
return error.WriteFailed;
|
||||
};
|
||||
@@ -60,7 +68,15 @@ 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, 0) catch |err| {
|
||||
var visibility_cache: Element.VisibilityCache = .empty;
|
||||
var pointer_events_cache: Element.PointerEventsCache = .empty;
|
||||
var ctx: WalkContext = .{
|
||||
.xpath_buffer = &xpath_buffer,
|
||||
.listener_targets = listener_targets,
|
||||
.visibility_cache = &visibility_cache,
|
||||
.pointer_events_cache = &pointer_events_cache,
|
||||
};
|
||||
self.walk(&ctx, self.dom_node, null, &visitor, 1, 0) catch |err| {
|
||||
log.err(.app, "semantic tree text dump failed", .{ .err = err });
|
||||
return error.WriteFailed;
|
||||
};
|
||||
@@ -84,7 +100,22 @@ 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, current_depth: u32) !void {
|
||||
const WalkContext = struct {
|
||||
xpath_buffer: *std.ArrayList(u8),
|
||||
listener_targets: interactive.ListenerTargetMap,
|
||||
visibility_cache: *Element.VisibilityCache,
|
||||
pointer_events_cache: *Element.PointerEventsCache,
|
||||
};
|
||||
|
||||
fn walk(
|
||||
self: @This(),
|
||||
ctx: *WalkContext,
|
||||
node: *Node,
|
||||
parent_name: ?[]const u8,
|
||||
visitor: anytype,
|
||||
index: usize,
|
||||
current_depth: u32,
|
||||
) !void {
|
||||
if (current_depth > self.max_depth) return;
|
||||
|
||||
// 1. Skip non-content nodes
|
||||
@@ -96,7 +127,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(ctx.visibility_cache, self.page)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -137,7 +168,7 @@ fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_nam
|
||||
}
|
||||
|
||||
if (el.is(Element.Html)) |html_el| {
|
||||
if (interactive.classifyInteractivity(el, html_el, listener_targets) != null) {
|
||||
if (interactive.classifyInteractivity(self.page, el, html_el, ctx.listener_targets, ctx.pointer_events_cache) != null) {
|
||||
is_interactive = true;
|
||||
}
|
||||
}
|
||||
@@ -145,9 +176,9 @@ fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_nam
|
||||
node_name = "root";
|
||||
}
|
||||
|
||||
const initial_xpath_len = xpath_buffer.items.len;
|
||||
try appendXPathSegment(node, xpath_buffer.writer(self.arena), index);
|
||||
const xpath = xpath_buffer.items;
|
||||
const initial_xpath_len = ctx.xpath_buffer.items.len;
|
||||
try appendXPathSegment(node, ctx.xpath_buffer.writer(self.arena), index);
|
||||
const xpath = ctx.xpath_buffer.items;
|
||||
|
||||
var name = try axn.getName(self.page, self.arena);
|
||||
|
||||
@@ -165,18 +196,6 @@ fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_nam
|
||||
name = null;
|
||||
}
|
||||
|
||||
var data = NodeData{
|
||||
.id = cdp_node.id,
|
||||
.axn = axn,
|
||||
.role = role,
|
||||
.name = name,
|
||||
.value = value,
|
||||
.options = options,
|
||||
.xpath = xpath,
|
||||
.is_interactive = is_interactive,
|
||||
.node_name = node_name,
|
||||
};
|
||||
|
||||
var should_visit = true;
|
||||
if (self.interactive_only) {
|
||||
var keep = false;
|
||||
@@ -208,6 +227,18 @@ fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_nam
|
||||
|
||||
var did_visit = false;
|
||||
var should_walk_children = true;
|
||||
var data: NodeData = .{
|
||||
.id = cdp_node.id,
|
||||
.axn = axn,
|
||||
.role = role,
|
||||
.name = name,
|
||||
.value = value,
|
||||
.options = options,
|
||||
.xpath = xpath,
|
||||
.is_interactive = is_interactive,
|
||||
.node_name = node_name,
|
||||
};
|
||||
|
||||
if (should_visit) {
|
||||
should_walk_children = try visitor.visit(node, &data);
|
||||
did_visit = true; // Always true if should_visit was true, because visit() executed and opened structures
|
||||
@@ -233,7 +264,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, current_depth + 1);
|
||||
try self.walk(ctx, child, name, visitor, gop.value_ptr.*, current_depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,11 +272,11 @@ fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_nam
|
||||
try visitor.leave();
|
||||
}
|
||||
|
||||
xpath_buffer.shrinkRetainingCapacity(initial_xpath_len);
|
||||
ctx.xpath_buffer.shrinkRetainingCapacity(initial_xpath_len);
|
||||
}
|
||||
|
||||
fn extractSelectOptions(node: *Node, page: *Page, arena: std.mem.Allocator) ![]OptionData {
|
||||
var options = std.ArrayListUnmanaged(OptionData){};
|
||||
var options: std.ArrayList(OptionData) = .empty;
|
||||
var it = node.childrenIterator();
|
||||
while (it.next()) |child| {
|
||||
if (child.is(Element)) |el| {
|
||||
|
||||
@@ -35,6 +35,7 @@ const Factory = @import("Factory.zig");
|
||||
const Session = @import("Session.zig");
|
||||
const EventManager = @import("EventManager.zig");
|
||||
const ScriptManager = @import("ScriptManager.zig");
|
||||
const StyleManager = @import("StyleManager.zig");
|
||||
|
||||
const Parser = @import("parser/Parser.zig");
|
||||
|
||||
@@ -144,6 +145,7 @@ _blob_urls: std.StringHashMapUnmanaged(*Blob) = .{},
|
||||
/// A call to `documentIsComplete` (which calls `_documentIsComplete`) resets it.
|
||||
_to_load: std.ArrayList(*Element.Html) = .{},
|
||||
|
||||
_style_manager: StyleManager,
|
||||
_script_manager: ScriptManager,
|
||||
|
||||
// List of active live ranges (for mutation updates per DOM spec)
|
||||
@@ -269,6 +271,7 @@ pub fn init(self: *Page, frame_id: u32, session: *Session, parent: ?*Page) !void
|
||||
._factory = factory,
|
||||
._pending_loads = 1, // always 1 for the ScriptManager
|
||||
._type = if (parent == null) .root else .frame,
|
||||
._style_manager = undefined,
|
||||
._script_manager = undefined,
|
||||
._event_manager = EventManager.init(session.page_arena, self),
|
||||
};
|
||||
@@ -298,6 +301,9 @@ pub fn init(self: *Page, frame_id: u32, session: *Session, parent: ?*Page) !void
|
||||
._visual_viewport = visual_viewport,
|
||||
});
|
||||
|
||||
self._style_manager = try StyleManager.init(self);
|
||||
errdefer self._style_manager.deinit();
|
||||
|
||||
const browser = session.browser;
|
||||
self._script_manager = ScriptManager.init(browser.allocator, browser.http_client, self);
|
||||
errdefer self._script_manager.deinit();
|
||||
@@ -360,6 +366,7 @@ pub fn deinit(self: *Page, abort_http: bool) void {
|
||||
}
|
||||
|
||||
self._script_manager.deinit();
|
||||
self._style_manager.deinit();
|
||||
|
||||
session.releaseArena(self.call_arena);
|
||||
}
|
||||
@@ -2588,6 +2595,17 @@ pub fn removeNode(self: *Page, parent: *Node, child: *Node, opts: RemoveNodeOpts
|
||||
}
|
||||
|
||||
Element.Html.Custom.invokeDisconnectedCallbackOnElement(el, self);
|
||||
|
||||
// If a <style> element is being removed, remove its sheet from the list
|
||||
if (el.is(Element.Html.Style)) |style| {
|
||||
if (style._sheet) |sheet| {
|
||||
if (self.document._style_sheets) |sheets| {
|
||||
sheets.remove(sheet);
|
||||
}
|
||||
style._sheet = null;
|
||||
}
|
||||
self._style_manager.sheetModified();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
856
src/browser/StyleManager.zig
Normal file
856
src/browser/StyleManager.zig
Normal file
@@ -0,0 +1,856 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const log = @import("../log.zig");
|
||||
const String = @import("../string.zig").String;
|
||||
|
||||
const Page = @import("Page.zig");
|
||||
|
||||
const CssParser = @import("css/Parser.zig");
|
||||
const Element = @import("webapi/Element.zig");
|
||||
|
||||
const Selector = @import("webapi/selector/Selector.zig");
|
||||
const SelectorParser = @import("webapi/selector/Parser.zig");
|
||||
const SelectorList = @import("webapi/selector/List.zig");
|
||||
|
||||
const CSSStyleRule = @import("webapi/css/CSSStyleRule.zig");
|
||||
const CSSStyleSheet = @import("webapi/css/CSSStyleSheet.zig");
|
||||
const CSSStyleProperties = @import("webapi/css/CSSStyleProperties.zig");
|
||||
const CSSStyleProperty = @import("webapi/css/CSSStyleDeclaration.zig").Property;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
pub const VisibilityCache = std.AutoHashMapUnmanaged(*Element, bool);
|
||||
pub const PointerEventsCache = std.AutoHashMapUnmanaged(*Element, bool);
|
||||
|
||||
// Tracks visibility-relevant CSS rules from <style> elements.
|
||||
// Rules are bucketed by their rightmost selector part for fast lookup.
|
||||
const StyleManager = @This();
|
||||
|
||||
const Tag = Element.Tag;
|
||||
const RuleList = std.MultiArrayList(VisibilityRule);
|
||||
|
||||
page: *Page,
|
||||
|
||||
arena: Allocator,
|
||||
|
||||
// Bucketed rules for fast lookup - keyed by rightmost selector part
|
||||
id_rules: std.StringHashMapUnmanaged(RuleList) = .empty,
|
||||
class_rules: std.StringHashMapUnmanaged(RuleList) = .empty,
|
||||
tag_rules: std.AutoHashMapUnmanaged(Tag, RuleList) = .empty,
|
||||
other_rules: RuleList = .empty, // universal, attribute, pseudo-class endings
|
||||
|
||||
// Document order counter for tie-breaking equal specificity
|
||||
next_doc_order: u32 = 0,
|
||||
|
||||
// When true, rules need to be rebuilt
|
||||
dirty: bool = false,
|
||||
|
||||
pub fn init(page: *Page) !StyleManager {
|
||||
return .{
|
||||
.page = page,
|
||||
.arena = try page.getArena(.{ .debug = "StyleManager" }),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *StyleManager) void {
|
||||
self.page.releaseArena(self.arena);
|
||||
}
|
||||
|
||||
fn parseSheet(self: *StyleManager, sheet: *CSSStyleSheet) !void {
|
||||
if (sheet._css_rules) |css_rules| {
|
||||
for (css_rules._rules.items) |rule| {
|
||||
const style_rule = rule.is(CSSStyleRule) orelse continue;
|
||||
try self.addRule(style_rule);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const owner_node = sheet.getOwnerNode() orelse return;
|
||||
if (owner_node.is(Element.Html.Style)) |style| {
|
||||
const text = try style.asNode().getTextContentAlloc(self.arena);
|
||||
var it = CssParser.parseStylesheet(text);
|
||||
while (it.next()) |parsed_rule| {
|
||||
try self.addRawRule(parsed_rule.selector, parsed_rule.block);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn addRawRule(self: *StyleManager, selector_text: []const u8, block_text: []const u8) !void {
|
||||
if (selector_text.len == 0) return;
|
||||
|
||||
var props = VisibilityProperties{};
|
||||
var it = CssParser.parseDeclarationsList(block_text);
|
||||
while (it.next()) |decl| {
|
||||
const name = decl.name;
|
||||
const val = decl.value;
|
||||
if (std.ascii.eqlIgnoreCase(name, "display")) {
|
||||
props.display_none = std.ascii.eqlIgnoreCase(val, "none");
|
||||
} else if (std.ascii.eqlIgnoreCase(name, "visibility")) {
|
||||
props.visibility_hidden = std.ascii.eqlIgnoreCase(val, "hidden") or std.ascii.eqlIgnoreCase(val, "collapse");
|
||||
} else if (std.ascii.eqlIgnoreCase(name, "opacity")) {
|
||||
props.opacity_zero = std.ascii.eqlIgnoreCase(val, "0");
|
||||
} else if (std.ascii.eqlIgnoreCase(name, "pointer-events")) {
|
||||
props.pointer_events_none = std.ascii.eqlIgnoreCase(val, "none");
|
||||
}
|
||||
}
|
||||
|
||||
if (!props.isRelevant()) return;
|
||||
|
||||
const selectors = SelectorParser.parseList(self.arena, selector_text, self.page) catch return;
|
||||
for (selectors) |selector| {
|
||||
const rightmost = if (selector.segments.len > 0) selector.segments[selector.segments.len - 1].compound else selector.first;
|
||||
const bucket_key = getBucketKey(rightmost) orelse continue;
|
||||
const rule = VisibilityRule{
|
||||
.props = props,
|
||||
.selector = selector,
|
||||
.priority = (@as(u64, computeSpecificity(selector)) << 32) | @as(u64, self.next_doc_order),
|
||||
};
|
||||
self.next_doc_order += 1;
|
||||
|
||||
switch (bucket_key) {
|
||||
.id => |id| {
|
||||
const gop = try self.id_rules.getOrPut(self.arena, id);
|
||||
if (!gop.found_existing) gop.value_ptr.* = .{};
|
||||
try gop.value_ptr.append(self.arena, rule);
|
||||
},
|
||||
.class => |class| {
|
||||
const gop = try self.class_rules.getOrPut(self.arena, class);
|
||||
if (!gop.found_existing) gop.value_ptr.* = .{};
|
||||
try gop.value_ptr.append(self.arena, rule);
|
||||
},
|
||||
.tag => |tag| {
|
||||
const gop = try self.tag_rules.getOrPut(self.arena, tag);
|
||||
if (!gop.found_existing) gop.value_ptr.* = .{};
|
||||
try gop.value_ptr.append(self.arena, rule);
|
||||
},
|
||||
.other => {
|
||||
try self.other_rules.append(self.arena, rule);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sheetRemoved(self: *StyleManager) void {
|
||||
self.dirty = true;
|
||||
}
|
||||
|
||||
pub fn sheetModified(self: *StyleManager) void {
|
||||
self.dirty = true;
|
||||
}
|
||||
|
||||
/// Rebuilds the rule list from all document stylesheets.
|
||||
/// Called lazily when dirty flag is set and rules are needed.
|
||||
fn rebuildIfDirty(self: *StyleManager) !void {
|
||||
if (!self.dirty) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.dirty = false;
|
||||
errdefer self.dirty = true;
|
||||
const id_rules_count = self.id_rules.count();
|
||||
const class_rules_count = self.class_rules.count();
|
||||
const tag_rules_count = self.tag_rules.count();
|
||||
const other_rules_count = self.other_rules.len;
|
||||
|
||||
self.page._session.arena_pool.resetRetain(self.arena);
|
||||
|
||||
self.next_doc_order = 0;
|
||||
|
||||
self.id_rules = .empty;
|
||||
try self.id_rules.ensureTotalCapacity(self.arena, id_rules_count);
|
||||
|
||||
self.class_rules = .empty;
|
||||
try self.class_rules.ensureTotalCapacity(self.arena, class_rules_count);
|
||||
|
||||
self.tag_rules = .empty;
|
||||
try self.tag_rules.ensureTotalCapacity(self.arena, tag_rules_count);
|
||||
|
||||
self.other_rules = .{};
|
||||
try self.other_rules.ensureTotalCapacity(self.arena, other_rules_count);
|
||||
|
||||
const sheets = self.page.document._style_sheets orelse return;
|
||||
for (sheets._sheets.items) |sheet| {
|
||||
self.parseSheet(sheet) catch |err| {
|
||||
log.err(.browser, "StyleManager parseSheet", .{ .err = err });
|
||||
return err;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check if an element is hidden based on options.
|
||||
// By default only checks display:none.
|
||||
// Walks up the tree to check ancestors.
|
||||
pub fn isHidden(self: *StyleManager, el: *Element, cache: ?*VisibilityCache, options: CheckVisibilityOptions) bool {
|
||||
var current: ?*Element = el;
|
||||
|
||||
while (current) |elem| {
|
||||
// Check cache first (only when checking all properties for caching consistency)
|
||||
if (cache) |c| {
|
||||
if (c.get(elem)) |hidden| {
|
||||
if (hidden) {
|
||||
return true;
|
||||
}
|
||||
current = elem.parentElement();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const hidden = self.isElementHidden(elem, options);
|
||||
|
||||
// Store in cache
|
||||
if (cache) |c| {
|
||||
c.put(self.page.call_arena, elem, hidden) catch {};
|
||||
}
|
||||
|
||||
if (hidden) {
|
||||
return true;
|
||||
}
|
||||
current = elem.parentElement();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Check if a single element (not ancestors) is hidden.
|
||||
fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOptions) bool {
|
||||
// Track best match per property (value + priority)
|
||||
// Initialize priority to INLINE_PRIORITY for properties we don't care about - this makes
|
||||
// the loop naturally skip them since no stylesheet rule can have priority >= INLINE_PRIORITY
|
||||
var display_none: ?bool = null;
|
||||
var display_priority: u64 = 0;
|
||||
|
||||
var visibility_hidden: ?bool = null;
|
||||
var visibility_priority: u64 = 0;
|
||||
|
||||
var opacity_zero: ?bool = null;
|
||||
var opacity_priority: u64 = 0;
|
||||
|
||||
// Check inline styles FIRST - they use INLINE_PRIORITY so no stylesheet can beat them
|
||||
if (getInlineStyleProperty(el, comptime .wrap("display"), self.page)) |property| {
|
||||
if (property._value.eql(comptime .wrap("none"))) {
|
||||
return true; // Early exit for hiding value
|
||||
}
|
||||
display_none = false;
|
||||
display_priority = INLINE_PRIORITY;
|
||||
}
|
||||
|
||||
if (options.check_visibility) {
|
||||
if (getInlineStyleProperty(el, comptime .wrap("visibility"), self.page)) |property| {
|
||||
if (property._value.eql(comptime .wrap("hidden")) or property._value.eql(comptime .wrap("collapse"))) {
|
||||
return true;
|
||||
}
|
||||
visibility_hidden = false;
|
||||
visibility_priority = INLINE_PRIORITY;
|
||||
}
|
||||
} else {
|
||||
// This can't be beat. Setting this means that, when checking rules
|
||||
// we no longer have to check if options.check_visibility is enabled.
|
||||
// We can just compare the priority.
|
||||
visibility_priority = INLINE_PRIORITY;
|
||||
}
|
||||
|
||||
if (options.check_opacity) {
|
||||
if (getInlineStyleProperty(el, comptime .wrap("opacity"), self.page)) |property| {
|
||||
if (property._value.eql(comptime .wrap("0"))) {
|
||||
return true;
|
||||
}
|
||||
opacity_zero = false;
|
||||
opacity_priority = INLINE_PRIORITY;
|
||||
}
|
||||
} else {
|
||||
opacity_priority = INLINE_PRIORITY;
|
||||
}
|
||||
|
||||
if (display_priority == INLINE_PRIORITY and visibility_priority == INLINE_PRIORITY and opacity_priority == INLINE_PRIORITY) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.rebuildIfDirty() catch return false;
|
||||
|
||||
// Helper to check a single rule
|
||||
const Ctx = struct {
|
||||
display_none: *?bool,
|
||||
display_priority: *u64,
|
||||
visibility_hidden: *?bool,
|
||||
visibility_priority: *u64,
|
||||
opacity_zero: *?bool,
|
||||
opacity_priority: *u64,
|
||||
el: *Element,
|
||||
page: *Page,
|
||||
|
||||
fn checkRules(ctx: @This(), rules: *const RuleList) void {
|
||||
if (ctx.display_priority.* == INLINE_PRIORITY and
|
||||
ctx.visibility_priority.* == INLINE_PRIORITY and
|
||||
ctx.opacity_priority.* == INLINE_PRIORITY)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const priorities = rules.items(.priority);
|
||||
const props_list = rules.items(.props);
|
||||
const selectors = rules.items(.selector);
|
||||
|
||||
for (priorities, props_list, selectors) |p, props, selector| {
|
||||
// Fast skip using packed u64 priority
|
||||
if (p <= ctx.display_priority.* and p <= ctx.visibility_priority.* and p <= ctx.opacity_priority.*) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Logic for property dominance
|
||||
const dominated = (props.display_none == null or p <= ctx.display_priority.*) and
|
||||
(props.visibility_hidden == null or p <= ctx.visibility_priority.*) and
|
||||
(props.opacity_zero == null or p <= ctx.opacity_priority.*);
|
||||
|
||||
if (dominated) continue;
|
||||
|
||||
if (matchesSelector(ctx.el, selector, ctx.page)) {
|
||||
// Update best priorities
|
||||
if (props.display_none != null and p > ctx.display_priority.*) {
|
||||
ctx.display_none.* = props.display_none;
|
||||
ctx.display_priority.* = p;
|
||||
}
|
||||
if (props.visibility_hidden != null and p > ctx.visibility_priority.*) {
|
||||
ctx.visibility_hidden.* = props.visibility_hidden;
|
||||
ctx.visibility_priority.* = p;
|
||||
}
|
||||
if (props.opacity_zero != null and p > ctx.opacity_priority.*) {
|
||||
ctx.opacity_zero.* = props.opacity_zero;
|
||||
ctx.opacity_priority.* = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const ctx = Ctx{
|
||||
.display_none = &display_none,
|
||||
.display_priority = &display_priority,
|
||||
.visibility_hidden = &visibility_hidden,
|
||||
.visibility_priority = &visibility_priority,
|
||||
.opacity_zero = &opacity_zero,
|
||||
.opacity_priority = &opacity_priority,
|
||||
.el = el,
|
||||
.page = self.page,
|
||||
};
|
||||
|
||||
if (el.getAttributeSafe(comptime .wrap("id"))) |id| {
|
||||
if (self.id_rules.get(id)) |rules| {
|
||||
ctx.checkRules(&rules);
|
||||
}
|
||||
}
|
||||
|
||||
if (el.getAttributeSafe(comptime .wrap("class"))) |class_attr| {
|
||||
var it = std.mem.tokenizeAny(u8, class_attr, &std.ascii.whitespace);
|
||||
while (it.next()) |class| {
|
||||
if (self.class_rules.get(class)) |rules| {
|
||||
ctx.checkRules(&rules);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (self.tag_rules.get(el.getTag())) |rules| {
|
||||
ctx.checkRules(&rules);
|
||||
}
|
||||
|
||||
ctx.checkRules(&self.other_rules);
|
||||
|
||||
return (display_none orelse false) or (visibility_hidden orelse false) or (opacity_zero orelse false);
|
||||
}
|
||||
|
||||
/// Check if an element has pointer-events:none.
|
||||
/// Checks inline style first - if set, skips stylesheet lookup.
|
||||
/// Walks up the tree to check ancestors.
|
||||
pub fn hasPointerEventsNone(self: *StyleManager, el: *Element, cache: ?*PointerEventsCache) bool {
|
||||
var current: ?*Element = el;
|
||||
|
||||
while (current) |elem| {
|
||||
// Check cache first
|
||||
if (cache) |c| {
|
||||
if (c.get(elem)) |pe_none| {
|
||||
if (pe_none) return true;
|
||||
current = elem.parentElement();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const pe_none = self.elementHasPointerEventsNone(elem);
|
||||
|
||||
if (cache) |c| {
|
||||
c.put(self.page.call_arena, elem, pe_none) catch {};
|
||||
}
|
||||
|
||||
if (pe_none) {
|
||||
return true;
|
||||
}
|
||||
current = elem.parentElement();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Check if a single element (not ancestors) has pointer-events:none.
|
||||
fn elementHasPointerEventsNone(self: *StyleManager, el: *Element) bool {
|
||||
const page = self.page;
|
||||
|
||||
// Check inline style first
|
||||
if (getInlineStyleProperty(el, .wrap("pointer-events"), page)) |property| {
|
||||
if (property._value.eql(comptime .wrap("none"))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check stylesheet rules
|
||||
self.rebuildIfDirty() catch return false;
|
||||
|
||||
var result: ?bool = null;
|
||||
var best_priority: u64 = 0;
|
||||
|
||||
// Helper to check a single rule
|
||||
const checkRules = struct {
|
||||
fn check(rules: *const RuleList, res: *?bool, current_priority: *u64, elem: *Element, p: *Page) void {
|
||||
if (current_priority.* == INLINE_PRIORITY) return;
|
||||
|
||||
const priorities = rules.items(.priority);
|
||||
const props_list = rules.items(.props);
|
||||
const selectors = rules.items(.selector);
|
||||
|
||||
for (priorities, props_list, selectors) |priority, props, selector| {
|
||||
if (priority <= current_priority.*) continue;
|
||||
if (props.pointer_events_none == null) continue;
|
||||
|
||||
if (matchesSelector(elem, selector, p)) {
|
||||
res.* = props.pointer_events_none;
|
||||
current_priority.* = priority;
|
||||
}
|
||||
}
|
||||
}
|
||||
}.check;
|
||||
|
||||
if (el.getAttributeSafe(comptime .wrap("id"))) |id| {
|
||||
if (self.id_rules.get(id)) |rules| {
|
||||
checkRules(&rules, &result, &best_priority, el, page);
|
||||
}
|
||||
}
|
||||
|
||||
if (el.getAttributeSafe(comptime .wrap("class"))) |class_attr| {
|
||||
var it = std.mem.tokenizeAny(u8, class_attr, &std.ascii.whitespace);
|
||||
while (it.next()) |class| {
|
||||
if (self.class_rules.get(class)) |rules| {
|
||||
checkRules(&rules, &result, &best_priority, el, page);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (self.tag_rules.get(el.getTag())) |rules| {
|
||||
checkRules(&rules, &result, &best_priority, el, page);
|
||||
}
|
||||
|
||||
checkRules(&self.other_rules, &result, &best_priority, el, page);
|
||||
|
||||
return result orelse false;
|
||||
}
|
||||
|
||||
// Extracts visibility-relevant rules from a CSS rule.
|
||||
// Creates one VisibilityRule per selector (not per selector list) so each has correct specificity.
|
||||
// Buckets rules by their rightmost selector part for fast lookup.
|
||||
fn addRule(self: *StyleManager, style_rule: *CSSStyleRule) !void {
|
||||
const selector_text = style_rule._selector_text;
|
||||
if (selector_text.len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the rule has visibility-relevant properties
|
||||
const style = style_rule._style orelse return;
|
||||
const props = extractVisibilityProperties(style);
|
||||
if (!props.isRelevant()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse the selector list
|
||||
const selectors = SelectorParser.parseList(self.arena, selector_text, self.page) catch return;
|
||||
if (selectors.len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create one rule per selector - each has its own specificity
|
||||
// e.g., "#id, .class { display: none }" becomes two rules with different specificities
|
||||
for (selectors) |selector| {
|
||||
// Get the rightmost compound (last segment, or first if no segments)
|
||||
const rightmost = if (selector.segments.len > 0)
|
||||
selector.segments[selector.segments.len - 1].compound
|
||||
else
|
||||
selector.first;
|
||||
|
||||
// Find the bucketing key from rightmost compound
|
||||
const bucket_key = getBucketKey(rightmost) orelse continue; // skip if dynamic pseudo-class
|
||||
|
||||
const rule = VisibilityRule{
|
||||
.props = props,
|
||||
.selector = selector,
|
||||
.priority = (@as(u64, computeSpecificity(selector)) << 32) | @as(u64, self.next_doc_order),
|
||||
};
|
||||
self.next_doc_order += 1;
|
||||
|
||||
// Add to appropriate bucket
|
||||
switch (bucket_key) {
|
||||
.id => |id| {
|
||||
const gop = try self.id_rules.getOrPut(self.arena, id);
|
||||
if (!gop.found_existing) gop.value_ptr.* = .{};
|
||||
try gop.value_ptr.append(self.arena, rule);
|
||||
},
|
||||
.class => |class| {
|
||||
const gop = try self.class_rules.getOrPut(self.arena, class);
|
||||
if (!gop.found_existing) gop.value_ptr.* = .{};
|
||||
try gop.value_ptr.append(self.arena, rule);
|
||||
},
|
||||
.tag => |tag| {
|
||||
const gop = try self.tag_rules.getOrPut(self.arena, tag);
|
||||
if (!gop.found_existing) gop.value_ptr.* = .{};
|
||||
try gop.value_ptr.append(self.arena, rule);
|
||||
},
|
||||
.other => {
|
||||
try self.other_rules.append(self.arena, rule);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const BucketKey = union(enum) {
|
||||
id: []const u8,
|
||||
class: []const u8,
|
||||
tag: Tag,
|
||||
other,
|
||||
};
|
||||
|
||||
/// Returns the best bucket key for a compound selector, or null if it contains
|
||||
/// a dynamic pseudo-class we should skip (hover, active, focus, etc.)
|
||||
/// Priority: id > class > tag > other
|
||||
fn getBucketKey(compound: Selector.Compound) ?BucketKey {
|
||||
var best_key: BucketKey = .other;
|
||||
|
||||
for (compound.parts) |part| {
|
||||
switch (part) {
|
||||
.id => |id| {
|
||||
best_key = .{ .id = id };
|
||||
},
|
||||
.class => |class| {
|
||||
if (best_key != .id) {
|
||||
best_key = .{ .class = class };
|
||||
}
|
||||
},
|
||||
.tag => |tag| {
|
||||
if (best_key == .other) {
|
||||
best_key = .{ .tag = tag };
|
||||
}
|
||||
},
|
||||
.tag_name => {
|
||||
// Custom tag - put in other bucket (can't efficiently look up)
|
||||
// Keep current best_key if we have something better
|
||||
},
|
||||
.pseudo_class => |pc| {
|
||||
// Skip dynamic pseudo-classes - they depend on interaction state
|
||||
switch (pc) {
|
||||
.hover, .active, .focus, .focus_within, .focus_visible, .visited, .target => {
|
||||
return null; // Skip this selector entirely
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
},
|
||||
.universal, .attribute => {},
|
||||
}
|
||||
}
|
||||
|
||||
return best_key;
|
||||
}
|
||||
|
||||
/// Extracts visibility-relevant properties from a style declaration.
|
||||
fn extractVisibilityProperties(style: *CSSStyleProperties) VisibilityProperties {
|
||||
var props = VisibilityProperties{};
|
||||
const decl = style.asCSSStyleDeclaration();
|
||||
|
||||
if (decl.findProperty(comptime .wrap("display"))) |property| {
|
||||
props.display_none = property._value.eql(comptime .wrap("none"));
|
||||
}
|
||||
|
||||
if (decl.findProperty(comptime .wrap("visibility"))) |property| {
|
||||
props.visibility_hidden = property._value.eql(comptime .wrap("hidden")) or property._value.eql(comptime .wrap("collapse"));
|
||||
}
|
||||
|
||||
if (decl.findProperty(comptime .wrap("opacity"))) |property| {
|
||||
props.opacity_zero = property._value.eql(comptime .wrap("0"));
|
||||
}
|
||||
|
||||
if (decl.findProperty(.wrap("pointer-events"))) |property| {
|
||||
props.pointer_events_none = property._value.eql(comptime .wrap("none"));
|
||||
}
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
// Computes CSS specificity for a selector.
|
||||
// Returns packed value: (id_count << 20) | (class_count << 10) | element_count
|
||||
pub fn computeSpecificity(selector: Selector.Selector) u32 {
|
||||
var ids: u32 = 0;
|
||||
var classes: u32 = 0; // includes classes, attributes, pseudo-classes
|
||||
var elements: u32 = 0; // includes elements, pseudo-elements
|
||||
|
||||
// Count specificity for first compound
|
||||
countCompoundSpecificity(selector.first, &ids, &classes, &elements);
|
||||
|
||||
// Count specificity for subsequent segments
|
||||
for (selector.segments) |segment| {
|
||||
countCompoundSpecificity(segment.compound, &ids, &classes, &elements);
|
||||
}
|
||||
|
||||
// Pack into single u32: (ids << 20) | (classes << 10) | elements
|
||||
// This gives us 10 bits each, supporting up to 1023 of each type
|
||||
return (@as(u32, @min(ids, 1023)) << 20) | (@as(u32, @min(classes, 1023)) << 10) | @min(elements, 1023);
|
||||
}
|
||||
|
||||
fn countCompoundSpecificity(compound: Selector.Compound, ids: *u32, classes: *u32, elements: *u32) void {
|
||||
for (compound.parts) |part| {
|
||||
switch (part) {
|
||||
.id => ids.* += 1,
|
||||
.class => classes.* += 1,
|
||||
.tag, .tag_name => elements.* += 1,
|
||||
.universal => {}, // zero specificity
|
||||
.attribute => classes.* += 1,
|
||||
.pseudo_class => |pc| {
|
||||
switch (pc) {
|
||||
// :where() has zero specificity
|
||||
.where => {},
|
||||
// :not(), :is(), :has() take specificity of their most specific argument
|
||||
.not, .is, .has => |nested| {
|
||||
var max_nested: u32 = 0;
|
||||
for (nested) |nested_sel| {
|
||||
const spec = computeSpecificity(nested_sel);
|
||||
if (spec > max_nested) max_nested = spec;
|
||||
}
|
||||
// Unpack and add to our counts
|
||||
ids.* += (max_nested >> 20) & 0x3FF;
|
||||
classes.* += (max_nested >> 10) & 0x3FF;
|
||||
elements.* += max_nested & 0x3FF;
|
||||
},
|
||||
// All other pseudo-classes count as class-level specificity
|
||||
else => classes.* += 1,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn matchesSelector(el: *Element, selector: Selector.Selector, page: *Page) bool {
|
||||
const node = el.asNode();
|
||||
return SelectorList.matches(node, selector, node, page);
|
||||
}
|
||||
|
||||
const VisibilityProperties = struct {
|
||||
display_none: ?bool = null,
|
||||
visibility_hidden: ?bool = null,
|
||||
opacity_zero: ?bool = null,
|
||||
pointer_events_none: ?bool = null,
|
||||
|
||||
// returne true if any field in VisibilityProperties is not null
|
||||
fn isRelevant(self: VisibilityProperties) bool {
|
||||
return self.display_none != null or
|
||||
self.visibility_hidden != null or
|
||||
self.opacity_zero != null or
|
||||
self.pointer_events_none != null;
|
||||
}
|
||||
};
|
||||
|
||||
const VisibilityRule = struct {
|
||||
selector: Selector.Selector, // Single selector, not a list
|
||||
props: VisibilityProperties,
|
||||
|
||||
// Packed priority: (specificity << 32) | doc_order
|
||||
priority: u64,
|
||||
};
|
||||
|
||||
const CheckVisibilityOptions = struct {
|
||||
check_opacity: bool = false,
|
||||
check_visibility: bool = false,
|
||||
};
|
||||
|
||||
// Inline styles always win over stylesheets - use max u64 as sentinel
|
||||
const INLINE_PRIORITY: u64 = std.math.maxInt(u64);
|
||||
|
||||
fn getInlineStyleProperty(el: *Element, property_name: String, page: *Page) ?*CSSStyleProperty {
|
||||
const style = el.getOrCreateStyle(page) catch |err| {
|
||||
log.err(.browser, "StyleManager getOrCreateStyle", .{ .err = err });
|
||||
return null;
|
||||
};
|
||||
return style.asCSSStyleDeclaration().findProperty(property_name);
|
||||
}
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
test "StyleManager: computeSpecificity: element selector" {
|
||||
// div -> (0, 0, 1)
|
||||
const selector = Selector.Selector{
|
||||
.first = .{ .parts = &.{.{ .tag = .div }} },
|
||||
.segments = &.{},
|
||||
};
|
||||
try testing.expectEqual(1, computeSpecificity(selector));
|
||||
}
|
||||
|
||||
test "StyleManager: computeSpecificity: class selector" {
|
||||
// .foo -> (0, 1, 0)
|
||||
const selector = Selector.Selector{
|
||||
.first = .{ .parts = &.{.{ .class = "foo" }} },
|
||||
.segments = &.{},
|
||||
};
|
||||
try testing.expectEqual(1 << 10, computeSpecificity(selector));
|
||||
}
|
||||
|
||||
test "StyleManager: computeSpecificity: id selector" {
|
||||
// #bar -> (1, 0, 0)
|
||||
const selector = Selector.Selector{
|
||||
.first = .{ .parts = &.{.{ .id = "bar" }} },
|
||||
.segments = &.{},
|
||||
};
|
||||
try testing.expectEqual(1 << 20, computeSpecificity(selector));
|
||||
}
|
||||
|
||||
test "StyleManager: computeSpecificity: combined selector" {
|
||||
// div.foo#bar -> (1, 1, 1)
|
||||
const selector = Selector.Selector{
|
||||
.first = .{ .parts = &.{
|
||||
.{ .tag = .div },
|
||||
.{ .class = "foo" },
|
||||
.{ .id = "bar" },
|
||||
} },
|
||||
.segments = &.{},
|
||||
};
|
||||
try testing.expectEqual((1 << 20) | (1 << 10) | 1, computeSpecificity(selector));
|
||||
}
|
||||
|
||||
test "StyleManager: computeSpecificity: universal selector" {
|
||||
// * -> (0, 0, 0)
|
||||
const selector = Selector.Selector{
|
||||
.first = .{ .parts = &.{.universal} },
|
||||
.segments = &.{},
|
||||
};
|
||||
try testing.expectEqual(0, computeSpecificity(selector));
|
||||
}
|
||||
|
||||
test "StyleManager: computeSpecificity: multiple classes" {
|
||||
// .a.b.c -> (0, 3, 0)
|
||||
const selector = Selector.Selector{
|
||||
.first = .{ .parts = &.{
|
||||
.{ .class = "a" },
|
||||
.{ .class = "b" },
|
||||
.{ .class = "c" },
|
||||
} },
|
||||
.segments = &.{},
|
||||
};
|
||||
try testing.expectEqual(3 << 10, computeSpecificity(selector));
|
||||
}
|
||||
|
||||
test "StyleManager: computeSpecificity: descendant combinator" {
|
||||
// div span -> (0, 0, 2)
|
||||
const selector = Selector.Selector{
|
||||
.first = .{ .parts = &.{.{ .tag = .div }} },
|
||||
.segments = &.{
|
||||
.{ .combinator = .descendant, .compound = .{ .parts = &.{.{ .tag = .span }} } },
|
||||
},
|
||||
};
|
||||
try testing.expectEqual(2, computeSpecificity(selector));
|
||||
}
|
||||
|
||||
test "StyleManager: computeSpecificity: :where() has zero specificity" {
|
||||
// :where(.foo) -> (0, 0, 0) regardless of what's inside
|
||||
const inner_selector = Selector.Selector{
|
||||
.first = .{ .parts = &.{.{ .class = "foo" }} },
|
||||
.segments = &.{},
|
||||
};
|
||||
const selector = Selector.Selector{
|
||||
.first = .{ .parts = &.{
|
||||
.{ .pseudo_class = .{ .where = &.{inner_selector} } },
|
||||
} },
|
||||
.segments = &.{},
|
||||
};
|
||||
try testing.expectEqual(0, computeSpecificity(selector));
|
||||
}
|
||||
|
||||
test "StyleManager: computeSpecificity: :not() takes inner specificity" {
|
||||
// :not(.foo) -> (0, 1, 0) - takes specificity of .foo
|
||||
const inner_selector = Selector.Selector{
|
||||
.first = .{ .parts = &.{.{ .class = "foo" }} },
|
||||
.segments = &.{},
|
||||
};
|
||||
const selector = Selector.Selector{
|
||||
.first = .{ .parts = &.{
|
||||
.{ .pseudo_class = .{ .not = &.{inner_selector} } },
|
||||
} },
|
||||
.segments = &.{},
|
||||
};
|
||||
try testing.expectEqual(1 << 10, computeSpecificity(selector));
|
||||
}
|
||||
|
||||
test "StyleManager: computeSpecificity: :is() takes most specific inner" {
|
||||
// :is(.foo, #bar) -> (1, 0, 0) - takes the most specific (#bar)
|
||||
const class_selector = Selector.Selector{
|
||||
.first = .{ .parts = &.{.{ .class = "foo" }} },
|
||||
.segments = &.{},
|
||||
};
|
||||
const id_selector = Selector.Selector{
|
||||
.first = .{ .parts = &.{.{ .id = "bar" }} },
|
||||
.segments = &.{},
|
||||
};
|
||||
const selector = Selector.Selector{
|
||||
.first = .{ .parts = &.{
|
||||
.{ .pseudo_class = .{ .is = &.{ class_selector, id_selector } } },
|
||||
} },
|
||||
.segments = &.{},
|
||||
};
|
||||
try testing.expectEqual(1 << 20, computeSpecificity(selector));
|
||||
}
|
||||
|
||||
test "StyleManager: computeSpecificity: pseudo-class (general)" {
|
||||
// :hover -> (0, 1, 0) - pseudo-classes count as class-level
|
||||
const selector = Selector.Selector{
|
||||
.first = .{ .parts = &.{
|
||||
.{ .pseudo_class = .hover },
|
||||
} },
|
||||
.segments = &.{},
|
||||
};
|
||||
try testing.expectEqual(1 << 10, computeSpecificity(selector));
|
||||
}
|
||||
|
||||
test "StyleManager: document order tie-breaking" {
|
||||
// When specificity is equal, higher doc_order (later in document) wins
|
||||
const beats = struct {
|
||||
fn f(spec: u32, doc_order: u32, best_spec: u32, best_doc_order: u32) bool {
|
||||
return spec > best_spec or (spec == best_spec and doc_order > best_doc_order);
|
||||
}
|
||||
}.f;
|
||||
|
||||
// Higher specificity always wins regardless of doc_order
|
||||
try testing.expect(beats(2, 0, 1, 10));
|
||||
try testing.expect(!beats(1, 10, 2, 0));
|
||||
|
||||
// Equal specificity: higher doc_order wins
|
||||
try testing.expect(beats(1, 5, 1, 3)); // doc_order 5 > 3
|
||||
try testing.expect(!beats(1, 3, 1, 5)); // doc_order 3 < 5
|
||||
|
||||
// Equal specificity and doc_order: no win
|
||||
try testing.expect(!beats(1, 5, 1, 5));
|
||||
}
|
||||
@@ -293,3 +293,189 @@ fn isBang(token: Tokenizer.Token) bool {
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
pub const Rule = struct {
|
||||
selector: []const u8,
|
||||
block: []const u8,
|
||||
};
|
||||
|
||||
pub fn parseStylesheet(input: []const u8) RulesIterator {
|
||||
return RulesIterator.init(input);
|
||||
}
|
||||
|
||||
pub const RulesIterator = struct {
|
||||
input: []const u8,
|
||||
stream: TokenStream,
|
||||
|
||||
pub fn init(input: []const u8) RulesIterator {
|
||||
return .{
|
||||
.input = input,
|
||||
.stream = TokenStream.init(input),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn next(self: *RulesIterator) ?Rule {
|
||||
var selector_start: ?usize = null;
|
||||
var selector_end: ?usize = null;
|
||||
|
||||
while (true) {
|
||||
const peeked = self.stream.peek() orelse return null;
|
||||
|
||||
if (peeked.token == .curly_bracket_block) {
|
||||
if (selector_start == null) {
|
||||
self.skipBlock();
|
||||
continue;
|
||||
}
|
||||
|
||||
const open_brace = self.stream.next() orelse return null;
|
||||
const block_start = open_brace.end;
|
||||
var block_end = block_start;
|
||||
|
||||
var depth: usize = 1;
|
||||
while (true) {
|
||||
const span = self.stream.next() orelse {
|
||||
block_end = self.input.len;
|
||||
break;
|
||||
};
|
||||
if (span.token == .curly_bracket_block) {
|
||||
depth += 1;
|
||||
} else if (span.token == .close_curly_bracket) {
|
||||
depth -= 1;
|
||||
if (depth == 0) {
|
||||
block_end = span.start;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var selector = self.input[selector_start.?..selector_end.?];
|
||||
selector = std.mem.trim(u8, selector, &std.ascii.whitespace);
|
||||
|
||||
return .{
|
||||
.selector = selector,
|
||||
.block = self.input[block_start..block_end],
|
||||
};
|
||||
}
|
||||
|
||||
if (peeked.token == .at_keyword) {
|
||||
self.skipAtRule();
|
||||
selector_start = null;
|
||||
selector_end = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (selector_start == null and (isWhitespaceOrComment(peeked.token) or isSemicolon(peeked.token))) {
|
||||
_ = self.stream.next();
|
||||
continue;
|
||||
}
|
||||
|
||||
const span = self.stream.next() orelse return null;
|
||||
if (!isWhitespaceOrComment(span.token)) {
|
||||
if (selector_start == null) selector_start = span.start;
|
||||
selector_end = span.end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn skipBlock(self: *RulesIterator) void {
|
||||
const span = self.stream.next() orelse return;
|
||||
if (span.token != .curly_bracket_block) return;
|
||||
|
||||
var depth: usize = 1;
|
||||
while (true) {
|
||||
const next_span = self.stream.next() orelse return;
|
||||
if (next_span.token == .curly_bracket_block) {
|
||||
depth += 1;
|
||||
} else if (next_span.token == .close_curly_bracket) {
|
||||
depth -= 1;
|
||||
if (depth == 0) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn skipAtRule(self: *RulesIterator) void {
|
||||
_ = self.stream.next(); // consume @keyword
|
||||
var depth: usize = 0;
|
||||
var saw_block = false;
|
||||
|
||||
while (true) {
|
||||
const peeked = self.stream.peek() orelse return;
|
||||
if (!saw_block and isSemicolon(peeked.token) and depth == 0) {
|
||||
_ = self.stream.next();
|
||||
return;
|
||||
}
|
||||
|
||||
const span = self.stream.next() orelse return;
|
||||
if (isWhitespaceOrComment(span.token)) continue;
|
||||
|
||||
if (span.token == .curly_bracket_block) {
|
||||
depth += 1;
|
||||
saw_block = true;
|
||||
} else if (span.token == .close_curly_bracket) {
|
||||
if (depth > 0) depth -= 1;
|
||||
if (saw_block and depth == 0) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
test "RulesIterator: single rule" {
|
||||
var it = RulesIterator.init(".test { color: red; }");
|
||||
const rule = it.next() orelse return error.MissingRule;
|
||||
try testing.expectEqualStrings(".test", rule.selector);
|
||||
try testing.expectEqualStrings(" color: red; ", rule.block);
|
||||
try testing.expectEqual(@as(?Rule, null), it.next());
|
||||
}
|
||||
|
||||
test "RulesIterator: multiple rules" {
|
||||
var it = RulesIterator.init("h1 { margin: 0; } p { padding: 10px; }");
|
||||
|
||||
var rule = it.next() orelse return error.MissingRule;
|
||||
try testing.expectEqualStrings("h1", rule.selector);
|
||||
try testing.expectEqualStrings(" margin: 0; ", rule.block);
|
||||
|
||||
rule = it.next() orelse return error.MissingRule;
|
||||
try testing.expectEqualStrings("p", rule.selector);
|
||||
try testing.expectEqualStrings(" padding: 10px; ", rule.block);
|
||||
|
||||
try testing.expectEqual(@as(?Rule, null), it.next());
|
||||
}
|
||||
|
||||
test "RulesIterator: skips at-rules without block" {
|
||||
var it = RulesIterator.init("@import url('style.css'); .test { color: red; }");
|
||||
|
||||
const rule = it.next() orelse return error.MissingRule;
|
||||
try testing.expectEqualStrings(".test", rule.selector);
|
||||
try testing.expectEqualStrings(" color: red; ", rule.block);
|
||||
try testing.expectEqual(@as(?Rule, null), it.next());
|
||||
}
|
||||
|
||||
test "RulesIterator: skips at-rules with block" {
|
||||
var it = RulesIterator.init("@media screen { .test { color: blue; } } .test2 { color: green; }");
|
||||
|
||||
const rule = it.next() orelse return error.MissingRule;
|
||||
try testing.expectEqualStrings(".test2", rule.selector);
|
||||
try testing.expectEqualStrings(" color: green; ", rule.block);
|
||||
try testing.expectEqual(@as(?Rule, null), it.next());
|
||||
}
|
||||
|
||||
test "RulesIterator: comments and whitespace" {
|
||||
var it = RulesIterator.init(" /* comment */ .test /* comment */ { /* comment */ color: red; } \n\t");
|
||||
|
||||
const rule = it.next() orelse return error.MissingRule;
|
||||
try testing.expectEqualStrings(".test", rule.selector);
|
||||
try testing.expectEqualStrings(" /* comment */ color: red; ", rule.block);
|
||||
try testing.expectEqual(@as(?Rule, null), it.next());
|
||||
}
|
||||
|
||||
test "RulesIterator: top-level semicolons" {
|
||||
var it = RulesIterator.init("*{}; ; p{}");
|
||||
var rule = it.next() orelse return error.MissingRule;
|
||||
try testing.expectEqualStrings("*", rule.selector);
|
||||
|
||||
rule = it.next() orelse return error.MissingRule;
|
||||
try testing.expectEqualStrings("p", rule.selector);
|
||||
try testing.expectEqual(@as(?Rule, null), it.next());
|
||||
}
|
||||
|
||||
@@ -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.PointerEventsCache = .empty;
|
||||
|
||||
var results: std.ArrayList(InteractiveElement) = .empty;
|
||||
|
||||
var tw = TreeWalker.Full.init(root, .{});
|
||||
@@ -146,7 +148,7 @@ pub fn collectInteractiveElements(
|
||||
else => {},
|
||||
}
|
||||
|
||||
const itype = classifyInteractivity(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(),
|
||||
@@ -210,10 +212,14 @@ pub fn buildListenerTargetMap(page: *Page, arena: Allocator) !ListenerTargetMap
|
||||
}
|
||||
|
||||
pub fn classifyInteractivity(
|
||||
page: *Page,
|
||||
el: *Element,
|
||||
html_el: *Element.Html,
|
||||
listener_targets: ListenerTargetMap,
|
||||
cache: ?*Element.PointerEventsCache,
|
||||
) ?InteractivityType {
|
||||
if (el.hasPointerEventsNone(cache, page)) return null;
|
||||
|
||||
// 1. Native interactive by tag
|
||||
switch (el.getTag()) {
|
||||
.button, .summary, .details, .select, .textarea => return .native,
|
||||
@@ -554,6 +560,11 @@ test "browser.interactive: disabled by fieldset" {
|
||||
try testing.expect(!elements[1].disabled);
|
||||
}
|
||||
|
||||
test "browser.interactive: pointer-events none" {
|
||||
const elements = try testInteractive("<button style=\"pointer-events: none;\">Click me</button>");
|
||||
try testing.expectEqual(0, elements.len);
|
||||
}
|
||||
|
||||
test "browser.interactive: non-interactive div" {
|
||||
const elements = try testInteractive("<div>Just text</div>");
|
||||
try testing.expectEqual(0, elements.len);
|
||||
|
||||
@@ -419,3 +419,99 @@
|
||||
testing.expectEqual('anchor-size(--foo width, anchor-size(--bar height))', div.style.width);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="CSSStyleSheet_insertRule_deleteRule">
|
||||
{
|
||||
const style = document.createElement('style');
|
||||
document.head.appendChild(style);
|
||||
const sheet = style.sheet;
|
||||
|
||||
testing.expectEqual(0, sheet.cssRules.length);
|
||||
|
||||
sheet.insertRule('.test { color: green; }', 0);
|
||||
testing.expectEqual(1, sheet.cssRules.length);
|
||||
testing.expectEqual('.test', sheet.cssRules[0].selectorText);
|
||||
testing.expectEqual('green', sheet.cssRules[0].style.color);
|
||||
|
||||
sheet.deleteRule(0);
|
||||
testing.expectEqual(0, sheet.cssRules.length);
|
||||
|
||||
let caught = false;
|
||||
try {
|
||||
sheet.deleteRule(5);
|
||||
} catch (e) {
|
||||
caught = true;
|
||||
testing.expectEqual('IndexSizeError', e.name);
|
||||
}
|
||||
testing.expectTrue(caught);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="CSSStyleSheet_insertRule_default_index">
|
||||
{
|
||||
const style = document.createElement('style');
|
||||
document.head.appendChild(style);
|
||||
const sheet = style.sheet;
|
||||
|
||||
testing.expectEqual(0, sheet.cssRules.length);
|
||||
|
||||
// Call without index, should default to 0
|
||||
sheet.insertRule('.test-default { color: blue; }');
|
||||
testing.expectEqual(1, sheet.cssRules.length);
|
||||
testing.expectEqual('.test-default', sheet.cssRules[0].selectorText);
|
||||
|
||||
// Insert another rule without index, should default to 0 and push the first one to index 1
|
||||
sheet.insertRule('.test-at-0 { color: red; }');
|
||||
testing.expectEqual(2, sheet.cssRules.length);
|
||||
testing.expectEqual('.test-at-0', sheet.cssRules[0].selectorText);
|
||||
testing.expectEqual('.test-default', sheet.cssRules[1].selectorText);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="CSSStyleSheet_insertRule_semicolon">
|
||||
{
|
||||
const style = document.createElement('style');
|
||||
document.head.appendChild(style);
|
||||
const sheet = style.sheet;
|
||||
|
||||
// Should not throw even with trailing semicolon
|
||||
sheet.insertRule('*{};');
|
||||
testing.expectEqual(1, sheet.cssRules.length);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="CSSStyleSheet_replaceSync">
|
||||
{
|
||||
const sheet = new CSSStyleSheet();
|
||||
testing.expectEqual(0, sheet.cssRules.length);
|
||||
|
||||
sheet.replaceSync('.test { color: blue; }');
|
||||
testing.expectEqual(1, sheet.cssRules.length);
|
||||
testing.expectEqual('.test', sheet.cssRules[0].selectorText);
|
||||
testing.expectEqual('blue', sheet.cssRules[0].style.color);
|
||||
|
||||
let replacedAsync = false;
|
||||
testing.async(async () => {
|
||||
const result = await sheet.replace('.async-test { margin: 10px; }');
|
||||
testing.expectTrue(result === sheet);
|
||||
testing.expectEqual(1, sheet.cssRules.length);
|
||||
testing.expectEqual('.async-test', sheet.cssRules[0].selectorText);
|
||||
replacedAsync = true;
|
||||
});
|
||||
testing.eventually(() => testing.expectTrue(replacedAsync));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="CSSStyleRule_cssText">
|
||||
{
|
||||
const sheet = new CSSStyleSheet();
|
||||
sheet.replaceSync('.test { color: red; margin: 10px; }');
|
||||
|
||||
// Check serialization format
|
||||
const cssText = sheet.cssRules[0].cssText;
|
||||
testing.expectTrue(cssText.includes('.test { '));
|
||||
testing.expectTrue(cssText.includes('color: red;'));
|
||||
testing.expectTrue(cssText.includes('margin: 10px;'));
|
||||
testing.expectTrue(cssText.includes('}'));
|
||||
}
|
||||
</script>
|
||||
|
||||
226
src/browser/tests/element/check_visibility.html
Normal file
226
src/browser/tests/element/check_visibility.html
Normal file
@@ -0,0 +1,226 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
<body></body>
|
||||
|
||||
<!-
|
||||
<script id="inline_display_none">
|
||||
{
|
||||
const el = document.createElement("div");
|
||||
document.body.appendChild(el);
|
||||
testing.expectEqual(true, el.checkVisibility());
|
||||
|
||||
el.style.display = "none";
|
||||
testing.expectEqual(false, el.checkVisibility());
|
||||
|
||||
el.style.display = "block";
|
||||
testing.expectEqual(true, el.checkVisibility());
|
||||
|
||||
el.remove();
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="inline_visibility_hidden">
|
||||
{
|
||||
const el = document.createElement("div");
|
||||
document.body.appendChild(el);
|
||||
|
||||
el.style.visibility = "hidden";
|
||||
// Without visibilityProperty option, visibility:hidden is not checked
|
||||
testing.expectEqual(true, el.checkVisibility());
|
||||
// With visibilityProperty: true, visibility:hidden is detected
|
||||
testing.expectEqual(false, el.checkVisibility({ visibilityProperty: true }));
|
||||
|
||||
el.style.visibility = "collapse";
|
||||
testing.expectEqual(false, el.checkVisibility({ visibilityProperty: true }));
|
||||
|
||||
el.style.visibility = "visible";
|
||||
testing.expectEqual(true, el.checkVisibility({ visibilityProperty: true }));
|
||||
|
||||
el.remove();
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="inline_opacity_zero">
|
||||
{
|
||||
const el = document.createElement("div");
|
||||
document.body.appendChild(el);
|
||||
|
||||
el.style.opacity = "0";
|
||||
// Without checkOpacity option, opacity:0 is not checked
|
||||
testing.expectEqual(true, el.checkVisibility());
|
||||
// With checkOpacity: true, opacity:0 is detected
|
||||
testing.expectEqual(false, el.checkVisibility({ checkOpacity: true }));
|
||||
|
||||
el.style.opacity = "0.5";
|
||||
testing.expectEqual(true, el.checkVisibility({ checkOpacity: true }));
|
||||
|
||||
el.style.opacity = "1";
|
||||
testing.expectEqual(true, el.checkVisibility({ checkOpacity: true }));
|
||||
|
||||
el.remove();
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="parent_hidden_hides_child">
|
||||
{
|
||||
const parent = document.createElement("div");
|
||||
const child = document.createElement("span");
|
||||
parent.appendChild(child);
|
||||
document.body.appendChild(parent);
|
||||
|
||||
testing.expectEqual(true, child.checkVisibility());
|
||||
|
||||
// display:none on parent hides children (no option needed)
|
||||
parent.style.display = "none";
|
||||
testing.expectEqual(false, child.checkVisibility());
|
||||
|
||||
// visibility:hidden on parent - needs visibilityProperty option
|
||||
parent.style.display = "block";
|
||||
parent.style.visibility = "hidden";
|
||||
testing.expectEqual(true, child.checkVisibility()); // without option
|
||||
testing.expectEqual(false, child.checkVisibility({ visibilityProperty: true }));
|
||||
|
||||
// opacity:0 on parent - needs checkOpacity option
|
||||
parent.style.visibility = "visible";
|
||||
parent.style.opacity = "0";
|
||||
testing.expectEqual(true, child.checkVisibility()); // without option
|
||||
testing.expectEqual(false, child.checkVisibility({ checkOpacity: true }));
|
||||
|
||||
parent.remove();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style id="style-basic">
|
||||
.hidden-by-class { display: none; }
|
||||
.visible-by-class { display: block; }
|
||||
</style>
|
||||
<script id="style_tag_basic">
|
||||
{
|
||||
const el = document.createElement("div");
|
||||
document.body.appendChild(el);
|
||||
|
||||
testing.expectEqual(true, el.checkVisibility());
|
||||
|
||||
el.className = "hidden-by-class";
|
||||
testing.expectEqual(false, el.checkVisibility());
|
||||
|
||||
el.className = "visible-by-class";
|
||||
testing.expectEqual(true, el.checkVisibility());
|
||||
|
||||
el.className = "";
|
||||
el.remove();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style id="style-specificity">
|
||||
.spec-hidden { display: none; }
|
||||
#spec-visible { display: block; }
|
||||
</style>
|
||||
<script id="specificity_id_beats_class">
|
||||
{
|
||||
const el = document.createElement("div");
|
||||
el.id = "spec-visible";
|
||||
el.className = "spec-hidden";
|
||||
document.body.appendChild(el);
|
||||
|
||||
// ID selector (#spec-visible: display:block) should beat class selector (.spec-hidden: display:none)
|
||||
testing.expectEqual(true, el.checkVisibility());
|
||||
|
||||
el.remove();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style id="style-order-1">
|
||||
.order-test { display: none; }
|
||||
</style>
|
||||
<style id="style-order-2">
|
||||
.order-test { display: block; }
|
||||
</style>
|
||||
<script id="rule_order_later_wins">
|
||||
{
|
||||
const el = document.createElement("div");
|
||||
el.className = "order-test";
|
||||
document.body.appendChild(el);
|
||||
|
||||
// Second style block should win (display: block)
|
||||
testing.expectEqual(true, el.checkVisibility());
|
||||
|
||||
el.remove();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style id="style-override">
|
||||
.should-be-hidden { display: none; }
|
||||
</style>
|
||||
<script id="inline_overrides_stylesheet">
|
||||
{
|
||||
const el = document.createElement("div");
|
||||
el.className = "should-be-hidden";
|
||||
document.body.appendChild(el);
|
||||
|
||||
testing.expectEqual(false, el.checkVisibility());
|
||||
|
||||
// Inline style should override
|
||||
el.style.display = "block";
|
||||
testing.expectEqual(true, el.checkVisibility());
|
||||
|
||||
el.remove();
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="dynamic_style_element">
|
||||
{
|
||||
const el = document.createElement("div");
|
||||
el.className = "dynamic-style-test";
|
||||
document.body.appendChild(el);
|
||||
|
||||
testing.expectEqual(true, el.checkVisibility());
|
||||
|
||||
// Add a style element
|
||||
const style = document.createElement("style");
|
||||
style.textContent = ".dynamic-style-test { display: none; }";
|
||||
document.head.appendChild(style);
|
||||
|
||||
testing.expectEqual(false, el.checkVisibility());
|
||||
|
||||
// Remove the style element
|
||||
style.remove();
|
||||
testing.expectEqual(true, el.checkVisibility());
|
||||
|
||||
el.remove();
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="deep_nesting">
|
||||
{
|
||||
const levels = 5;
|
||||
let current = document.body;
|
||||
const elements = [];
|
||||
|
||||
for (let i = 0; i < levels; i++) {
|
||||
const el = document.createElement("div");
|
||||
current.appendChild(el);
|
||||
elements.push(el);
|
||||
current = el;
|
||||
}
|
||||
|
||||
// All should be visible
|
||||
for (let i = 0; i < levels; i++) {
|
||||
testing.expectEqual(true, elements[i].checkVisibility());
|
||||
}
|
||||
|
||||
// Hide middle element
|
||||
elements[2].style.display = "none";
|
||||
|
||||
// Elements 0, 1 should still be visible
|
||||
testing.expectEqual(true, elements[0].checkVisibility());
|
||||
testing.expectEqual(true, elements[1].checkVisibility());
|
||||
|
||||
// Elements 2, 3, 4 should be hidden
|
||||
testing.expectEqual(false, elements[2].checkVisibility());
|
||||
testing.expectEqual(false, elements[3].checkVisibility());
|
||||
testing.expectEqual(false, elements[4].checkVisibility());
|
||||
|
||||
elements[0].remove();
|
||||
}
|
||||
</script>
|
||||
@@ -131,3 +131,17 @@
|
||||
testing.eventually(() => testing.expectEqual(true, result));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="style-tag-content-parsing">
|
||||
{
|
||||
const style = document.createElement("style");
|
||||
style.textContent = '.content-test { padding: 5px; }';
|
||||
document.head.appendChild(style);
|
||||
|
||||
const sheet = style.sheet;
|
||||
testing.expectTrue(sheet instanceof CSSStyleSheet);
|
||||
testing.expectEqual(1, sheet.cssRules.length);
|
||||
testing.expectEqual('.content-test', sheet.cssRules[0].selectorText);
|
||||
testing.expectEqual('5px', sheet.cssRules[0].style.padding);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -564,7 +564,7 @@ pub fn elementFromPoint(self: *Document, x: f64, y: f64, page: *Page) !?*Element
|
||||
while (stack.items.len > 0) {
|
||||
const node = stack.pop() orelse break;
|
||||
if (node.is(Element)) |element| {
|
||||
if (element.checkVisibility(page)) {
|
||||
if (element.checkVisibilityCached(null, page)) {
|
||||
const rect = element.getBoundingClientRectForVisible(page);
|
||||
if (x >= rect.getLeft() and x <= rect.getRight() and y >= rect.getTop() and y <= rect.getBottom()) {
|
||||
topmost = element;
|
||||
|
||||
@@ -24,6 +24,7 @@ const String = @import("../../string.zig").String;
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
const StyleManager = @import("../StyleManager.zig");
|
||||
const reflect = @import("../reflect.zig");
|
||||
|
||||
const Node = @import("Node.zig");
|
||||
@@ -1024,20 +1025,32 @@ pub fn parentElement(self: *Element) ?*Element {
|
||||
return self._proto.parentElement();
|
||||
}
|
||||
|
||||
pub fn checkVisibility(self: *Element, page: *Page) bool {
|
||||
var current: ?*Element = self;
|
||||
/// Cache for visibility checks - re-exported from StyleManager for convenience.
|
||||
pub const VisibilityCache = StyleManager.VisibilityCache;
|
||||
|
||||
while (current) |el| {
|
||||
if (el.getStyle(page)) |style| {
|
||||
const display = style.asCSSStyleDeclaration().getPropertyValue("display", page);
|
||||
if (std.mem.eql(u8, display, "none")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
current = el.parentElement();
|
||||
}
|
||||
/// Cache for pointer-events checks - re-exported from StyleManager for convenience.
|
||||
pub const PointerEventsCache = StyleManager.PointerEventsCache;
|
||||
|
||||
return true;
|
||||
pub fn hasPointerEventsNone(self: *Element, cache: ?*PointerEventsCache, page: *Page) bool {
|
||||
return page._style_manager.hasPointerEventsNone(self, cache);
|
||||
}
|
||||
|
||||
pub fn checkVisibilityCached(self: *Element, cache: ?*VisibilityCache, page: *Page) bool {
|
||||
return !page._style_manager.isHidden(self, cache, .{});
|
||||
}
|
||||
|
||||
const CheckVisibilityOpts = struct {
|
||||
checkOpacity: bool = false,
|
||||
opacityProperty: bool = false,
|
||||
checkVisibilityCSS: bool = false,
|
||||
visibilityProperty: bool = false,
|
||||
};
|
||||
pub fn checkVisibility(self: *Element, opts_: ?CheckVisibilityOpts, page: *Page) bool {
|
||||
const opts = opts_ orelse CheckVisibilityOpts{};
|
||||
return !page._style_manager.isHidden(self, null, .{
|
||||
.check_opacity = opts.checkOpacity or opts.opacityProperty,
|
||||
.check_visibility = opts.visibilityProperty or opts.checkVisibilityCSS,
|
||||
});
|
||||
}
|
||||
|
||||
fn getElementDimensions(self: *Element, page: *Page) struct { width: f64, height: f64 } {
|
||||
@@ -1074,7 +1087,7 @@ fn getElementDimensions(self: *Element, page: *Page) struct { width: f64, height
|
||||
}
|
||||
|
||||
pub fn getClientWidth(self: *Element, page: *Page) f64 {
|
||||
if (!self.checkVisibility(page)) {
|
||||
if (!self.checkVisibilityCached(null, page)) {
|
||||
return 0.0;
|
||||
}
|
||||
const dims = self.getElementDimensions(page);
|
||||
@@ -1082,7 +1095,7 @@ pub fn getClientWidth(self: *Element, page: *Page) f64 {
|
||||
}
|
||||
|
||||
pub fn getClientHeight(self: *Element, page: *Page) f64 {
|
||||
if (!self.checkVisibility(page)) {
|
||||
if (!self.checkVisibilityCached(null, page)) {
|
||||
return 0.0;
|
||||
}
|
||||
const dims = self.getElementDimensions(page);
|
||||
@@ -1090,7 +1103,7 @@ pub fn getClientHeight(self: *Element, page: *Page) f64 {
|
||||
}
|
||||
|
||||
pub fn getBoundingClientRect(self: *Element, page: *Page) DOMRect {
|
||||
if (!self.checkVisibility(page)) {
|
||||
if (!self.checkVisibilityCached(null, page)) {
|
||||
return .{
|
||||
._x = 0.0,
|
||||
._y = 0.0,
|
||||
@@ -1120,7 +1133,7 @@ pub fn getBoundingClientRectForVisible(self: *Element, page: *Page) DOMRect {
|
||||
}
|
||||
|
||||
pub fn getClientRects(self: *Element, page: *Page) ![]DOMRect {
|
||||
if (!self.checkVisibility(page)) {
|
||||
if (!self.checkVisibilityCached(null, page)) {
|
||||
return &.{};
|
||||
}
|
||||
const rects = try page.call_arena.alloc(DOMRect, 1);
|
||||
@@ -1165,7 +1178,7 @@ pub fn getScrollWidth(self: *Element, page: *Page) f64 {
|
||||
}
|
||||
|
||||
pub fn getOffsetHeight(self: *Element, page: *Page) f64 {
|
||||
if (!self.checkVisibility(page)) {
|
||||
if (!self.checkVisibilityCached(null, page)) {
|
||||
return 0.0;
|
||||
}
|
||||
const dims = self.getElementDimensions(page);
|
||||
@@ -1173,7 +1186,7 @@ pub fn getOffsetHeight(self: *Element, page: *Page) f64 {
|
||||
}
|
||||
|
||||
pub fn getOffsetWidth(self: *Element, page: *Page) f64 {
|
||||
if (!self.checkVisibility(page)) {
|
||||
if (!self.checkVisibilityCached(null, page)) {
|
||||
return 0.0;
|
||||
}
|
||||
const dims = self.getElementDimensions(page);
|
||||
@@ -1181,14 +1194,14 @@ pub fn getOffsetWidth(self: *Element, page: *Page) f64 {
|
||||
}
|
||||
|
||||
pub fn getOffsetTop(self: *Element, page: *Page) f64 {
|
||||
if (!self.checkVisibility(page)) {
|
||||
if (!self.checkVisibilityCached(null, page)) {
|
||||
return 0.0;
|
||||
}
|
||||
return calculateDocumentPosition(self.asNode());
|
||||
}
|
||||
|
||||
pub fn getOffsetLeft(self: *Element, page: *Page) f64 {
|
||||
if (!self.checkVisibility(page)) {
|
||||
if (!self.checkVisibilityCached(null, page)) {
|
||||
return 0.0;
|
||||
}
|
||||
return calculateSiblingPosition(self.asNode());
|
||||
|
||||
@@ -2,29 +2,42 @@ const std = @import("std");
|
||||
const js = @import("../../js/js.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
|
||||
const CSSStyleRule = @import("CSSStyleRule.zig");
|
||||
|
||||
const CSSRule = @This();
|
||||
|
||||
pub const Type = enum(u16) {
|
||||
style = 1,
|
||||
charset = 2,
|
||||
import = 3,
|
||||
media = 4,
|
||||
font_face = 5,
|
||||
page = 6,
|
||||
keyframes = 7,
|
||||
keyframe = 8,
|
||||
margin = 9,
|
||||
namespace = 10,
|
||||
counter_style = 11,
|
||||
supports = 12,
|
||||
document = 13,
|
||||
font_feature_values = 14,
|
||||
viewport = 15,
|
||||
region_style = 16,
|
||||
pub const Type = union(enum) {
|
||||
style: *CSSStyleRule,
|
||||
charset: void,
|
||||
import: void,
|
||||
media: void,
|
||||
font_face: void,
|
||||
page: void,
|
||||
keyframes: void,
|
||||
keyframe: void,
|
||||
margin: void,
|
||||
namespace: void,
|
||||
counter_style: void,
|
||||
supports: void,
|
||||
document: void,
|
||||
font_feature_values: void,
|
||||
viewport: void,
|
||||
region_style: void,
|
||||
};
|
||||
|
||||
_type: Type,
|
||||
|
||||
pub fn as(self: *CSSRule, comptime T: type) *T {
|
||||
return self.is(T).?;
|
||||
}
|
||||
|
||||
pub fn is(self: *CSSRule, comptime T: type) ?*T {
|
||||
switch (self._type) {
|
||||
.style => |r| return if (T == CSSStyleRule) r else null,
|
||||
else => return null,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(rule_type: Type, page: *Page) !*CSSRule {
|
||||
return page._factory.create(CSSRule{
|
||||
._type = rule_type,
|
||||
@@ -32,23 +45,14 @@ pub fn init(rule_type: Type, page: *Page) !*CSSRule {
|
||||
}
|
||||
|
||||
pub fn getType(self: *const CSSRule) u16 {
|
||||
return @intFromEnum(self._type);
|
||||
return @as(u16, @intFromEnum(std.meta.activeTag(self._type))) + 1;
|
||||
}
|
||||
|
||||
pub fn getCssText(self: *const CSSRule, page: *Page) []const u8 {
|
||||
_ = self;
|
||||
_ = page;
|
||||
pub fn getCssText(_: *const CSSRule, _: *Page) []const u8 {
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn setCssText(self: *CSSRule, text: []const u8, page: *Page) !void {
|
||||
_ = self;
|
||||
_ = text;
|
||||
_ = page;
|
||||
}
|
||||
|
||||
pub fn getParentRule(self: *const CSSRule) ?*CSSRule {
|
||||
_ = self;
|
||||
pub fn getParentRule(_: *const CSSRule) ?*CSSRule {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -62,8 +66,8 @@ pub const JsApi = struct {
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "CSSRule";
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const STYLE_RULE = 1;
|
||||
@@ -84,7 +88,7 @@ pub const JsApi = struct {
|
||||
pub const REGION_STYLE_RULE = 16;
|
||||
|
||||
pub const @"type" = bridge.accessor(CSSRule.getType, null, .{});
|
||||
pub const cssText = bridge.accessor(CSSRule.getCssText, CSSRule.setCssText, .{});
|
||||
pub const cssText = bridge.accessor(CSSRule.getCssText, null, .{});
|
||||
pub const parentRule = bridge.accessor(CSSRule.getParentRule, null, .{});
|
||||
pub const parentStyleSheet = bridge.accessor(CSSRule.getParentStyleSheet, null, .{});
|
||||
};
|
||||
|
||||
@@ -5,21 +5,39 @@ const CSSRule = @import("CSSRule.zig");
|
||||
|
||||
const CSSRuleList = @This();
|
||||
|
||||
_rules: []*CSSRule = &.{},
|
||||
_rules: std.ArrayList(*CSSRule) = .empty,
|
||||
|
||||
pub fn init(page: *Page) !*CSSRuleList {
|
||||
return page._factory.create(CSSRuleList{});
|
||||
}
|
||||
|
||||
pub fn length(self: *const CSSRuleList) u32 {
|
||||
return @intCast(self._rules.len);
|
||||
return @intCast(self._rules.items.len);
|
||||
}
|
||||
|
||||
pub fn item(self: *const CSSRuleList, index: usize) ?*CSSRule {
|
||||
if (index >= self._rules.len) {
|
||||
if (index >= self._rules.items.len) {
|
||||
return null;
|
||||
}
|
||||
return self._rules[index];
|
||||
return self._rules.items[index];
|
||||
}
|
||||
|
||||
pub fn insert(self: *CSSRuleList, index: u32, rule: *CSSRule, page: *Page) !void {
|
||||
if (index > self._rules.items.len) {
|
||||
return error.IndexSizeError;
|
||||
}
|
||||
try self._rules.insert(page.arena, index, rule);
|
||||
}
|
||||
|
||||
pub fn remove(self: *CSSRuleList, index: u32) !void {
|
||||
if (index >= self._rules.items.len) {
|
||||
return error.IndexSizeError;
|
||||
}
|
||||
_ = self._rules.orderedRemove(index);
|
||||
}
|
||||
|
||||
pub fn clear(self: *CSSRuleList) void {
|
||||
self._rules.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
|
||||
@@ -77,10 +77,11 @@ pub fn item(self: *const CSSStyleDeclaration, index: u32) []const u8 {
|
||||
|
||||
pub fn getPropertyValue(self: *const CSSStyleDeclaration, property_name: []const u8, page: *Page) []const u8 {
|
||||
const normalized = normalizePropertyName(property_name, &page.buf);
|
||||
const prop = self.findProperty(normalized) orelse {
|
||||
const wrapped = String.wrap(normalized);
|
||||
const prop = self.findProperty(wrapped) orelse {
|
||||
// Only return default values for computed styles
|
||||
if (self._is_computed) {
|
||||
return getDefaultPropertyValue(self, normalized);
|
||||
return getDefaultPropertyValue(self, wrapped);
|
||||
}
|
||||
return "";
|
||||
};
|
||||
@@ -89,7 +90,7 @@ pub fn getPropertyValue(self: *const CSSStyleDeclaration, property_name: []const
|
||||
|
||||
pub fn getPropertyPriority(self: *const CSSStyleDeclaration, property_name: []const u8, page: *Page) []const u8 {
|
||||
const normalized = normalizePropertyName(property_name, &page.buf);
|
||||
const prop = self.findProperty(normalized) orelse return "";
|
||||
const prop = self.findProperty(.wrap(normalized)) orelse return "";
|
||||
return if (prop._important) "important" else "";
|
||||
}
|
||||
|
||||
@@ -120,7 +121,7 @@ fn setPropertyImpl(self: *CSSStyleDeclaration, property_name: []const u8, value:
|
||||
const normalized_value = try normalizePropertyValue(page.call_arena, normalized, value);
|
||||
|
||||
// Find existing property
|
||||
if (self.findProperty(normalized)) |existing| {
|
||||
if (self.findProperty(.wrap(normalized))) |existing| {
|
||||
existing._value = try String.init(page.arena, normalized_value, .{});
|
||||
existing._important = important;
|
||||
return;
|
||||
@@ -144,7 +145,7 @@ pub fn removeProperty(self: *CSSStyleDeclaration, property_name: []const u8, pag
|
||||
|
||||
fn removePropertyImpl(self: *CSSStyleDeclaration, property_name: []const u8, page: *Page) ![]const u8 {
|
||||
const normalized = normalizePropertyName(property_name, &page.buf);
|
||||
const prop = self.findProperty(normalized) orelse return "";
|
||||
const prop = self.findProperty(.wrap(normalized)) orelse return "";
|
||||
|
||||
// the value might not be on the heap (it could be inlined in the small string
|
||||
// optimization), so we need to dupe it.
|
||||
@@ -172,16 +173,12 @@ pub fn setFloat(self: *CSSStyleDeclaration, value_: ?[]const u8, page: *Page) !v
|
||||
}
|
||||
|
||||
pub fn getCssText(self: *const CSSStyleDeclaration, page: *Page) ![]const u8 {
|
||||
if (self._element == null) return "";
|
||||
|
||||
var buf = std.Io.Writer.Allocating.init(page.call_arena);
|
||||
try self.format(&buf.writer);
|
||||
return buf.written();
|
||||
}
|
||||
|
||||
pub fn setCssText(self: *CSSStyleDeclaration, text: []const u8, page: *Page) !void {
|
||||
if (self._element == null) return;
|
||||
|
||||
// Clear existing properties
|
||||
var node = self._properties.first;
|
||||
while (node) |n| {
|
||||
@@ -212,11 +209,11 @@ pub fn format(self: *const CSSStyleDeclaration, writer: *std.Io.Writer) !void {
|
||||
}
|
||||
}
|
||||
|
||||
fn findProperty(self: *const CSSStyleDeclaration, name: []const u8) ?*Property {
|
||||
pub fn findProperty(self: *const CSSStyleDeclaration, name: String) ?*Property {
|
||||
var node = self._properties.first;
|
||||
while (node) |n| {
|
||||
const prop = Property.fromNodeLink(n);
|
||||
if (prop._name.eqlSlice(name)) {
|
||||
if (prop._name.eql(name)) {
|
||||
return prop;
|
||||
}
|
||||
node = n.next;
|
||||
@@ -621,26 +618,36 @@ fn isLengthProperty(name: []const u8) bool {
|
||||
return length_properties.has(name);
|
||||
}
|
||||
|
||||
fn getDefaultPropertyValue(self: *const CSSStyleDeclaration, normalized_name: []const u8) []const u8 {
|
||||
if (std.mem.eql(u8, normalized_name, "visibility")) {
|
||||
return "visible";
|
||||
fn getDefaultPropertyValue(self: *const CSSStyleDeclaration, name: String) []const u8 {
|
||||
switch (name.len) {
|
||||
5 => {
|
||||
if (name.eql(comptime .wrap("color"))) {
|
||||
const element = self._element orelse return "";
|
||||
return getDefaultColor(element);
|
||||
}
|
||||
},
|
||||
7 => {
|
||||
if (name.eql(comptime .wrap("opacity"))) {
|
||||
return "1";
|
||||
}
|
||||
if (name.eql(comptime .wrap("display"))) {
|
||||
const element = self._element orelse return "";
|
||||
return getDefaultDisplay(element);
|
||||
}
|
||||
},
|
||||
10 => {
|
||||
if (name.eql(comptime .wrap("visibility"))) {
|
||||
return "visible";
|
||||
}
|
||||
},
|
||||
16 => {
|
||||
if (name.eqlSlice("background-color")) {
|
||||
// transparent
|
||||
return "rgba(0, 0, 0, 0)";
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
if (std.mem.eql(u8, normalized_name, "opacity")) {
|
||||
return "1";
|
||||
}
|
||||
if (std.mem.eql(u8, normalized_name, "display")) {
|
||||
const element = self._element orelse return "";
|
||||
return getDefaultDisplay(element);
|
||||
}
|
||||
if (std.mem.eql(u8, normalized_name, "color")) {
|
||||
const element = self._element orelse return "";
|
||||
return getDefaultColor(element);
|
||||
}
|
||||
if (std.mem.eql(u8, normalized_name, "background-color")) {
|
||||
// transparent
|
||||
return "rgba(0, 0, 0, 0)";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
@@ -2,19 +2,20 @@ const std = @import("std");
|
||||
const js = @import("../../js/js.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
const CSSRule = @import("CSSRule.zig");
|
||||
const CSSStyleDeclaration = @import("CSSStyleDeclaration.zig");
|
||||
const CSSStyleProperties = @import("CSSStyleProperties.zig");
|
||||
|
||||
const CSSStyleRule = @This();
|
||||
|
||||
_proto: *CSSRule,
|
||||
_selector_text: []const u8 = "",
|
||||
_style: ?*CSSStyleDeclaration = null,
|
||||
_style: ?*CSSStyleProperties = null,
|
||||
|
||||
pub fn init(page: *Page) !*CSSStyleRule {
|
||||
const rule = try CSSRule.init(.style, page);
|
||||
return page._factory.create(CSSStyleRule{
|
||||
._proto = rule,
|
||||
const style_rule = try page._factory.create(CSSStyleRule{
|
||||
._proto = undefined,
|
||||
});
|
||||
style_rule._proto = try CSSRule.init(.{ .style = style_rule }, page);
|
||||
return style_rule;
|
||||
}
|
||||
|
||||
pub fn getSelectorText(self: *const CSSStyleRule) []const u8 {
|
||||
@@ -25,24 +26,35 @@ pub fn setSelectorText(self: *CSSStyleRule, text: []const u8, page: *Page) !void
|
||||
self._selector_text = try page.dupeString(text);
|
||||
}
|
||||
|
||||
pub fn getStyle(self: *CSSStyleRule, page: *Page) !*CSSStyleDeclaration {
|
||||
pub fn getStyle(self: *CSSStyleRule, page: *Page) !*CSSStyleProperties {
|
||||
if (self._style) |style| {
|
||||
return style;
|
||||
}
|
||||
const style = try CSSStyleDeclaration.init(null, false, page);
|
||||
const style = try CSSStyleProperties.init(null, false, page);
|
||||
self._style = style;
|
||||
return style;
|
||||
}
|
||||
|
||||
pub fn getCssText(self: *CSSStyleRule, page: *Page) ![]const u8 {
|
||||
const style_props = try self.getStyle(page);
|
||||
const style = style_props.asCSSStyleDeclaration();
|
||||
var buf = std.Io.Writer.Allocating.init(page.call_arena);
|
||||
try buf.writer.print("{s} {{ ", .{self._selector_text});
|
||||
try style.format(&buf.writer);
|
||||
try buf.writer.writeAll(" }");
|
||||
return buf.written();
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(CSSStyleRule);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "CSSStyleRule";
|
||||
pub const prototype_chain = bridge.prototypeChain(CSSRule);
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const selectorText = bridge.accessor(CSSStyleRule.getSelectorText, CSSStyleRule.setSelectorText, .{});
|
||||
pub const style = bridge.accessor(CSSStyleRule.getStyle, null, .{});
|
||||
pub const cssText = bridge.accessor(CSSStyleRule.getCssText, null, .{});
|
||||
};
|
||||
|
||||
@@ -4,9 +4,19 @@ const Page = @import("../../Page.zig");
|
||||
const Element = @import("../Element.zig");
|
||||
const CSSRuleList = @import("CSSRuleList.zig");
|
||||
const CSSRule = @import("CSSRule.zig");
|
||||
const CSSStyleRule = @import("CSSStyleRule.zig");
|
||||
const Parser = @import("../../css/Parser.zig");
|
||||
|
||||
const CSSStyleSheet = @This();
|
||||
|
||||
pub const CSSError = error{
|
||||
OutOfMemory,
|
||||
IndexSizeError,
|
||||
WriteFailed,
|
||||
StringTooLarge,
|
||||
SyntaxError,
|
||||
};
|
||||
|
||||
_href: ?[]const u8 = null,
|
||||
_title: []const u8 = "",
|
||||
_disabled: bool = false,
|
||||
@@ -44,8 +54,17 @@ pub fn setDisabled(self: *CSSStyleSheet, disabled: bool) void {
|
||||
|
||||
pub fn getCssRules(self: *CSSStyleSheet, page: *Page) !*CSSRuleList {
|
||||
if (self._css_rules) |rules| return rules;
|
||||
|
||||
const rules = try CSSRuleList.init(page);
|
||||
self._css_rules = rules;
|
||||
|
||||
if (self.getOwnerNode()) |owner| {
|
||||
if (owner.is(Element.Html.Style)) |style| {
|
||||
const text = try style.asNode().getTextContentAlloc(page.call_arena);
|
||||
try self.replaceSync(text, page);
|
||||
}
|
||||
}
|
||||
|
||||
return rules;
|
||||
}
|
||||
|
||||
@@ -53,31 +72,60 @@ pub fn getOwnerRule(self: *const CSSStyleSheet) ?*CSSRule {
|
||||
return self._owner_rule;
|
||||
}
|
||||
|
||||
pub fn insertRule(self: *CSSStyleSheet, rule: []const u8, index: u32, page: *Page) !u32 {
|
||||
_ = self;
|
||||
_ = rule;
|
||||
_ = index;
|
||||
_ = page;
|
||||
return 0;
|
||||
pub fn insertRule(self: *CSSStyleSheet, rule: []const u8, maybe_index: ?u32, page: *Page) !u32 {
|
||||
const index = maybe_index orelse 0;
|
||||
var it = Parser.parseStylesheet(rule);
|
||||
const parsed_rule = it.next() orelse return error.SyntaxError;
|
||||
|
||||
const style_rule = try CSSStyleRule.init(page);
|
||||
try style_rule.setSelectorText(parsed_rule.selector, page);
|
||||
|
||||
const style_props = try style_rule.getStyle(page);
|
||||
const style = style_props.asCSSStyleDeclaration();
|
||||
try style.setCssText(parsed_rule.block, page);
|
||||
|
||||
const rules = try self.getCssRules(page);
|
||||
try rules.insert(index, style_rule._proto, page);
|
||||
|
||||
// Notify StyleManager that rules have changed
|
||||
page._style_manager.sheetModified();
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
pub fn deleteRule(self: *CSSStyleSheet, index: u32, page: *Page) !void {
|
||||
_ = self;
|
||||
_ = index;
|
||||
_ = page;
|
||||
const rules = try self.getCssRules(page);
|
||||
try rules.remove(index);
|
||||
|
||||
// Notify StyleManager that rules have changed
|
||||
page._style_manager.sheetModified();
|
||||
}
|
||||
|
||||
pub fn replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !js.Promise {
|
||||
_ = self;
|
||||
_ = text;
|
||||
// TODO: clear self.css_rules
|
||||
return page.js.local.?.resolvePromise({});
|
||||
pub fn replace(self: *CSSStyleSheet, text: []const u8, page: *Page) CSSError!js.Promise {
|
||||
try self.replaceSync(text, page);
|
||||
return page.js.local.?.resolvePromise(self);
|
||||
}
|
||||
|
||||
pub fn replaceSync(self: *CSSStyleSheet, text: []const u8) !void {
|
||||
_ = self;
|
||||
_ = text;
|
||||
// TODO: clear self.css_rules
|
||||
pub fn replaceSync(self: *CSSStyleSheet, text: []const u8, page: *Page) CSSError!void {
|
||||
const rules = try self.getCssRules(page);
|
||||
rules.clear();
|
||||
|
||||
var it = Parser.parseStylesheet(text);
|
||||
var index: u32 = 0;
|
||||
while (it.next()) |parsed_rule| {
|
||||
const style_rule = try CSSStyleRule.init(page);
|
||||
try style_rule.setSelectorText(parsed_rule.selector, page);
|
||||
|
||||
const style_props = try style_rule.getStyle(page);
|
||||
const style = style_props.asCSSStyleDeclaration();
|
||||
try style.setCssText(parsed_rule.block, page);
|
||||
|
||||
try rules.insert(index, style_rule._proto, page);
|
||||
index += 1;
|
||||
}
|
||||
|
||||
// Notify StyleManager that rules have changed
|
||||
page._style_manager.sheetModified();
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
@@ -96,13 +144,15 @@ pub const JsApi = struct {
|
||||
pub const disabled = bridge.accessor(CSSStyleSheet.getDisabled, CSSStyleSheet.setDisabled, .{});
|
||||
pub const cssRules = bridge.accessor(CSSStyleSheet.getCssRules, null, .{});
|
||||
pub const ownerRule = bridge.accessor(CSSStyleSheet.getOwnerRule, null, .{});
|
||||
pub const insertRule = bridge.function(CSSStyleSheet.insertRule, .{});
|
||||
pub const deleteRule = bridge.function(CSSStyleSheet.deleteRule, .{});
|
||||
pub const insertRule = bridge.function(CSSStyleSheet.insertRule, .{ .dom_exception = true });
|
||||
pub const deleteRule = bridge.function(CSSStyleSheet.deleteRule, .{ .dom_exception = true });
|
||||
pub const replace = bridge.function(CSSStyleSheet.replace, .{});
|
||||
pub const replaceSync = bridge.function(CSSStyleSheet.replaceSync, .{});
|
||||
};
|
||||
|
||||
const testing = @import("../../../testing.zig");
|
||||
test "WebApi: CSSStyleSheet" {
|
||||
const filter: testing.LogFilter = .init(&.{.js});
|
||||
defer filter.deinit();
|
||||
try testing.htmlRunner("css/stylesheet.html", .{});
|
||||
}
|
||||
|
||||
@@ -5,19 +5,32 @@ const CSSStyleSheet = @import("CSSStyleSheet.zig");
|
||||
|
||||
const StyleSheetList = @This();
|
||||
|
||||
_sheets: []*CSSStyleSheet = &.{},
|
||||
_sheets: std.ArrayList(*CSSStyleSheet) = .empty,
|
||||
|
||||
pub fn init(page: *Page) !*StyleSheetList {
|
||||
return page._factory.create(StyleSheetList{});
|
||||
}
|
||||
|
||||
pub fn length(self: *const StyleSheetList) u32 {
|
||||
return @intCast(self._sheets.len);
|
||||
return @intCast(self._sheets.items.len);
|
||||
}
|
||||
|
||||
pub fn item(self: *const StyleSheetList, index: usize) ?*CSSStyleSheet {
|
||||
if (index >= self._sheets.len) return null;
|
||||
return self._sheets[index];
|
||||
if (index >= self._sheets.items.len) return null;
|
||||
return self._sheets.items[index];
|
||||
}
|
||||
|
||||
pub fn add(self: *StyleSheetList, sheet: *CSSStyleSheet, page: *Page) !void {
|
||||
try self._sheets.append(page.arena, sheet);
|
||||
}
|
||||
|
||||
pub fn remove(self: *StyleSheetList, sheet: *CSSStyleSheet) void {
|
||||
for (self._sheets.items, 0..) |s, i| {
|
||||
if (s == sheet) {
|
||||
_ = self._sheets.orderedRemove(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
|
||||
@@ -94,10 +94,20 @@ pub fn getSheet(self: *Style, page: *Page) !?*CSSStyleSheet {
|
||||
if (self._sheet) |sheet| return sheet;
|
||||
const sheet = try CSSStyleSheet.initWithOwner(self.asElement(), page);
|
||||
self._sheet = sheet;
|
||||
|
||||
const sheets = try page.document.getStyleSheets(page);
|
||||
try sheets.add(sheet, page);
|
||||
|
||||
return sheet;
|
||||
}
|
||||
|
||||
pub fn styleAddedCallback(self: *Style, page: *Page) !void {
|
||||
// Force stylesheet initialization so rules are parsed immediately
|
||||
if (self.getSheet(page) catch null) |_| {
|
||||
// Notify StyleManager about the new stylesheet
|
||||
page._style_manager.sheetModified();
|
||||
}
|
||||
|
||||
// if we're planning on navigating to another page, don't trigger load event.
|
||||
if (page.isGoingAway()) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user