diff --git a/src/css/match_test.zig b/src/css/match_test.zig index 6997a970..d0ac3c81 100644 --- a/src/css/match_test.zig +++ b/src/css/match_test.zig @@ -83,6 +83,76 @@ test "matchFirst" { .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "foo t1" } } }, .exp = 1, }, + .{ + .q = "[foo]", + .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p" } } }, + .exp = 0, + }, + .{ + .q = "[foo]", + .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } }, + .exp = 1, + }, + .{ + .q = "[foo=bar]", + .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } }, + .exp = 1, + }, + .{ + .q = "[foo=baz]", + .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } }, + .exp = 0, + }, + .{ + .q = "[foo!=bar]", + .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } }, + .exp = 1, + }, + .{ + .q = "[foo!=baz]", + .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } }, + .exp = 1, + }, + .{ + .q = "[foo~=bar]", + .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "baz bar" } } }, + .exp = 1, + }, + .{ + .q = "[foo~=bar]", + .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } }, + .exp = 0, + }, + .{ + .q = "[foo^=bar]", + .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } }, + .exp = 1, + }, + .{ + .q = "[foo$=baz]", + .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } }, + .exp = 1, + }, + .{ + .q = "[foo*=rb]", + .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } }, + .exp = 1, + }, + .{ + .q = "[foo|=bar]", + .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } }, + .exp = 1, + }, + .{ + .q = "[foo|=bar]", + .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar-baz" } } }, + .exp = 1, + }, + .{ + .q = "[foo|=bar]", + .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "ba" } } }, + .exp = 0, + }, }; for (testcases) |tc| { @@ -127,6 +197,76 @@ test "matchAll" { .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "foo t1" } } }, .exp = 1, }, + .{ + .q = "[foo]", + .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p" } } }, + .exp = 0, + }, + .{ + .q = "[foo]", + .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } }, + .exp = 1, + }, + .{ + .q = "[foo=bar]", + .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } }, + .exp = 1, + }, + .{ + .q = "[foo=baz]", + .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } }, + .exp = 0, + }, + .{ + .q = "[foo!=bar]", + .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } }, + .exp = 1, + }, + .{ + .q = "[foo!=baz]", + .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } }, + .exp = 2, + }, + .{ + .q = "[foo~=bar]", + .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "baz bar" } } }, + .exp = 1, + }, + .{ + .q = "[foo~=bar]", + .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } }, + .exp = 0, + }, + .{ + .q = "[foo^=bar]", + .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } }, + .exp = 1, + }, + .{ + .q = "[foo$=baz]", + .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } }, + .exp = 1, + }, + .{ + .q = "[foo*=rb]", + .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } }, + .exp = 1, + }, + .{ + .q = "[foo|=bar]", + .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } }, + .exp = 1, + }, + .{ + .q = "[foo|=bar]", + .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar-baz" } } }, + .exp = 1, + }, + .{ + .q = "[foo|=bar]", + .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "ba" } } }, + .exp = 0, + }, }; for (testcases) |tc| { diff --git a/src/css/selector.zig b/src/css/selector.zig index 06b95937..8eae1aef 100644 --- a/src/css/selector.zig +++ b/src/css/selector.zig @@ -166,20 +166,69 @@ pub const Selector = union(enum) { pseudo_element: PseudoClass, // returns true if s is a whitespace-separated list that includes val. - fn contains(haystack: []const u8, needle: []const u8) bool { + fn word(haystack: []const u8, needle: []const u8, ci: bool) bool { if (haystack.len == 0) return false; var it = std.mem.splitAny(u8, haystack, " \t\r\n"); // TODO add \f while (it.next()) |part| { - if (std.mem.eql(u8, part, needle)) return true; + if (eql(part, needle, ci)) return true; } return false; } + fn eql(a: []const u8, b: []const u8, ci: bool) bool { + if (ci) return std.ascii.eqlIgnoreCase(a, b); + return std.mem.eql(u8, a, b); + } + + fn starts(haystack: []const u8, needle: []const u8, ci: bool) bool { + if (ci) return std.ascii.startsWithIgnoreCase(haystack, needle); + return std.mem.startsWith(u8, haystack, needle); + } + + fn ends(haystack: []const u8, needle: []const u8, ci: bool) bool { + if (ci) return std.ascii.endsWithIgnoreCase(haystack, needle); + return std.mem.endsWith(u8, haystack, needle); + } + + fn contains(haystack: []const u8, needle: []const u8, ci: bool) bool { + if (ci) return std.ascii.indexOfIgnoreCase(haystack, needle) != null; + return std.mem.indexOf(u8, haystack, needle) != null; + } + pub fn match(s: Selector, n: anytype) !bool { return switch (s) { .tag => |v| n.isElement() and std.ascii.eqlIgnoreCase(v, try n.tag()), .id => |v| return n.isElement() and std.mem.eql(u8, v, try n.attr("id") orelse return false), - .class => |v| return n.isElement() and contains(try n.attr("class") orelse return false, v), + .class => |v| return n.isElement() and word(try n.attr("class") orelse return false, v, false), + .attribute => |v| { + const attr = try n.attr(v.key); + + if (v.op == null) return attr != null; + if (v.val == null or v.val.?.len == 0) return false; + + const val = v.val.?; + + return switch (v.op.?) { + .eql => attr != null and eql(attr.?, val, v.ci), + .not_eql => attr == null or !eql(attr.?, val, v.ci), + .one_of => attr != null and word(attr.?, val, v.ci), + .prefix => attr != null and starts(attr.?, val, v.ci), + .suffix => attr != null and ends(attr.?, val, v.ci), + .contains => attr != null and contains(attr.?, val, v.ci), + .prefix_hyphen => { + if (attr == null) return false; + if (eql(attr.?, val, v.ci)) return true; + + if (attr.?.len <= val.len) return false; + + if (!starts(attr.?, val, v.ci)) return false; + + return attr.?[val.len] == '-'; + }, + .regexp => false, // TODO handle regexp attribute operator. + }; + }, + .never_match => return false, else => false, }; }