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;