From 9ff221e2c0cb68dd79d762258b1fdc9f82374e02 Mon Sep 17 00:00:00 2001 From: Francis Bouvier Date: Sun, 12 Jul 2026 11:59:55 +0200 Subject: [PATCH] selector: attribute names match case-sensitively on foreign elements Fixes WPT /dom/nodes/querySelector-mixed-case.html: per the Selectors spec, attribute names in selectors match ASCII case-insensitively against HTML elements (in an HTML document) but case-sensitively against foreign (SVG/MathML) elements. We lowercased the selector's attribute name at parse time for everyone, so [viewBox] never matched the case-preserved viewBox attribute on an SVG element. The parsed attribute selector now also keeps the name as written; matching picks the lowercased name for HTML elements (whose stored attributes are normalized) and the original for foreign elements (whose attributes are stored as written). Coverage: /dom/nodes/querySelector-mixed-case.html 0/1 -> 1/1 (fully green); Element-matches.html stays 669/669. Co-Authored-By: Claude Fable 5 --- src/browser/webapi/selector/List.zig | 6 +++++- src/browser/webapi/selector/Parser.zig | 6 ++++-- src/browser/webapi/selector/Selector.zig | 4 ++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/browser/webapi/selector/List.zig b/src/browser/webapi/selector/List.zig index 81ef82c3f..2261e0c83 100644 --- a/src/browser/webapi/selector/List.zig +++ b/src/browser/webapi/selector/List.zig @@ -445,7 +445,11 @@ fn matchesPart(el: *Node.Element, part: Part, scope: *Node, frame: *Frame) bool } fn matchesAttribute(el: *Node.Element, attr: Selector.Attribute) bool { - const value = el.getAttributeSafe(attr.name) orelse { + // Attribute names match ASCII case-insensitively on HTML elements (both + // sides lowercased) and case-sensitively on foreign elements, whose + // attributes are stored as written. + const name = if (el._namespace == .html) attr.name else attr.original_name; + const value = el.getAttributeSafe(name) orelse { return false; }; diff --git a/src/browser/webapi/selector/Parser.zig b/src/browser/webapi/selector/Parser.zig index 29adb4b3b..b66ea8c71 100644 --- a/src/browser/webapi/selector/Parser.zig +++ b/src/browser/webapi/selector/Parser.zig @@ -962,6 +962,8 @@ fn attribute(self: *Parser, arena: Allocator) !Selector.Attribute { } const attr_name = try self.attributeName(arena); + // As written, for the case-sensitive match on foreign elements. + const original_name = try arena.dupe(u8, attr_name); // Normalize the name to lowercase for fast matching (consistent with Attribute.normalizeNameForLookup) const name = try Attribute.normalizeNameForLookupAlloc(arena, .wrap(attr_name)); @@ -974,7 +976,7 @@ fn attribute(self: *Parser, arena: Allocator) !Selector.Attribute { if (self.peek() == ']') { self.input = self.input[1..]; } - return .{ .name = name, .matcher = .presence, .case_insensitive = case_insensitive }; + return .{ .name = name, .original_name = .wrap(original_name), .matcher = .presence, .case_insensitive = case_insensitive }; } const matcher_type = try self.attributeMatcher(); @@ -1012,7 +1014,7 @@ fn attribute(self: *Parser, arena: Allocator) !Selector.Attribute { .presence => unreachable, }; - return .{ .name = name, .matcher = matcher, .case_insensitive = case_insensitive }; + return .{ .name = name, .original_name = .wrap(original_name), .matcher = matcher, .case_insensitive = case_insensitive }; } fn attributeName(self: *Parser, arena: Allocator) ![]const u8 { diff --git a/src/browser/webapi/selector/Selector.zig b/src/browser/webapi/selector/Selector.zig index 71658419b..c6762372c 100644 --- a/src/browser/webapi/selector/Selector.zig +++ b/src/browser/webapi/selector/Selector.zig @@ -229,7 +229,11 @@ pub const Part = union(enum) { }; pub const Attribute = struct { + // Lowercased for the HTML-element case-insensitive match. name: String, + // As written in the selector: attribute names match case-sensitively on + // foreign (SVG/MathML) elements, whose attributes are stored unnormalized. + original_name: String, matcher: AttributeMatcher, case_insensitive: bool, };