diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index 0ff835480..92abc1fcd 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -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(); diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index 59cc05669..d1ea31693 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -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. diff --git a/src/browser/webapi/collections/node_live.zig b/src/browser/webapi/collections/node_live.zig index 69e30d178..79b4bf87f 100644 --- a/src/browser/webapi/collections/node_live.zig +++ b/src/browser/webapi/collections/node_live.zig @@ -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; } } diff --git a/src/browser/webapi/selector/Selector.zig b/src/browser/webapi/selector/Selector.zig index ec5c09532..f6bdae851 100644 --- a/src/browser/webapi/selector/Selector.zig +++ b/src/browser/webapi/selector/Selector.zig @@ -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]);