parse style attribute on CSSStyleDeclaration init

To reflect the current style attribute, CSSStyleDeclaration now parses
it on init.

Moreover, this PR synchronizes the element's style attribute with the
dynamic changes.
This commit is contained in:
Pierre Tachoire
2026-02-19 12:26:04 +01:00
parent c3555bfcab
commit de3f5011bc
2 changed files with 91 additions and 3 deletions

View File

@@ -205,3 +205,54 @@
testing.expectEqual('', style.getPropertyPriority('content')); testing.expectEqual('', style.getPropertyPriority('content'));
} }
</script> </script>
<script id="CSSStyleDeclaration_style_syncs_to_attribute">
{
// JS style modifications must be reflected in getAttribute.
const div = document.createElement('div');
// Named property assignment (element.style.X = ...)
div.style.opacity = '0';
testing.expectEqual('opacity: 0;', div.getAttribute('style'));
// Update existing property
div.style.opacity = '1';
testing.expectEqual('opacity: 1;', div.getAttribute('style'));
// Add a second property
div.style.color = 'red';
testing.expectTrue(div.getAttribute('style').includes('opacity: 1'));
testing.expectTrue(div.getAttribute('style').includes('color: red'));
// removeProperty syncs back
div.style.removeProperty('opacity');
testing.expectTrue(!div.getAttribute('style').includes('opacity'));
testing.expectTrue(div.getAttribute('style').includes('color: red'));
// setCssText syncs back
div.style.cssText = 'filter: blur(0px)';
testing.expectEqual('filter: blur(0px);', div.getAttribute('style'));
// setCssText with empty string clears attribute
div.style.cssText = '';
testing.expectEqual('', div.getAttribute('style'));
}
</script>
<script id="CSSStyleDeclaration_outerHTML_reflects_style_changes">
{
// outerHTML must reflect JS-modified styles (regression test for
// DOM serialization reading stale HTML-parsed attribute values).
const div = document.createElement('div');
div.setAttribute('style', 'filter:blur(10px);opacity:0');
div.style.filter = 'blur(0px)';
div.style.opacity = '1';
const html = div.outerHTML;
testing.expectTrue(html.includes('filter: blur(0px)'));
testing.expectTrue(html.includes('opacity: 1'));
testing.expectTrue(!html.includes('blur(10px)'));
testing.expectTrue(!html.includes('opacity:0'));
}
</script>

View File

@@ -33,10 +33,27 @@ _properties: std.DoublyLinkedList = .{},
_is_computed: bool = false, _is_computed: bool = false,
pub fn init(element: ?*Element, is_computed: bool, page: *Page) !*CSSStyleDeclaration { pub fn init(element: ?*Element, is_computed: bool, page: *Page) !*CSSStyleDeclaration {
return page._factory.create(CSSStyleDeclaration{ const self = try page._factory.create(CSSStyleDeclaration{
._element = element, ._element = element,
._is_computed = is_computed, ._is_computed = is_computed,
}); });
// Parse the element's existing style attribute into _properties so that
// subsequent JS reads and writes see all CSS properties, not just newly
// added ones. Computed styles have no inline attribute to parse.
if (!is_computed) {
if (element) |el| {
if (el.getAttributeSafe(String.wrap("style"))) |attr_value| {
var it = CssParser.parseDeclarationsList(attr_value);
while (it.next()) |declaration| {
const priority: ?[]const u8 = if (declaration.important) "important" else null;
try self.setPropertyImpl(declaration.name, declaration.value, priority, page);
}
}
}
}
return self;
} }
pub fn length(self: *const CSSStyleDeclaration) u32 { pub fn length(self: *const CSSStyleDeclaration) u32 {
@@ -76,8 +93,13 @@ pub fn getPropertyPriority(self: *const CSSStyleDeclaration, property_name: []co
} }
pub fn setProperty(self: *CSSStyleDeclaration, property_name: []const u8, value: []const u8, priority_: ?[]const u8, page: *Page) !void { pub fn setProperty(self: *CSSStyleDeclaration, property_name: []const u8, value: []const u8, priority_: ?[]const u8, page: *Page) !void {
try self.setPropertyImpl(property_name, value, priority_, page);
try self.syncStyleAttribute(page);
}
fn setPropertyImpl(self: *CSSStyleDeclaration, property_name: []const u8, value: []const u8, priority_: ?[]const u8, page: *Page) !void {
if (value.len == 0) { if (value.len == 0) {
_ = try self.removeProperty(property_name, page); _ = try self.removePropertyImpl(property_name, page);
return; return;
} }
@@ -110,6 +132,12 @@ pub fn setProperty(self: *CSSStyleDeclaration, property_name: []const u8, value:
} }
pub fn removeProperty(self: *CSSStyleDeclaration, property_name: []const u8, page: *Page) ![]const u8 { pub fn removeProperty(self: *CSSStyleDeclaration, property_name: []const u8, page: *Page) ![]const u8 {
const result = try self.removePropertyImpl(property_name, page);
try self.syncStyleAttribute(page);
return result;
}
fn removePropertyImpl(self: *CSSStyleDeclaration, property_name: []const u8, page: *Page) ![]const u8 {
const normalized = normalizePropertyName(property_name, &page.buf); const normalized = normalizePropertyName(property_name, &page.buf);
const prop = self.findProperty(normalized) orelse return ""; const prop = self.findProperty(normalized) orelse return "";
@@ -121,6 +149,14 @@ pub fn removeProperty(self: *CSSStyleDeclaration, property_name: []const u8, pag
return old_value; return old_value;
} }
// Serialize current properties back to the element's style attribute so that
// DOM serialization (outerHTML, getAttribute) reflects JS-modified styles.
fn syncStyleAttribute(self: *CSSStyleDeclaration, page: *Page) !void {
const element = self._element orelse return;
const css_text = try self.getCssText(page);
try element.setAttributeSafe(String.wrap("style"), String.wrap(css_text), page);
}
pub fn getFloat(self: *const CSSStyleDeclaration, page: *Page) []const u8 { pub fn getFloat(self: *const CSSStyleDeclaration, page: *Page) []const u8 {
return self.getPropertyValue("float", page); return self.getPropertyValue("float", page);
} }
@@ -154,8 +190,9 @@ pub fn setCssText(self: *CSSStyleDeclaration, text: []const u8, page: *Page) !vo
var it = CssParser.parseDeclarationsList(text); var it = CssParser.parseDeclarationsList(text);
while (it.next()) |declaration| { while (it.next()) |declaration| {
const priority: ?[]const u8 = if (declaration.important) "important" else null; const priority: ?[]const u8 = if (declaration.important) "important" else null;
try self.setProperty(declaration.name, declaration.value, priority, page); try self.setPropertyImpl(declaration.name, declaration.value, priority, page);
} }
try self.syncStyleAttribute(page);
} }
pub fn format(self: *const CSSStyleDeclaration, writer: *std.Io.Writer) !void { pub fn format(self: *const CSSStyleDeclaration, writer: *std.Io.Writer) !void {