From 701e361dd5ac794f0944f9590eb75cefe798994b Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 18 Aug 2026 16:17:16 +0800 Subject: [PATCH 1/2] webapi: Re-parse element.style when style attribute changes If we have a materialized element.style (stored in Frame._element_styles) and the style attribute changes (e.g. via setAttribute('style', '....'), then the materialized CSSStyleDeclaration has to be updated (cleared and the style re-parsed) --- src/browser/Frame.zig | 11 ++++ .../tests/element/css_style_properties.html | 29 ++++++++++ .../webapi/css/CSSStyleDeclaration.zig | 57 ++++++++++++------- 3 files changed, 78 insertions(+), 19 deletions(-) diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index 45555932e..1ff57df9d 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -2964,6 +2964,8 @@ pub fn attributeChange(self: *Frame, element: *Element, name: String, value: Str } else if (name.eql(comptime .wrap("popover"))) { const old = if (old_value) |o| o.str() else null; popover.attributeChanged(element, old, value.str(), self); + } else if (name.eql(comptime .wrap("style"))) { + self.styleAttributeChanged(element, value.str()); } } @@ -2985,9 +2987,18 @@ pub fn attributeRemove(self: *Frame, element: *Element, name: String, old_value: } } else if (name.eql(comptime .wrap("popover"))) { popover.attributeChanged(element, old_value.str(), null, self); + } else if (name.eql(comptime .wrap("style"))) { + self.styleAttributeChanged(element, null); } } +fn styleAttributeChanged(self: *Frame, element: *Element, value: ?[]const u8) void { + const style = self._element_styles.get(element) orelse return; + style.asCSSStyleDeclaration().styleAttributeChanged(value, self) catch |err| { + log.err(.frame, "style attribute reparse", .{ .err = err, .type = self._type, .url = self.url }); + }; +} + pub fn signalSlotChange(self: *Frame, slot: *Element.Html.Slot) void { self._slots_pending_slotchange.put(self.arena, slot, {}) catch |err| { log.err(.frame, "signalSlotChange.put", .{ .err = err, .type = self._type, .url = self.url }); diff --git a/src/browser/tests/element/css_style_properties.html b/src/browser/tests/element/css_style_properties.html index 3592df67e..61cb8e945 100644 --- a/src/browser/tests/element/css_style_properties.html +++ b/src/browser/tests/element/css_style_properties.html @@ -191,3 +191,32 @@ testing.expectEqual('0.5em 0.25em', div.style.margin); } + + diff --git a/src/browser/webapi/css/CSSStyleDeclaration.zig b/src/browser/webapi/css/CSSStyleDeclaration.zig index 8a9ca1258..a78bd3b5d 100644 --- a/src/browser/webapi/css/CSSStyleDeclaration.zig +++ b/src/browser/webapi/css/CSSStyleDeclaration.zig @@ -34,6 +34,7 @@ const CSSStyleDeclaration = @This(); _element: ?*Element = null, _properties: std.DoublyLinkedList = .{}, _is_computed: bool = false, +_syncing: bool = false, // Parse the element's existing style attribute into _properties so that // subsequent JS reads and writes see all CSS properties, not just newly @@ -43,11 +44,14 @@ pub fn parseInlineStyle(self: *CSSStyleDeclaration, frame: *Frame) !void { return; } const el = self._element orelse return; - if (el.getAttributeSafe(comptime .wrap("style"))) |attr_value| { - var it = CssParser.parseDeclarationsList(attr_value); - while (it.next()) |declaration| { - try self.applyParsedDeclaration(declaration, frame); - } + const attr_value = el.getAttributeSafe(comptime .wrap("style")) orelse return; + try self.applyDeclarations(attr_value, frame); +} + +fn applyDeclarations(self: *CSSStyleDeclaration, text: []const u8, frame: *Frame) !void { + var it = CssParser.parseDeclarationsList(text); + while (it.next()) |declaration| { + try self.applyParsedDeclaration(declaration, frame); } } @@ -213,9 +217,36 @@ fn removePropertyImpl(self: *CSSStyleDeclaration, property_name: []const u8, fra fn syncStyleAttribute(self: *CSSStyleDeclaration, frame: *Frame) !void { const element = self._element orelse return; const css_text = try self.getCssText(frame); + self._syncing = true; + defer self._syncing = false; try element.setAttributeSafe(comptime .wrap("style"), .wrap(css_text), frame); } +// The element's style attribute changed (null: removed) +pub fn styleAttributeChanged(self: *CSSStyleDeclaration, text: ?[]const u8, frame: *Frame) !void { + if (self._syncing) { + // this was us making the change internally, automatically in-sync + return; + } + // ok, this change was from the outside, e.g. via setAttribute('style', ....) + // we need to get this declaration back in sync + self.clearProperties(frame); + if (text) |t| { + try self.applyDeclarations(t, frame); + } +} + +fn clearProperties(self: *CSSStyleDeclaration, frame: *Frame) void { + var node = self._properties.first; + while (node) |n| { + const next = n.next; + const prop = Property.fromNodeLink(n); + self._properties.remove(n); + frame._factory.destroy(prop); + node = next; + } +} + pub fn getFloat(self: *const CSSStyleDeclaration, frame: *Frame) []const u8 { return self.getPropertyValue("float", frame); } @@ -232,21 +263,9 @@ pub fn getCssText(self: *const CSSStyleDeclaration, frame: *Frame) ![]const u8 { } pub fn setCssText(self: *CSSStyleDeclaration, text: []const u8, frame: *Frame) !void { - // Clear existing properties - var node = self._properties.first; - while (node) |n| { - const next = n.next; - const prop = Property.fromNodeLink(n); - self._properties.remove(n); - frame._factory.destroy(prop); - node = next; - } + self.clearProperties(frame); - // Parse and set new properties - var it = CssParser.parseDeclarationsList(text); - while (it.next()) |declaration| { - try self.applyParsedDeclaration(declaration, frame); - } + try self.applyDeclarations(text, frame); try self.syncStyleAttribute(frame); } From 4b06c248e70a7e1c1bdea65302d3c929afef09a4 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 19 Aug 2026 07:33:38 +0800 Subject: [PATCH 2/2] use element.ownerFrame to access StyleManager --- src/browser/StyleManager.zig | 2 + .../tests/frames/cross_realm_attributes.html | 43 +++++++++++++++++++ .../support/cross_realm_attributes.html | 4 ++ src/browser/webapi/Element.zig | 19 +++++--- .../webapi/css/CSSStyleDeclaration.zig | 7 +-- .../webapi/element/svg/TextContent.zig | 3 +- src/browser/webapi/svg/Length.zig | 7 +-- 7 files changed, 72 insertions(+), 13 deletions(-) diff --git a/src/browser/StyleManager.zig b/src/browser/StyleManager.zig index b054de4f3..6dc83ead9 100644 --- a/src/browser/StyleManager.zig +++ b/src/browser/StyleManager.zig @@ -1191,6 +1191,8 @@ const CheckVisibilityOptions = struct { // its field max, so a real rule can never pack to all-ones. const INLINE_PRIORITY: u64 = std.math.maxInt(u64); +// `frame` is the StyleManager's frame, which callers guarantee is el's owner +// frame (el.ownerFrame) — the map where the materialized style lives. fn getInlineStyleProperty(el: *Element, property_name: String, frame: *Frame) ?*CSSStyleProperty { const style = frame._element_styles.get(el) orelse blk: { // No JS-set style object and no style attribute -> nothing inline to read. diff --git a/src/browser/tests/frames/cross_realm_attributes.html b/src/browser/tests/frames/cross_realm_attributes.html index d06e5d45f..ac38acf20 100644 --- a/src/browser/tests/frames/cross_realm_attributes.html +++ b/src/browser/tests/frames/cross_realm_attributes.html @@ -80,4 +80,47 @@ reads of iframe data-* returned null (WPT encoding/*). }); } + + diff --git a/src/browser/tests/frames/support/cross_realm_attributes.html b/src/browser/tests/frames/support/cross_realm_attributes.html index 756e5c0d4..0629b8766 100644 --- a/src/browser/tests/frames/support/cross_realm_attributes.html +++ b/src/browser/tests/frames/support/cross_realm_attributes.html @@ -1,4 +1,5 @@ + x