From 1c044b089baff988cbd49da971e485f3f848ccc4 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sat, 27 Jun 2026 23:13:36 -0700 Subject: [PATCH 1/6] fix: resolve inline style= declarations in getComputedStyle --- src/browser/tests/element/styles.html | 19 +++++++++++++++++++ .../webapi/css/CSSStyleDeclaration.zig | 16 ++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/browser/tests/element/styles.html b/src/browser/tests/element/styles.html index 05dfa34f0..42250d168 100644 --- a/src/browser/tests/element/styles.html +++ b/src/browser/tests/element/styles.html @@ -198,3 +198,22 @@ testing.expectEqual('1', divStyle.opacity); } + + diff --git a/src/browser/webapi/css/CSSStyleDeclaration.zig b/src/browser/webapi/css/CSSStyleDeclaration.zig index f1c915506..a106731fd 100644 --- a/src/browser/webapi/css/CSSStyleDeclaration.zig +++ b/src/browser/webapi/css/CSSStyleDeclaration.zig @@ -96,6 +96,9 @@ pub fn getPropertyValue(self: *const CSSStyleDeclaration, property_name: []const const prop = self.findProperty(wrapped) orelse { // Only return default values for computed styles if (self._is_computed) { + if (self._element) |element| { + if (findInlineValue(element, normalized, frame)) |value| return value; + } return getDefaultPropertyValue(self, wrapped); } return ""; @@ -236,6 +239,19 @@ pub fn findProperty(self: *const CSSStyleDeclaration, name: String) ?*Property { return null; } +fn findInlineValue(element: *const Element, normalized_name: []const u8, frame: *Frame) ?[]const u8 { + const attr_value = element.getAttributeSafe(comptime .wrap("style")) orelse return null; + var it = CssParser.parseDeclarationsList(attr_value); + var result: ?[]const u8 = null; + while (it.next()) |declaration| { + if (std.ascii.eqlIgnoreCase(declaration.name, normalized_name)) { + // Keep the last matching declaration, matching CSS declaration order. + result = normalizePropertyValue(frame.call_arena, normalized_name, declaration.value) catch declaration.value; + } + } + return result; +} + fn normalizePropertyName(name: []const u8, buf: []u8) []const u8 { if (name.len > buf.len) { log.info(.dom, "css.long.name", .{ .name = name }); From 16b16564d535b14f191ca9133c2a83837d275b34 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sat, 27 Jun 2026 23:29:41 -0700 Subject: [PATCH 2/6] css: honor !important for duplicate inline declarations in computed style --- src/browser/tests/element/styles.html | 6 ++++++ src/browser/webapi/css/CSSStyleDeclaration.zig | 13 ++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/browser/tests/element/styles.html b/src/browser/tests/element/styles.html index 42250d168..adc9d395f 100644 --- a/src/browser/tests/element/styles.html +++ b/src/browser/tests/element/styles.html @@ -215,5 +215,11 @@ const plainDiv = document.createElement('div'); document.body.appendChild(plainDiv); testing.expectEqual('block', window.getComputedStyle(plainDiv).getPropertyValue('display')); + + // A normal declaration must not override an earlier !important one. + const impDiv = document.createElement('div'); + impDiv.setAttribute('style', 'color: red !important; color: blue'); + document.body.appendChild(impDiv); + testing.expectEqual('red', window.getComputedStyle(impDiv).getPropertyValue('color')); } diff --git a/src/browser/webapi/css/CSSStyleDeclaration.zig b/src/browser/webapi/css/CSSStyleDeclaration.zig index a106731fd..f58b7fd74 100644 --- a/src/browser/webapi/css/CSSStyleDeclaration.zig +++ b/src/browser/webapi/css/CSSStyleDeclaration.zig @@ -243,11 +243,18 @@ fn findInlineValue(element: *const Element, normalized_name: []const u8, frame: const attr_value = element.getAttributeSafe(comptime .wrap("style")) orelse return null; var it = CssParser.parseDeclarationsList(attr_value); var result: ?[]const u8 = null; + var result_important = false; while (it.next()) |declaration| { - if (std.ascii.eqlIgnoreCase(declaration.name, normalized_name)) { - // Keep the last matching declaration, matching CSS declaration order. - result = normalizePropertyValue(frame.call_arena, normalized_name, declaration.value) catch declaration.value; + if (!std.ascii.eqlIgnoreCase(declaration.name, normalized_name)) { + continue; } + // A later declaration wins, except a normal one never overrides an + // earlier !important declaration (CSS cascade precedence). + if (result_important and !declaration.important) { + continue; + } + result = normalizePropertyValue(frame.call_arena, normalized_name, declaration.value) catch declaration.value; + result_important = declaration.important; } return result; } From 0614795fc5b54213c7f3559aa5a7ab524aa30a5a Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Mon, 29 Jun 2026 14:37:54 +0200 Subject: [PATCH 3/6] css: resolve computed inline styles via StyleManager getComputedStyle now reads inline values from the element's parsed el.style through StyleManager.inlineStyleValue instead of re-parsing the style= attribute, so computed and inline values share one source of truth. Move the !important cascade precedence to the shared parse path (applyParsedDeclaration) so el.style resolves duplicate declarations correctly too. --- src/browser/StyleManager.zig | 9 ++++ src/browser/tests/element/styles.html | 4 +- .../webapi/css/CSSStyleDeclaration.zig | 41 ++++++++----------- 3 files changed, 30 insertions(+), 24 deletions(-) diff --git a/src/browser/StyleManager.zig b/src/browser/StyleManager.zig index 79936e1d3..d96eba0b0 100644 --- a/src/browser/StyleManager.zig +++ b/src/browser/StyleManager.zig @@ -871,6 +871,15 @@ fn getInlineStyleProperty(el: *Element, property_name: String, frame: *Frame) ?* return style.asCSSStyleDeclaration().findProperty(property_name); } +/// Resolved value of an element's inline `style=` declaration for `property_name`, +/// or null when the element has no such declaration. Reads the element's parsed +/// inline style (the same source `el.style` exposes), so `getComputedStyle` and +/// `el.style` agree on inline values instead of resolving them independently. +pub fn inlineStyleValue(self: *StyleManager, el: *Element, property_name: String) ?[]const u8 { + const property = getInlineStyleProperty(el, property_name, self.frame) orelse return null; + return property._value.str(); +} + const testing = @import("../testing.zig"); test "StyleManager: computeSpecificity: element selector" { // div -> (0, 0, 1) diff --git a/src/browser/tests/element/styles.html b/src/browser/tests/element/styles.html index adc9d395f..b4e81aa2b 100644 --- a/src/browser/tests/element/styles.html +++ b/src/browser/tests/element/styles.html @@ -216,10 +216,12 @@ document.body.appendChild(plainDiv); testing.expectEqual('block', window.getComputedStyle(plainDiv).getPropertyValue('display')); - // A normal declaration must not override an earlier !important one. + // A normal declaration must not override an earlier !important one, and the + // computed and inline (el.style) paths must agree on the resolved value. const impDiv = document.createElement('div'); impDiv.setAttribute('style', 'color: red !important; color: blue'); document.body.appendChild(impDiv); testing.expectEqual('red', window.getComputedStyle(impDiv).getPropertyValue('color')); + testing.expectEqual('red', impDiv.style.getPropertyValue('color')); } diff --git a/src/browser/webapi/css/CSSStyleDeclaration.zig b/src/browser/webapi/css/CSSStyleDeclaration.zig index f58b7fd74..5cf1c21b2 100644 --- a/src/browser/webapi/css/CSSStyleDeclaration.zig +++ b/src/browser/webapi/css/CSSStyleDeclaration.zig @@ -49,7 +49,7 @@ pub fn init(element: ?*Element, is_computed: bool, frame: *Frame) !*CSSStyleDecl if (el.getAttributeSafe(comptime .wrap("style"))) |attr_value| { var it = CssParser.parseDeclarationsList(attr_value); while (it.next()) |declaration| { - try self.setPropertyImpl(declaration.name, declaration.value, declaration.important, frame); + try self.applyParsedDeclaration(declaration, frame); } } } @@ -97,7 +97,9 @@ pub fn getPropertyValue(self: *const CSSStyleDeclaration, property_name: []const // Only return default values for computed styles if (self._is_computed) { if (self._element) |element| { - if (findInlineValue(element, normalized, frame)) |value| return value; + // Resolve inline `style=` declarations through the element's + // parsed inline style, so computed values match `el.style`. + if (frame._style_manager.inlineStyleValue(element, wrapped)) |value| return value; } return getDefaultPropertyValue(self, wrapped); } @@ -127,6 +129,19 @@ pub fn setProperty(self: *CSSStyleDeclaration, property_name: []const u8, value: try self.syncStyleAttribute(frame); } +// Apply one declaration parsed from a `style=` block. Unlike the imperative +// setProperty path, within a single declaration block a normal declaration must +// not override an earlier !important one (CSS cascade precedence). +fn applyParsedDeclaration(self: *CSSStyleDeclaration, declaration: CssParser.Declaration, frame: *Frame) !void { + if (!declaration.important) { + const normalized = normalizePropertyName(declaration.name, &frame.buf); + if (self.findProperty(.wrap(normalized))) |existing| { + if (existing._important) return; + } + } + try self.setPropertyImpl(declaration.name, declaration.value, declaration.important, frame); +} + fn setPropertyImpl(self: *CSSStyleDeclaration, property_name: []const u8, value: []const u8, important: bool, frame: *Frame) !void { if (value.len == 0) { _ = try self.removePropertyImpl(property_name, frame); @@ -210,7 +225,7 @@ pub fn setCssText(self: *CSSStyleDeclaration, text: []const u8, frame: *Frame) ! // Parse and set new properties var it = CssParser.parseDeclarationsList(text); while (it.next()) |declaration| { - try self.setPropertyImpl(declaration.name, declaration.value, declaration.important, frame); + try self.applyParsedDeclaration(declaration, frame); } try self.syncStyleAttribute(frame); } @@ -239,26 +254,6 @@ pub fn findProperty(self: *const CSSStyleDeclaration, name: String) ?*Property { return null; } -fn findInlineValue(element: *const Element, normalized_name: []const u8, frame: *Frame) ?[]const u8 { - const attr_value = element.getAttributeSafe(comptime .wrap("style")) orelse return null; - var it = CssParser.parseDeclarationsList(attr_value); - var result: ?[]const u8 = null; - var result_important = false; - while (it.next()) |declaration| { - if (!std.ascii.eqlIgnoreCase(declaration.name, normalized_name)) { - continue; - } - // A later declaration wins, except a normal one never overrides an - // earlier !important declaration (CSS cascade precedence). - if (result_important and !declaration.important) { - continue; - } - result = normalizePropertyValue(frame.call_arena, normalized_name, declaration.value) catch declaration.value; - result_important = declaration.important; - } - return result; -} - fn normalizePropertyName(name: []const u8, buf: []u8) []const u8 { if (name.len > buf.len) { log.info(.dom, "css.long.name", .{ .name = name }); From 96e23f78e8151ce2cc3f894c9dfd001865ac4c89 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 30 Jun 2026 12:50:50 +0800 Subject: [PATCH 4/6] perf: Skip CSSStyleProperty creation if there is no style attribute Was reviewing https://github.com/lightpanda-io/browser/pull/2836 and realized the StyleManager's getInlineStyleProperty could be optimized to avoid creating the CSSStyleProperties in the case where there's no style attribute. --- src/browser/StyleManager.zig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/browser/StyleManager.zig b/src/browser/StyleManager.zig index 79936e1d3..92d3a32e7 100644 --- a/src/browser/StyleManager.zig +++ b/src/browser/StyleManager.zig @@ -864,6 +864,10 @@ const CheckVisibilityOptions = struct { const INLINE_PRIORITY: u64 = std.math.maxInt(u64); fn getInlineStyleProperty(el: *Element, property_name: String, frame: *Frame) ?*CSSStyleProperty { + if (!el.hasAttributeSafe(comptime .wrap("style"))) { + // cheap guard to avoid creating the CSSStyleProperty if there is no style attribute + return null; + } const style = el.getOrCreateStyle(frame) catch |err| { log.err(.browser, "StyleManager getOrCreateStyle", .{ .err = err }); return null; From 87b93faf913259408a597c7d7977c68e1d50446a Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 30 Jun 2026 20:40:36 +0800 Subject: [PATCH 5/6] minor: ignore test server error when writing internal server error --- src/TestHTTPServer.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TestHTTPServer.zig b/src/TestHTTPServer.zig index d2d00a077..73ee07907 100644 --- a/src/TestHTTPServer.zig +++ b/src/TestHTTPServer.zig @@ -93,7 +93,7 @@ fn handleConnection(self: *TestHTTPServer, conn: std.net.Server.Connection) !voi error.BrokenPipe => {}, else => { std.debug.print("test http error '{s}': {}\n", .{ req.head.target, err }); - try req.respond("server error", .{ .status = .internal_server_error }); + req.respond("server error", .{ .status = .internal_server_error }) catch {}; }, } return; From 056cd3261fef1670a9cac90bc770d70c75c25f87 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Tue, 30 Jun 2026 15:01:27 +0200 Subject: [PATCH 6/6] Update src/browser/webapi/css/CSSStyleDeclaration.zig MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: AdriĆ  Arrufat <1671644+arrufat@users.noreply.github.com> --- src/browser/webapi/css/CSSStyleDeclaration.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/browser/webapi/css/CSSStyleDeclaration.zig b/src/browser/webapi/css/CSSStyleDeclaration.zig index 5cf1c21b2..393243187 100644 --- a/src/browser/webapi/css/CSSStyleDeclaration.zig +++ b/src/browser/webapi/css/CSSStyleDeclaration.zig @@ -129,9 +129,9 @@ pub fn setProperty(self: *CSSStyleDeclaration, property_name: []const u8, value: try self.syncStyleAttribute(frame); } -// Apply one declaration parsed from a `style=` block. Unlike the imperative -// setProperty path, within a single declaration block a normal declaration must -// not override an earlier !important one (CSS cascade precedence). +/// Apply one declaration parsed from a `style=` block. Unlike the imperative +/// setProperty path, within a single declaration block a normal declaration must +/// not override an earlier !important one (CSS cascade precedence). fn applyParsedDeclaration(self: *CSSStyleDeclaration, declaration: CssParser.Declaration, frame: *Frame) !void { if (!declaration.important) { const normalized = normalizePropertyName(declaration.name, &frame.buf);