diff --git a/src/ArenaPool.zig b/src/ArenaPool.zig
index 2e3f25a4..a48e00a7 100644
--- a/src/ArenaPool.zig
+++ b/src/ArenaPool.zig
@@ -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" {
diff --git a/src/SemanticTree.zig b/src/SemanticTree.zig
index af8720e9..6366890f 100644
--- a/src/SemanticTree.zig
+++ b/src/SemanticTree.zig
@@ -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| {
diff --git a/src/browser/Page.zig b/src/browser/Page.zig
index 9768d5b8..bb51be90 100644
--- a/src/browser/Page.zig
+++ b/src/browser/Page.zig
@@ -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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/tests/element/html/style.html b/src/browser/tests/element/html/style.html
index 8abbb229..ee393846 100644
--- a/src/browser/tests/element/html/style.html
+++ b/src/browser/tests/element/html/style.html
@@ -131,3 +131,17 @@
testing.eventually(() => testing.expectEqual(true, result));
}
+
+
diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig
index 45510a89..91e439be 100644
--- a/src/browser/webapi/Document.zig
+++ b/src/browser/webapi/Document.zig
@@ -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;
diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig
index 160e31f4..e9f77e45 100644
--- a/src/browser/webapi/Element.zig
+++ b/src/browser/webapi/Element.zig
@@ -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());
diff --git a/src/browser/webapi/css/CSSRule.zig b/src/browser/webapi/css/CSSRule.zig
index dcf41db9..a96fe82b 100644
--- a/src/browser/webapi/css/CSSRule.zig
+++ b/src/browser/webapi/css/CSSRule.zig
@@ -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, .{});
};
diff --git a/src/browser/webapi/css/CSSRuleList.zig b/src/browser/webapi/css/CSSRuleList.zig
index 7e727a56..6c159aa2 100644
--- a/src/browser/webapi/css/CSSRuleList.zig
+++ b/src/browser/webapi/css/CSSRuleList.zig
@@ -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 {
diff --git a/src/browser/webapi/css/CSSStyleDeclaration.zig b/src/browser/webapi/css/CSSStyleDeclaration.zig
index ebaafbe0..d207c11a 100644
--- a/src/browser/webapi/css/CSSStyleDeclaration.zig
+++ b/src/browser/webapi/css/CSSStyleDeclaration.zig
@@ -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 "";
}
diff --git a/src/browser/webapi/css/CSSStyleRule.zig b/src/browser/webapi/css/CSSStyleRule.zig
index cff5ebae..561d39c1 100644
--- a/src/browser/webapi/css/CSSStyleRule.zig
+++ b/src/browser/webapi/css/CSSStyleRule.zig
@@ -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, .{});
};
diff --git a/src/browser/webapi/css/CSSStyleSheet.zig b/src/browser/webapi/css/CSSStyleSheet.zig
index 5500f63c..675e87f9 100644
--- a/src/browser/webapi/css/CSSStyleSheet.zig
+++ b/src/browser/webapi/css/CSSStyleSheet.zig
@@ -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", .{});
}
diff --git a/src/browser/webapi/css/StyleSheetList.zig b/src/browser/webapi/css/StyleSheetList.zig
index c44dc601..c0732c73 100644
--- a/src/browser/webapi/css/StyleSheetList.zig
+++ b/src/browser/webapi/css/StyleSheetList.zig
@@ -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 {
diff --git a/src/browser/webapi/element/html/Style.zig b/src/browser/webapi/element/html/Style.zig
index 131b7634..e6cac8c3 100644
--- a/src/browser/webapi/element/html/Style.zig
+++ b/src/browser/webapi/element/html/Style.zig
@@ -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;