feat(css): implement stylesheet rule management

Adds a CSS rule parser and implements `insertRule`, `deleteRule`, and
`replaceSync` in `CSSStyleSheet`. Also updates `CSSRuleList` to use
dynamic storage and populates sheets from `<style>` elements.
This commit is contained in:
Adrià Arrufat
2026-03-12 16:27:25 +09:00
parent e4f7fca10d
commit ee034943b6
5 changed files with 215 additions and 19 deletions

View File

@@ -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,
};
}

View File

@@ -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 {

View File

@@ -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, .{});
};

View File

@@ -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 {

View File

@@ -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;
}