From f37d96c2854cf4ac35f54e4f3f4dad0fa1106628 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Tue, 25 Aug 2026 14:53:35 +0200 Subject: [PATCH] css: compare visibility keyword values case-insensitively CSS keywords are ASCII case-insensitive, but StyleManager matched display:none / visibility:hidden|collapse / opacity:0 / pointer-events:none with exact String.eql, so `style="DISPLAY: NONE"` or a `.x { Visibility: Hidden }` rule left the element visible to checkVisibility, getComputedStyle, the semantic tree and interactiveElements. Compare at the check sites via a new String.eqlSliceIgnoreCase rather than lowercasing declared values, which would corrupt content, url() and custom property values that el.style must reflect verbatim. --- src/browser/StyleManager.zig | 16 +++++----- src/browser/interactive.zig | 6 ++++ .../tests/element/check_visibility.html | 29 +++++++++++++++++++ src/string.zig | 4 +++ 4 files changed, 47 insertions(+), 8 deletions(-) diff --git a/src/browser/StyleManager.zig b/src/browser/StyleManager.zig index 8f2791751..c7dd36601 100644 --- a/src/browser/StyleManager.zig +++ b/src/browser/StyleManager.zig @@ -689,7 +689,7 @@ fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOp // Check inline styles FIRST - they use INLINE_PRIORITY so no stylesheet can beat them if (options.check_display) { if (getInlineStyleProperty(el, comptime .wrap("display"), self.frame)) |property| { - if (property._value.eql(comptime .wrap("none"))) { + if (property._value.eqlSliceIgnoreCase("none")) { return true; // Early exit for hiding value } display_none = false; @@ -702,7 +702,7 @@ fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOp if (options.check_visibility) { if (getInlineStyleProperty(el, comptime .wrap("visibility"), self.frame)) |property| { - if (property._value.eql(comptime .wrap("hidden")) or property._value.eql(comptime .wrap("collapse"))) { + if (property._value.eqlSliceIgnoreCase("hidden") or property._value.eqlSliceIgnoreCase("collapse")) { return true; } visibility_hidden = false; @@ -717,7 +717,7 @@ fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOp if (options.check_opacity) { if (getInlineStyleProperty(el, comptime .wrap("opacity"), self.frame)) |property| { - if (property._value.eql(comptime .wrap("0"))) { + if (property._value.eqlSliceIgnoreCase("0")) { return true; } opacity_zero = false; @@ -872,7 +872,7 @@ fn elementHasPointerEventsNone(self: *StyleManager, el: *Element) bool { // Check inline style first if (getInlineStyleProperty(el, .wrap("pointer-events"), frame)) |property| { - if (property._value.eql(comptime .wrap("none"))) { + if (property._value.eqlSliceIgnoreCase("none")) { return true; } return false; @@ -1046,19 +1046,19 @@ fn extractVisibilityProperties(style: *CSSStyleProperties) VisibilityProperties const decl = style.asCSSStyleDeclaration(); if (decl.findProperty(comptime .wrap("display"))) |property| { - props.display_none = property._value.eql(comptime .wrap("none")); + props.display_none = property._value.eqlSliceIgnoreCase("none"); } if (decl.findProperty(comptime .wrap("visibility"))) |property| { - props.visibility_hidden = property._value.eql(comptime .wrap("hidden")) or property._value.eql(comptime .wrap("collapse")); + props.visibility_hidden = property._value.eqlSliceIgnoreCase("hidden") or property._value.eqlSliceIgnoreCase("collapse"); } if (decl.findProperty(comptime .wrap("opacity"))) |property| { - props.opacity_zero = property._value.eql(comptime .wrap("0")); + props.opacity_zero = property._value.eqlSliceIgnoreCase("0"); } if (decl.findProperty(.wrap("pointer-events"))) |property| { - props.pointer_events_none = property._value.eql(comptime .wrap("none")); + props.pointer_events_none = property._value.eqlSliceIgnoreCase("none"); } return props; diff --git a/src/browser/interactive.zig b/src/browser/interactive.zig index d6f092842..13c354431 100644 --- a/src/browser/interactive.zig +++ b/src/browser/interactive.zig @@ -606,6 +606,12 @@ test "browser.interactive: pointer-events none" { try testing.expectEqual(0, elements.len); } +test "browser.interactive: pointer-events none is case-insensitive" { + const elements = try testInteractive(""); + defer testing.test_session.closeAllPages(); + try testing.expectEqual(0, elements.len); +} + test "browser.interactive: non-interactive div" { const elements = try testInteractive("
Just text
"); defer testing.test_session.closeAllPages(); diff --git a/src/browser/tests/element/check_visibility.html b/src/browser/tests/element/check_visibility.html index 6b2372256..e4a421b69 100644 --- a/src/browser/tests/element/check_visibility.html +++ b/src/browser/tests/element/check_visibility.html @@ -443,3 +443,32 @@ elements[0].remove(); } + + + diff --git a/src/string.zig b/src/string.zig index 73c7dbdba..97287537c 100644 --- a/src/string.zig +++ b/src/string.zig @@ -224,6 +224,10 @@ pub const String = extern struct { }; } + pub fn eqlSliceIgnoreCase(a: String, b: []const u8) bool { + return std.ascii.eqlIgnoreCase(a.str(), b); + } + const EqualOrDeleted = union(enum) { deleted, equal: bool,