mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-07-31 09:46:05 -04:00
webapi: quirks-mode documents match class names case-insensitively
Fixes WPT /dom/nodes/getElementsByClassName-14.htm: in quirks mode getElementsByClassName must match class names ASCII case-insensitively (class="a A" matches "a" twice), while Unicode case stays significant. The document had no notion of quirks mode at all (compatMode was hardcoded to "CSS1Compat"). Document.isQuirksMode approximates the HTML parser's mode: an HTML document without a doctype child is in quirks mode (legacy doctypes that also trigger quirks are not detected). compatMode now reports BackCompat accordingly, and getElementsByClassName bakes the mode into its live-collection filter (ClassNameFilter), with classAttributeContainsCase doing the ASCII-case-insensitive token scan. Coverage: /dom/nodes/getElementsByClassName-14.htm 1/2 -> 2/2 (fully green). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -179,6 +179,21 @@ pub fn setLocation(self: *Document, url: [:0]const u8) !void {
|
||||
return frame.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .push = null } }, .{ .script = frame });
|
||||
}
|
||||
|
||||
// Approximation of quirks mode: an HTML document without a doctype is in
|
||||
// quirks mode. (Legacy doctypes that also trigger quirks are not detected.)
|
||||
pub fn isQuirksMode(self: *const Document) bool {
|
||||
if (self._type != .html) {
|
||||
return false;
|
||||
}
|
||||
var child = self._proto.firstChild();
|
||||
while (child) |c| : (child = c.nextSibling()) {
|
||||
if (c._type == .document_type) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn getCharset(self: *const Document) []const u8 {
|
||||
if (self._charset) |charset| {
|
||||
return charset;
|
||||
@@ -1445,6 +1460,7 @@ pub const JsApi = struct {
|
||||
return frame._factory.node(Document{
|
||||
._proto = undefined,
|
||||
._type = .generic,
|
||||
._url = "about:blank",
|
||||
._charset = "UTF-8",
|
||||
});
|
||||
}
|
||||
@@ -1534,7 +1550,10 @@ pub const JsApi = struct {
|
||||
pub const characterSet = bridge.accessor(getCharacterSet, null, .{});
|
||||
pub const charset = bridge.accessor(getCharacterSet, null, .{});
|
||||
pub const inputEncoding = bridge.accessor(getCharacterSet, null, .{});
|
||||
pub const compatMode = bridge.property("CSS1Compat", .{ .template = false });
|
||||
pub const compatMode = bridge.accessor(getCompatMode, null, .{});
|
||||
fn getCompatMode(self: *const Document) []const u8 {
|
||||
return if (self.isQuirksMode()) "BackCompat" else "CSS1Compat";
|
||||
}
|
||||
|
||||
fn getCharacterSet(self: *const Document) []const u8 {
|
||||
return self.getCharset();
|
||||
|
||||
@@ -1398,7 +1398,12 @@ pub fn getElementsByClassName(self: *Node, class_name: []const u8, frame: *Frame
|
||||
try class_names.append(arena, try frame.dupeString(name));
|
||||
}
|
||||
|
||||
return collections.NodeLive(.class_name).init(self, class_names.items, frame);
|
||||
const doc: ?*Document = if (self._type == .document) self._type.document else self.ownerDocument(frame);
|
||||
const quirks = if (doc) |d| d.isQuirksMode() else false;
|
||||
return collections.NodeLive(.class_name).init(self, .{
|
||||
.names = class_names.items,
|
||||
.case_insensitive = quirks,
|
||||
}, frame);
|
||||
}
|
||||
|
||||
/// Shared implementation of replaceChildren for Element, Document, and DocumentFragment.
|
||||
|
||||
@@ -46,6 +46,13 @@ const Mode = enum {
|
||||
form,
|
||||
};
|
||||
|
||||
pub const ClassNameFilter = struct {
|
||||
names: [][]const u8,
|
||||
// getElementsByClassName matches class names ASCII case-insensitively
|
||||
// when the document is in quirks mode.
|
||||
case_insensitive: bool = false,
|
||||
};
|
||||
|
||||
pub const TagNameNsFilter = struct {
|
||||
namespace: ?Element.Namespace, // null means wildcard "*"
|
||||
local_name: String,
|
||||
@@ -55,7 +62,7 @@ const Filters = union(Mode) {
|
||||
tag: Element.Tag,
|
||||
tag_name: String,
|
||||
tag_name_ns: TagNameNsFilter,
|
||||
class_name: [][]const u8,
|
||||
class_name: ClassNameFilter,
|
||||
name: []const u8,
|
||||
all_elements,
|
||||
child_elements,
|
||||
@@ -261,14 +268,14 @@ pub fn NodeLive(comptime mode: Mode) type {
|
||||
return self._filter.local_name.eqlSlice(el.getLocalName());
|
||||
},
|
||||
.class_name => {
|
||||
if (self._filter.len == 0) {
|
||||
if (self._filter.names.len == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const el = node.is(Element) orelse return false;
|
||||
const class_attr = el.getAttributeSafe(comptime .wrap("class")) orelse return false;
|
||||
for (self._filter) |class_name| {
|
||||
if (!Selector.classAttributeContains(class_attr, class_name)) {
|
||||
for (self._filter.names) |class_name| {
|
||||
if (!Selector.classAttributeContainsCase(class_attr, class_name, self._filter.case_insensitive)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,10 +183,18 @@ pub fn matchesUncached(arena: Allocator, el: *Node.Element, input: []const u8, f
|
||||
}
|
||||
|
||||
pub fn classAttributeContains(class_attr: []const u8, class_name: []const u8) bool {
|
||||
return classAttributeContainsCase(class_attr, class_name, false);
|
||||
}
|
||||
|
||||
pub fn classAttributeContainsCase(class_attr: []const u8, class_name: []const u8, case_insensitive: bool) bool {
|
||||
if (class_name.len == 0 or class_name.len > class_attr.len) return false;
|
||||
|
||||
var search = class_attr;
|
||||
while (std.mem.indexOf(u8, search, class_name)) |pos| {
|
||||
while (if (case_insensitive)
|
||||
std.ascii.indexOfIgnoreCase(search, class_name)
|
||||
else
|
||||
std.mem.indexOf(u8, search, class_name)) |pos|
|
||||
{
|
||||
const is_start = pos == 0 or isClassWhitespace(search[pos - 1]);
|
||||
const end = pos + class_name.len;
|
||||
const is_end = end == search.len or isClassWhitespace(search[end]);
|
||||
|
||||
Reference in New Issue
Block a user