fix: Prevent class selector from doing substring match

Previously, "aa" would have matched "baaa". Now the matching is strict equality
on tokenized words.
This commit is contained in:
Karl Seguin
2026-07-16 16:07:20 +08:00
parent f40fd3a0bc
commit dcd8a6ed07
2 changed files with 19 additions and 24 deletions

View File

@@ -15,6 +15,8 @@
<div class="nested">nested2</div>
</div>
<div class="baaa">overlap1</div>
<script>
const foos = document.getElementsByClassName('foo');
</script>
@@ -96,3 +98,10 @@
testing.expectEqual('nested1', nested[0].textContent);
testing.expectEqual('nested2', nested[1].textContent);
</script>
<script id=overlappingSubstring>
// "baaa" is a single class token; "aa" is a substring of it but not its own
// token, so it must not match.
const overlap = document.getElementsByClassName('aa');
testing.expectEqual(0, overlap.length);
</script>

View File

@@ -187,37 +187,23 @@ pub fn classAttributeContains(class_attr: []const u8, class_name: []const u8) bo
}
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) {
if (class_name.len == 0) {
return false;
}
var search = class_attr;
while (true) {
const pos = (if (case_insensitive)
std.ascii.indexOfIgnoreCase(search, class_name)
else
std.mem.indexOf(u8, search, class_name)) orelse break;
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]);
if (is_start and is_end) return true;
search = search[pos + 1 ..];
var it = std.mem.tokenizeAny(u8, class_attr, &[_]u8{ '\t', '\n', 0x0C, '\r', ' ' });
while (it.next()) |token| {
if (case_insensitive) {
if (std.ascii.eqlIgnoreCase(token, class_name)) {
return true;
}
} else if (std.mem.eql(u8, token, class_name)) {
return true;
}
}
return false;
}
// The class attribute tokens are separated by ASCII whitespace (which,
// unlike std.ascii.isWhitespace, does not include vertical tab).
fn isClassWhitespace(c: u8) bool {
return switch (c) {
'\t', '\n', 0x0C, '\r', ' ' => true,
else => false,
};
}
pub const Part = union(enum) {
id: []const u8,
class: []const u8,