diff --git a/src/browser/css/Parser.zig b/src/browser/css/Parser.zig index 34309631..526aa1db 100644 --- a/src/browser/css/Parser.zig +++ b/src/browser/css/Parser.zig @@ -293,3 +293,143 @@ 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; + + // Skip leading trivia + while (self.stream.peek()) |peeked| { + if (!isWhitespaceOrComment(peeked.token)) break; + _ = self.stream.next(); + } + + while (true) { + const peeked = self.stream.peek() orelse return null; + + if (isCurlyBlockStart(peeked.token)) { + 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 (isCurlyBlockStart(span.token)) { + depth += 1; + } else if (isCurlyBlockEnd(span.token)) { + 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; + } + + 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 (!isCurlyBlockStart(span.token)) return; + + var depth: usize = 1; + while (true) { + const next_span = self.stream.next() orelse return; + if (isCurlyBlockStart(next_span.token)) { + depth += 1; + } else if (isCurlyBlockEnd(next_span.token)) { + 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 (isCurlyBlockStart(span.token)) { + depth += 1; + saw_block = true; + } else if (isCurlyBlockEnd(span.token)) { + if (depth > 0) depth -= 1; + if (saw_block and depth == 0) return; + } + } + } +}; + +fn isCurlyBlockStart(token: Tokenizer.Token) bool { + return switch (token) { + .curly_bracket_block => true, + else => false, + }; +} + +fn isCurlyBlockEnd(token: Tokenizer.Token) bool { + return switch (token) { + .close_curly_bracket => true, + else => false, + }; +} diff --git a/src/browser/webapi/css/CSSRuleList.zig b/src/browser/webapi/css/CSSRuleList.zig index 7e727a56..7f9d1a77 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.ArrayListUnmanaged(*CSSRule) = .{}, 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; // Or standard DOMException mapped error + } + try self._rules.insert(page.arena, index, rule); +} + +pub fn remove(self: *CSSRuleList, index: u32) void { + if (index >= self._rules.items.len) { + return; // Ignore or throw? Standard says IndexSizeError DOMException, but we might just no-op or return an error depending on the caller. + } + _ = self._rules.orderedRemove(index); +} + +pub fn clear(self: *CSSRuleList) void { + self._rules.clearRetainingCapacity(); } pub const JsApi = struct { diff --git a/src/browser/webapi/css/CSSStyleRule.zig b/src/browser/webapi/css/CSSStyleRule.zig index cff5ebae..767ae46e 100644 --- a/src/browser/webapi/css/CSSStyleRule.zig +++ b/src/browser/webapi/css/CSSStyleRule.zig @@ -34,6 +34,21 @@ pub fn getStyle(self: *CSSStyleRule, page: *Page) !*CSSStyleDeclaration { return style; } +pub fn getCssText(self: *CSSStyleRule, page: *Page) ![]const u8 { + const style = try self.getStyle(page); + 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 fn setCssText(self: *CSSStyleRule, text: []const u8, page: *Page) !void { + _ = self; + _ = text; + _ = page; +} + pub const JsApi = struct { pub const bridge = js.Bridge(CSSStyleRule); @@ -45,4 +60,5 @@ pub const JsApi = struct { pub const selectorText = bridge.accessor(CSSStyleRule.getSelectorText, CSSStyleRule.setSelectorText, .{}); pub const style = bridge.accessor(CSSStyleRule.getStyle, null, .{}); + pub const cssText = bridge.accessor(CSSStyleRule.getCssText, CSSStyleRule.setCssText, .{}); }; diff --git a/src/browser/webapi/css/CSSStyleSheet.zig b/src/browser/webapi/css/CSSStyleSheet.zig index 5500f63c..673ff370 100644 --- a/src/browser/webapi/css/CSSStyleSheet.zig +++ b/src/browser/webapi/css/CSSStyleSheet.zig @@ -4,6 +4,8 @@ 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(); @@ -54,30 +56,46 @@ pub fn getOwnerRule(self: *const CSSStyleSheet) ?*CSSRule { } pub fn insertRule(self: *CSSStyleSheet, rule: []const u8, index: u32, page: *Page) !u32 { - _ = self; - _ = rule; - _ = index; - _ = page; - return 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 = try style_rule.getStyle(page); + try style.setCssText(parsed_rule.block, page); + + const rules = try self.getCssRules(page); + try rules.insert(index, style_rule._proto, page); + return index; } pub fn deleteRule(self: *CSSStyleSheet, index: u32, page: *Page) !void { - _ = self; - _ = index; - _ = page; + const rules = try self.getCssRules(page); + rules.remove(index); } pub fn replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !js.Promise { - _ = self; - _ = text; - // TODO: clear self.css_rules + try self.replaceSync(text, page); return page.js.local.?.resolvePromise({}); } -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) !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 = try style_rule.getStyle(page); + try style.setCssText(parsed_rule.block, page); + + try rules.insert(index, style_rule._proto, page); + index += 1; + } } pub const JsApi = struct { diff --git a/src/browser/webapi/element/html/Style.zig b/src/browser/webapi/element/html/Style.zig index 131b7634..3a5f4bc5 100644 --- a/src/browser/webapi/element/html/Style.zig +++ b/src/browser/webapi/element/html/Style.zig @@ -94,6 +94,10 @@ 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 text = try self.asNode().getTextContentAlloc(page.call_arena); + try sheet.replaceSync(text, page); + return sheet; }