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);
}
</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));
}
</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 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,7 +45,7 @@ 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 {

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

View File

@@ -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,17 +26,18 @@ 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 = 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);
try buf.writer.print("{s} {{ ", .{self._selector_text});
try style.format(&buf.writer);
@@ -54,7 +56,7 @@ pub const JsApi = struct {
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;
};

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);
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);
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);
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 rules.insert(index, style_rule._proto, page);