css: improve CSSOM rule handling and serialization

Refactors `CSSRule` to a union type for better type safety and updates
`CSSStyleRule` to use `CSSStyleProperties`. Adds comprehensive tests for
`insertRule`, `deleteRule`, and `replaceSync`.
This commit is contained in:
Adrià Arrufat
2026-03-12 20:23:59 +09:00
parent ee034943b6
commit f58f6e8d65
6 changed files with 115 additions and 31 deletions

View File

@@ -419,3 +419,57 @@
testing.expectEqual('anchor-size(--foo width, anchor-size(--bar height))', div.style.width); testing.expectEqual('anchor-size(--foo width, anchor-size(--bar height))', div.style.width);
} }
</script> </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);
console.warn("constructor:", sheet.cssRules[0].constructor.name);
testing.expectEqual('.test', sheet.cssRules[0].selectorText);
testing.expectEqual('green', sheet.cssRules[0].style.color);
sheet.deleteRule(0);
testing.expectEqual(0, 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 () => {
await sheet.replace('.async-test { margin: 10px; }');
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>

View File

@@ -131,3 +131,17 @@
testing.eventually(() => testing.expectEqual(true, result)); testing.eventually(() => testing.expectEqual(true, result));
} }
</script> </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>

View File

@@ -2,29 +2,42 @@ const std = @import("std");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const CSSStyleRule = @import("CSSStyleRule.zig");
const CSSRule = @This(); const CSSRule = @This();
pub const Type = enum(u16) { pub const Type = union(enum) {
style = 1, style: *CSSStyleRule,
charset = 2, charset: void,
import = 3, import: void,
media = 4, media: void,
font_face = 5, font_face: void,
page = 6, page: void,
keyframes = 7, keyframes: void,
keyframe = 8, keyframe: void,
margin = 9, margin: void,
namespace = 10, namespace: void,
counter_style = 11, counter_style: void,
supports = 12, supports: void,
document = 13, document: void,
font_feature_values = 14, font_feature_values: void,
viewport = 15, viewport: void,
region_style = 16, region_style: void,
}; };
_type: Type, _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 { pub fn init(rule_type: Type, page: *Page) !*CSSRule {
return page._factory.create(CSSRule{ return page._factory.create(CSSRule{
._type = rule_type, ._type = rule_type,
@@ -32,7 +45,7 @@ pub fn init(rule_type: Type, page: *Page) !*CSSRule {
} }
pub fn getType(self: *const CSSRule) u16 { 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 { pub fn getCssText(self: *const CSSRule, page: *Page) []const u8 {

View File

@@ -180,8 +180,6 @@ pub fn getCssText(self: *const CSSStyleDeclaration, page: *Page) ![]const u8 {
} }
pub fn setCssText(self: *CSSStyleDeclaration, text: []const u8, page: *Page) !void { pub fn setCssText(self: *CSSStyleDeclaration, text: []const u8, page: *Page) !void {
if (self._element == null) return;
// Clear existing properties // Clear existing properties
var node = self._properties.first; var node = self._properties.first;
while (node) |n| { while (node) |n| {
@@ -197,6 +195,7 @@ pub fn setCssText(self: *CSSStyleDeclaration, text: []const u8, page: *Page) !vo
while (it.next()) |declaration| { while (it.next()) |declaration| {
try self.setPropertyImpl(declaration.name, declaration.value, declaration.important, page); try self.setPropertyImpl(declaration.name, declaration.value, declaration.important, page);
} }
try self.syncStyleAttribute(page); try self.syncStyleAttribute(page);
} }

View File

@@ -2,19 +2,20 @@ const std = @import("std");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const CSSRule = @import("CSSRule.zig"); const CSSRule = @import("CSSRule.zig");
const CSSStyleDeclaration = @import("CSSStyleDeclaration.zig"); const CSSStyleProperties = @import("CSSStyleProperties.zig");
const CSSStyleRule = @This(); const CSSStyleRule = @This();
_proto: *CSSRule, _proto: *CSSRule,
_selector_text: []const u8 = "", _selector_text: []const u8 = "",
_style: ?*CSSStyleDeclaration = null, _style: ?*CSSStyleProperties = null,
pub fn init(page: *Page) !*CSSStyleRule { pub fn init(page: *Page) !*CSSStyleRule {
const rule = try CSSRule.init(.style, page); const style_rule = try page._factory.create(CSSStyleRule{
return page._factory.create(CSSStyleRule{ ._proto = undefined,
._proto = rule,
}); });
style_rule._proto = try CSSRule.init(.{ .style = style_rule }, page);
return style_rule;
} }
pub fn getSelectorText(self: *const CSSStyleRule) []const u8 { pub fn getSelectorText(self: *const CSSStyleRule) []const u8 {
@@ -25,17 +26,18 @@ pub fn setSelectorText(self: *CSSStyleRule, text: []const u8, page: *Page) !void
self._selector_text = try page.dupeString(text); 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| { if (self._style) |style| {
return style; return style;
} }
const style = try CSSStyleDeclaration.init(null, false, page); const style = try CSSStyleProperties.init(null, false, page);
self._style = style; self._style = style;
return style; return style;
} }
pub fn getCssText(self: *CSSStyleRule, page: *Page) ![]const u8 { pub fn getCssText(self: *CSSStyleRule, page: *Page) ![]const u8 {
const style = try self.getStyle(page); const style_props = try self.getStyle(page);
const style = style_props.asCSSStyleDeclaration();
var buf = std.Io.Writer.Allocating.init(page.call_arena); var buf = std.Io.Writer.Allocating.init(page.call_arena);
try buf.writer.print("{s} {{ ", .{self._selector_text}); try buf.writer.print("{s} {{ ", .{self._selector_text});
try style.format(&buf.writer); try style.format(&buf.writer);
@@ -54,7 +56,7 @@ pub const JsApi = struct {
pub const Meta = struct { pub const Meta = struct {
pub const name = "CSSStyleRule"; 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 var class_id: bridge.ClassId = undefined;
}; };

View File

@@ -62,7 +62,8 @@ pub fn insertRule(self: *CSSStyleSheet, rule: []const u8, index: u32, page: *Pag
const style_rule = try CSSStyleRule.init(page); const style_rule = try CSSStyleRule.init(page);
try style_rule.setSelectorText(parsed_rule.selector, page); try style_rule.setSelectorText(parsed_rule.selector, page);
const style = try style_rule.getStyle(page); const style_props = try style_rule.getStyle(page);
const style = style_props.asCSSStyleDeclaration();
try style.setCssText(parsed_rule.block, page); try style.setCssText(parsed_rule.block, page);
const rules = try self.getCssRules(page); const rules = try self.getCssRules(page);
@@ -90,7 +91,8 @@ pub fn replaceSync(self: *CSSStyleSheet, text: []const u8, page: *Page) !void {
const style_rule = try CSSStyleRule.init(page); const style_rule = try CSSStyleRule.init(page);
try style_rule.setSelectorText(parsed_rule.selector, page); try style_rule.setSelectorText(parsed_rule.selector, page);
const style = try style_rule.getStyle(page); const style_props = try style_rule.getStyle(page);
const style = style_props.asCSSStyleDeclaration();
try style.setCssText(parsed_rule.block, page); try style.setCssText(parsed_rule.block, page);
try rules.insert(index, style_rule._proto, page); try rules.insert(index, style_rule._proto, page);