", .exp = 1 },
- .{ .q = "p:only-child", .html = "", .exp = 1 },
- .{ .q = "p:only-of-type", .html = "", .exp = 1 },
- .{ .q = ":empty", .html = "Hello
", .exp = 3 },
- .{ .q = "div p", .html = "", .exp = 2 },
- .{ .q = "div table p", .html = "
", .exp = 1 },
- .{ .q = "div > p", .html = "
", .exp = 2 },
- .{ .q = "p ~ p", .html = "", .exp = 2 },
- .{ .q = "p + p", .html = "
", .exp = 1 },
- .{ .q = "li, p", .html = "
", .exp = 3 },
- .{ .q = "p +/*This is a comment*/ p", .html = "
", .exp = 1 },
- // .{ .q = "p:contains(\"that wraps\")", .html = "
Text block that wraps inner text and continues
", .exp = 1 },
- // .{ .q = "p:containsOwn(\"that wraps\")", .html = "Text block that wraps inner text and continues
", .exp = 0 },
- // .{ .q = ":containsOwn(\"inner\")", .html = "Text block that wraps inner text and continues
", .exp = 1 },
- // .{ .q = "p:containsOwn(\"block\")", .html = "Text block that wraps inner text and continues
", .exp = 1 },
- .{ .q = "div:has(#p1)", .html = "", .exp = 1 },
- // .{ .q = "div:has(:containsOwn(\"2\"))", .html = "
", .exp = 1 },
- // .{ .q = "body :has(:containsOwn(\"2\"))", .html = "
", .exp = 2 },
- // .{ .q = "body :haschild(:containsOwn(\"2\"))", .html = "
", .exp = 1 },
- // .{ .q = "p:matches([\\d])", .html = "
0123456789
abcdef
0123ABCD
", .exp = 2 },
- // .{ .q = "p:matches([a-z])", .html = "
0123456789
abcdef
0123ABCD
", .exp = 1 },
- // .{ .q = "p:matches([a-zA-Z])", .html = "
0123456789
abcdef
0123ABCD
", .exp = 2 },
- // .{ .q = "p:matches([^\\d])", .html = "
0123456789
abcdef
0123ABCD
", .exp = 2 },
- // .{ .q = "p:matches(^(0|a))", .html = "
0123456789
abcdef
0123ABCD
", .exp = 3 },
- // .{ .q = "p:matches(^\\d+$)", .html = "
0123456789
abcdef
0123ABCD
", .exp = 1 },
- // .{ .q = "p:not(:matches(^\\d+$))", .html = "
0123456789
abcdef
0123ABCD
", .exp = 2 },
- // .{ .q = "div :matchesOwn(^\\d+$)", .html = "
0123456789
", .exp = 2 },
- // .{ .q = "[href#=(fina)]:not([href#=(\\/\\/[^\\/]+untrusted)])", .html = "
", .exp = 2 },
- // .{ .q = "[href#=(^https:\\/\\/[^\\/]*\\/?news)]", .html = "
", .exp = 1 },
- .{ .q = ":input", .html = "
", .exp = 5 },
- .{ .q = ":root", .html = "", .exp = 1 },
- .{ .q = "*:root", .html = "", .exp = 1 },
- .{ .q = "html:nth-child(1)", .html = "", .exp = 1 },
- .{ .q = "*:root:first-child", .html = "", .exp = 1 },
- .{ .q = "*:root:nth-child(1)", .html = "", .exp = 1 },
- .{ .q = "a:not(:root)", .html = "
", .exp = 1 },
- .{ .q = "body > *:nth-child(3n+2)", .html = "
", .exp = 2 },
- .{ .q = "input:disabled", .html = "
", .exp = 1 },
- .{ .q = ":disabled", .html = "
", .exp = 1 },
- .{ .q = ":enabled", .html = "
", .exp = 1 },
- .{ .q = "div.class1, div.class2", .html = "
", .exp = 2 },
- };
-
- for (testcases) |tc| {
- matcher.reset();
-
- const doc = try parser.documentHTMLParseFromStr(tc.html);
- defer parser.documentHTMLClose(doc) catch {};
-
- const s = css.parse(alloc, tc.q, .{}) catch |e| {
- std.debug.print("parse, query: {s}\n", .{tc.q});
- return e;
- };
- defer s.deinit(alloc);
-
- const node = Node{ .node = parser.documentHTMLToNode(doc) };
-
- _ = css.matchAll(s, node, &matcher) catch |e| {
- std.debug.print("match, query: {s}\n", .{tc.q});
- return e;
- };
- std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
- std.debug.print("expectation, query: {s}\n", .{tc.q});
- return e;
- };
- }
-}
diff --git a/src/browser/css/match_test.zig b/src/browser/css/match_test.zig
deleted file mode 100644
index a62c3727..00000000
--- a/src/browser/css/match_test.zig
+++ /dev/null
@@ -1,591 +0,0 @@
-// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
-//
-// Francis Bouvier
-// Pierre Tachoire
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as
-// published by the Free Software Foundation, either version 3 of the
-// License, or (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-const std = @import("std");
-const css = @import("css.zig");
-
-// Node mock implementation for test only.
-pub const Node = struct {
- child: ?*const Node = null,
- last: ?*const Node = null,
- sibling: ?*const Node = null,
- prev: ?*const Node = null,
- par: ?*const Node = null,
-
- name: []const u8 = "",
- att: ?[]const u8 = null,
-
- pub fn firstChild(n: *const Node) !?*const Node {
- return n.child;
- }
-
- pub fn lastChild(n: *const Node) !?*const Node {
- return n.last;
- }
-
- pub fn nextSibling(n: *const Node) !?*const Node {
- return n.sibling;
- }
-
- pub fn prevSibling(n: *const Node) !?*const Node {
- return n.prev;
- }
-
- pub fn parent(n: *const Node) !?*const Node {
- return n.par;
- }
-
- pub fn isElement(_: *const Node) bool {
- return true;
- }
-
- pub fn isDocument(_: *const Node) bool {
- return false;
- }
-
- pub fn isComment(_: *const Node) bool {
- return false;
- }
-
- pub fn isText(_: *const Node) bool {
- return false;
- }
-
- pub fn isEmptyText(_: *const Node) !bool {
- return false;
- }
-
- pub fn tag(n: *const Node) ![]const u8 {
- return n.name;
- }
-
- pub fn attr(n: *const Node, _: []const u8) !?[]const u8 {
- return n.att;
- }
-
- pub fn eql(a: *const Node, b: *const Node) bool {
- return a == b;
- }
-};
-
-const Matcher = struct {
- const Nodes = std.ArrayListUnmanaged(*const Node);
-
- nodes: Nodes,
- allocator: Allocator,
-
- fn init(allocator: Allocator) Matcher {
- return .{
- .nodes = .empty,
- .allocator = allocator,
- };
- }
-
- fn deinit(m: *Matcher) void {
- m.nodes.deinit(self.allocator);
- }
-
- fn reset(m: *Matcher) void {
- m.nodes.clearRetainingCapacity();
- }
-
- pub fn match(m: *Matcher, n: *const Node) !void {
- try m.nodes.append(self.allocator, n);
- }
-};
-
-test "matchFirst" {
- const alloc = std.testing.allocator;
-
- var matcher = Matcher.init(alloc);
- defer matcher.deinit();
-
- const testcases = [_]struct {
- q: []const u8,
- n: Node,
- exp: usize,
- }{
- .{
- .q = "address",
- .n = .{ .child = &.{ .name = "body", .child = &.{ .name = "address" } } },
- .exp = 1,
- },
- .{
- .q = "#foo",
- .n = .{ .child = &.{ .name = "p", .att = "foo", .child = &.{ .name = "p" } } },
- .exp = 1,
- },
- .{
- .q = ".t1",
- .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "t1" } } },
- .exp = 1,
- },
- .{
- .q = ".t1",
- .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,
- },
- .{
- .q = "strong, a",
- .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
- .exp = 1,
- },
- .{
- .q = "p a",
- .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .name = "p" } }, .sibling = &.{ .name = "a" } } },
- .exp = 1,
- },
- .{
- .q = "p a",
- .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "span", .child = &.{
- .name = "a",
- .par = &.{ .name = "span", .par = &.{ .name = "p" } },
- } } } },
- .exp = 1,
- },
- .{
- .q = ":not(p)",
- .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
- .exp = 1,
- },
- .{
- .q = "p:has(a)",
- .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
- .exp = 1,
- },
- .{
- .q = "p:has(strong)",
- .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
- .exp = 0,
- },
- .{
- .q = "p:haschild(a)",
- .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
- .exp = 1,
- },
- .{
- .q = "p:haschild(strong)",
- .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
- .exp = 0,
- },
- .{
- .q = "p:lang(en)",
- .n = .{ .child = &.{ .name = "p", .att = "en-US", .child = &.{ .name = "a" } } },
- .exp = 1,
- },
- .{
- .q = "a:lang(en)",
- .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .att = "en-US" } } } },
- .exp = 1,
- },
- };
-
- for (testcases) |tc| {
- matcher.reset();
-
- const s = try css.parse(alloc, tc.q, .{});
- defer s.deinit(alloc);
-
- _ = css.matchFirst(s, &tc.n, &matcher) catch |e| {
- std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
- return e;
- };
-
- std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
- std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
- return e;
- };
- }
-}
-
-test "matchAll" {
- const alloc = std.testing.allocator;
-
- var matcher = Matcher.init(alloc);
- defer matcher.deinit();
-
- const testcases = [_]struct {
- q: []const u8,
- n: Node,
- exp: usize,
- }{
- .{
- .q = "address",
- .n = .{ .child = &.{ .name = "body", .child = &.{ .name = "address" } } },
- .exp = 1,
- },
- .{
- .q = "#foo",
- .n = .{ .child = &.{ .name = "p", .att = "foo", .child = &.{ .name = "p" } } },
- .exp = 1,
- },
- .{
- .q = ".t1",
- .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "t1" } } },
- .exp = 1,
- },
- .{
- .q = ".t1",
- .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,
- },
- .{
- .q = "strong, a",
- .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
- .exp = 2,
- },
- .{
- .q = "p a",
- .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .name = "p" } }, .sibling = &.{ .name = "a" } } },
- .exp = 1,
- },
- .{
- .q = "p a",
- .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "span", .child = &.{
- .name = "a",
- .par = &.{ .name = "span", .par = &.{ .name = "p" } },
- } } } },
- .exp = 1,
- },
- .{
- .q = ":not(p)",
- .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
- .exp = 2,
- },
- .{
- .q = "p:has(a)",
- .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
- .exp = 1,
- },
- .{
- .q = "p:has(strong)",
- .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
- .exp = 0,
- },
- .{
- .q = "p:haschild(a)",
- .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
- .exp = 1,
- },
- .{
- .q = "p:haschild(strong)",
- .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
- .exp = 0,
- },
- .{
- .q = "p:lang(en)",
- .n = .{ .child = &.{ .name = "p", .att = "en-US", .child = &.{ .name = "a" } } },
- .exp = 1,
- },
- .{
- .q = "a:lang(en)",
- .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .att = "en-US" } } } },
- .exp = 1,
- },
- };
-
- for (testcases) |tc| {
- matcher.reset();
-
- const s = try css.parse(alloc, tc.q, .{});
- defer s.deinit(alloc);
-
- css.matchAll(s, &tc.n, &matcher) catch |e| {
- std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
- return e;
- };
-
- std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
- std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
- return e;
- };
- }
-}
-
-test "pseudo class" {
- const alloc = std.testing.allocator;
-
- var matcher = Matcher.init(alloc);
- defer matcher.deinit();
-
- var p1: Node = .{ .name = "p" };
- var p2: Node = .{ .name = "p" };
- var a1: Node = .{ .name = "a" };
-
- p1.sibling = &p2;
- p2.prev = &p1;
-
- p2.sibling = &a1;
- a1.prev = &p2;
-
- var root: Node = .{ .child = &p1, .last = &a1 };
- p1.par = &root;
- p2.par = &root;
- a1.par = &root;
-
- const testcases = [_]struct {
- q: []const u8,
- n: Node,
- exp: ?*const Node,
- }{
- .{ .q = "p:only-child", .n = root, .exp = null },
- .{ .q = "a:only-of-type", .n = root, .exp = &a1 },
- };
-
- for (testcases) |tc| {
- matcher.reset();
-
- const s = try css.parse(alloc, tc.q, .{});
- defer s.deinit(alloc);
-
- css.matchAll(s, &tc.n, &matcher) catch |e| {
- std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
- return e;
- };
-
- if (tc.exp) |exp_n| {
- const exp: usize = 1;
- std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
- std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
- return e;
- };
-
- std.testing.expectEqual(exp_n, matcher.nodes.items[0]) catch |e| {
- std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
- return e;
- };
-
- continue;
- }
-
- const exp: usize = 0;
- std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
- std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
- return e;
- };
- }
-}
-
-test "nth pseudo class" {
- const alloc = std.testing.allocator;
-
- var matcher = Matcher.init(alloc);
- defer matcher.deinit();
-
- var p1: Node = .{ .name = "p" };
- var p2: Node = .{ .name = "p" };
-
- p1.sibling = &p2;
- p2.prev = &p1;
-
- var root: Node = .{ .child = &p1, .last = &p2 };
- p1.par = &root;
- p2.par = &root;
-
- const testcases = [_]struct {
- q: []const u8,
- n: Node,
- exp: ?*const Node,
- }{
- .{ .q = "a:nth-of-type(1)", .n = root, .exp = null },
- .{ .q = "p:nth-of-type(1)", .n = root, .exp = &p1 },
- .{ .q = "p:nth-of-type(2)", .n = root, .exp = &p2 },
- .{ .q = "p:nth-of-type(0)", .n = root, .exp = null },
- .{ .q = "p:nth-of-type(2n)", .n = root, .exp = &p2 },
- .{ .q = "p:nth-last-child(1)", .n = root, .exp = &p2 },
- .{ .q = "p:nth-last-child(2)", .n = root, .exp = &p1 },
- .{ .q = "p:nth-child(1)", .n = root, .exp = &p1 },
- .{ .q = "p:nth-child(2)", .n = root, .exp = &p2 },
- .{ .q = "p:nth-child(odd)", .n = root, .exp = &p1 },
- .{ .q = "p:nth-child(even)", .n = root, .exp = &p2 },
- .{ .q = "p:nth-child(n+2)", .n = root, .exp = &p2 },
- };
-
- for (testcases) |tc| {
- matcher.reset();
-
- const s = try css.parse(alloc, tc.q, .{});
- defer s.deinit(alloc);
-
- css.matchAll(s, &tc.n, &matcher) catch |e| {
- std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
- return e;
- };
-
- if (tc.exp) |exp_n| {
- const exp: usize = 1;
- std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
- std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
- return e;
- };
-
- std.testing.expectEqual(exp_n, matcher.nodes.items[0]) catch |e| {
- std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
- return e;
- };
-
- continue;
- }
-
- const exp: usize = 0;
- std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
- std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
- return e;
- };
- }
-}
diff --git a/src/browser/css/parser.zig b/src/browser/css/parser.zig
index 7b814ebc..63b863d8 100644
--- a/src/browser/css/parser.zig
+++ b/src/browser/css/parser.zig
@@ -557,8 +557,6 @@ pub const Parser = struct {
const val = try buf.toOwnedSlice(allocator);
errdefer allocator.free(val);
- lowerstr(val);
-
return .{ .pseudo_class_contains = .{ .own = pseudo_class == .containsown, .val = val } };
},
.matches, .matchesown => {
diff --git a/src/browser/css/selector.zig b/src/browser/css/selector.zig
index e36717f0..00ef1558 100644
--- a/src/browser/css/selector.zig
+++ b/src/browser/css/selector.zig
@@ -17,6 +17,8 @@
// along with this program. If not, see .
const std = @import("std");
+const Allocator = std.mem.Allocator;
+const css = @import("css.zig");
pub const AttributeOP = enum {
eql, // =
@@ -432,7 +434,25 @@ pub const Selector = union(enum) {
else => Error.UnsupportedRelativePseudoClass,
};
},
- .pseudo_class_contains => return Error.UnsupportedContainsPseudoClass, // TODO, need mem allocation.
+ .pseudo_class_contains => |v| {
+ // Only containsOwn is implemented.
+ if (v.own == false) return Error.UnsupportedContainsPseudoClass;
+
+ var c = try n.firstChild();
+ while (c != null) {
+ if (c.?.isText()) {
+ const text = try c.?.text();
+ if (text) |_text| {
+ if (contains(_text, v.val, false)) { // we are case sensitive. Is this correct behavior?
+ return true;
+ }
+ }
+ }
+
+ c = try c.?.nextSibling();
+ }
+ return false;
+ },
.pseudo_class_regexp => return Error.UnsupportedRegexpPseudoClass, // TODO need mem allocation.
.pseudo_class_nth => |v| {
if (v.a == 0) {
@@ -827,3 +847,578 @@ pub const Selector = union(enum) {
}
}
};
+
+// NodeTest mock implementation for test only.
+pub const NodeTest = struct {
+ child: ?*const NodeTest = null,
+ last: ?*const NodeTest = null,
+ sibling: ?*const NodeTest = null,
+ prev: ?*const NodeTest = null,
+ par: ?*const NodeTest = null,
+
+ name: []const u8 = "",
+ att: ?[]const u8 = null,
+
+ pub fn firstChild(n: *const NodeTest) !?*const NodeTest {
+ return n.child;
+ }
+
+ pub fn lastChild(n: *const NodeTest) !?*const NodeTest {
+ return n.last;
+ }
+
+ pub fn nextSibling(n: *const NodeTest) !?*const NodeTest {
+ return n.sibling;
+ }
+
+ pub fn prevSibling(n: *const NodeTest) !?*const NodeTest {
+ return n.prev;
+ }
+
+ pub fn parent(n: *const NodeTest) !?*const NodeTest {
+ return n.par;
+ }
+
+ pub fn isElement(_: *const NodeTest) bool {
+ return true;
+ }
+
+ pub fn isDocument(_: *const NodeTest) bool {
+ return false;
+ }
+
+ pub fn isComment(_: *const NodeTest) bool {
+ return false;
+ }
+
+ pub fn text(_: *const NodeTest) !?[]const u8 {
+ return null;
+ }
+
+ pub fn isText(_: *const NodeTest) bool {
+ return false;
+ }
+
+ pub fn isEmptyText(_: *const NodeTest) !bool {
+ return false;
+ }
+
+ pub fn tag(n: *const NodeTest) ![]const u8 {
+ return n.name;
+ }
+
+ pub fn attr(n: *const NodeTest, _: []const u8) !?[]const u8 {
+ return n.att;
+ }
+
+ pub fn eql(a: *const NodeTest, b: *const NodeTest) bool {
+ return a == b;
+ }
+};
+
+const MatcherTest = struct {
+ const NodeTests = std.ArrayListUnmanaged(*const NodeTest);
+
+ nodes: NodeTests,
+ allocator: Allocator,
+
+ fn init(allocator: Allocator) MatcherTest {
+ return .{
+ .nodes = .empty,
+ .allocator = allocator,
+ };
+ }
+
+ fn deinit(m: *MatcherTest) void {
+ m.nodes.deinit(m.allocator);
+ }
+
+ fn reset(m: *MatcherTest) void {
+ m.nodes.clearRetainingCapacity();
+ }
+
+ pub fn match(m: *MatcherTest, n: *const NodeTest) !void {
+ try m.nodes.append(m.allocator, n);
+ }
+};
+
+test "Browser.CSS.Selector: matchFirst" {
+ const alloc = std.testing.allocator;
+
+ var matcher = MatcherTest.init(alloc);
+ defer matcher.deinit();
+
+ const testcases = [_]struct {
+ q: []const u8,
+ n: NodeTest,
+ exp: usize,
+ }{
+ .{
+ .q = "address",
+ .n = .{ .child = &.{ .name = "body", .child = &.{ .name = "address" } } },
+ .exp = 1,
+ },
+ .{
+ .q = "#foo",
+ .n = .{ .child = &.{ .name = "p", .att = "foo", .child = &.{ .name = "p" } } },
+ .exp = 1,
+ },
+ .{
+ .q = ".t1",
+ .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "t1" } } },
+ .exp = 1,
+ },
+ .{
+ .q = ".t1",
+ .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,
+ },
+ .{
+ .q = "strong, a",
+ .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
+ .exp = 1,
+ },
+ .{
+ .q = "p a",
+ .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .name = "p" } }, .sibling = &.{ .name = "a" } } },
+ .exp = 1,
+ },
+ .{
+ .q = "p a",
+ .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "span", .child = &.{
+ .name = "a",
+ .par = &.{ .name = "span", .par = &.{ .name = "p" } },
+ } } } },
+ .exp = 1,
+ },
+ .{
+ .q = ":not(p)",
+ .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
+ .exp = 1,
+ },
+ .{
+ .q = "p:has(a)",
+ .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
+ .exp = 1,
+ },
+ .{
+ .q = "p:has(strong)",
+ .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
+ .exp = 0,
+ },
+ .{
+ .q = "p:haschild(a)",
+ .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
+ .exp = 1,
+ },
+ .{
+ .q = "p:haschild(strong)",
+ .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
+ .exp = 0,
+ },
+ .{
+ .q = "p:lang(en)",
+ .n = .{ .child = &.{ .name = "p", .att = "en-US", .child = &.{ .name = "a" } } },
+ .exp = 1,
+ },
+ .{
+ .q = "a:lang(en)",
+ .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .att = "en-US" } } } },
+ .exp = 1,
+ },
+ };
+
+ for (testcases) |tc| {
+ matcher.reset();
+
+ const s = try css.parse(alloc, tc.q, .{});
+ defer s.deinit(alloc);
+
+ _ = css.matchFirst(&s, &tc.n, &matcher) catch |e| {
+ std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
+ return e;
+ };
+
+ std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
+ std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
+ return e;
+ };
+ }
+}
+
+test "Browser.CSS.Selector: matchAll" {
+ const alloc = std.testing.allocator;
+
+ var matcher = MatcherTest.init(alloc);
+ defer matcher.deinit();
+
+ const testcases = [_]struct {
+ q: []const u8,
+ n: NodeTest,
+ exp: usize,
+ }{
+ .{
+ .q = "address",
+ .n = .{ .child = &.{ .name = "body", .child = &.{ .name = "address" } } },
+ .exp = 1,
+ },
+ .{
+ .q = "#foo",
+ .n = .{ .child = &.{ .name = "p", .att = "foo", .child = &.{ .name = "p" } } },
+ .exp = 1,
+ },
+ .{
+ .q = ".t1",
+ .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "t1" } } },
+ .exp = 1,
+ },
+ .{
+ .q = ".t1",
+ .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,
+ },
+ .{
+ .q = "strong, a",
+ .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
+ .exp = 2,
+ },
+ .{
+ .q = "p a",
+ .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .name = "p" } }, .sibling = &.{ .name = "a" } } },
+ .exp = 1,
+ },
+ .{
+ .q = "p a",
+ .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "span", .child = &.{
+ .name = "a",
+ .par = &.{ .name = "span", .par = &.{ .name = "p" } },
+ } } } },
+ .exp = 1,
+ },
+ .{
+ .q = ":not(p)",
+ .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
+ .exp = 2,
+ },
+ .{
+ .q = "p:has(a)",
+ .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
+ .exp = 1,
+ },
+ .{
+ .q = "p:has(strong)",
+ .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
+ .exp = 0,
+ },
+ .{
+ .q = "p:haschild(a)",
+ .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
+ .exp = 1,
+ },
+ .{
+ .q = "p:haschild(strong)",
+ .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
+ .exp = 0,
+ },
+ .{
+ .q = "p:lang(en)",
+ .n = .{ .child = &.{ .name = "p", .att = "en-US", .child = &.{ .name = "a" } } },
+ .exp = 1,
+ },
+ .{
+ .q = "a:lang(en)",
+ .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .att = "en-US" } } } },
+ .exp = 1,
+ },
+ };
+
+ for (testcases) |tc| {
+ matcher.reset();
+
+ const s = try css.parse(alloc, tc.q, .{});
+ defer s.deinit(alloc);
+
+ css.matchAll(&s, &tc.n, &matcher) catch |e| {
+ std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
+ return e;
+ };
+
+ std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
+ std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
+ return e;
+ };
+ }
+}
+
+test "Browser.CSS.Selector: pseudo class" {
+ const alloc = std.testing.allocator;
+
+ var matcher = MatcherTest.init(alloc);
+ defer matcher.deinit();
+
+ var p1: NodeTest = .{ .name = "p" };
+ var p2: NodeTest = .{ .name = "p" };
+ var a1: NodeTest = .{ .name = "a" };
+
+ p1.sibling = &p2;
+ p2.prev = &p1;
+
+ p2.sibling = &a1;
+ a1.prev = &p2;
+
+ var root: NodeTest = .{ .child = &p1, .last = &a1 };
+ p1.par = &root;
+ p2.par = &root;
+ a1.par = &root;
+
+ const testcases = [_]struct {
+ q: []const u8,
+ n: NodeTest,
+ exp: ?*const NodeTest,
+ }{
+ .{ .q = "p:only-child", .n = root, .exp = null },
+ .{ .q = "a:only-of-type", .n = root, .exp = &a1 },
+ };
+
+ for (testcases) |tc| {
+ matcher.reset();
+
+ const s = try css.parse(alloc, tc.q, .{});
+ defer s.deinit(alloc);
+
+ css.matchAll(&s, &tc.n, &matcher) catch |e| {
+ std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
+ return e;
+ };
+
+ if (tc.exp) |exp_n| {
+ const exp: usize = 1;
+ std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
+ std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
+ return e;
+ };
+
+ std.testing.expectEqual(exp_n, matcher.nodes.items[0]) catch |e| {
+ std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
+ return e;
+ };
+
+ continue;
+ }
+
+ const exp: usize = 0;
+ std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
+ std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
+ return e;
+ };
+ }
+}
+
+test "Browser.CSS.Selector: nth pseudo class" {
+ const alloc = std.testing.allocator;
+
+ var matcher = MatcherTest.init(alloc);
+ defer matcher.deinit();
+
+ var p1: NodeTest = .{ .name = "p" };
+ var p2: NodeTest = .{ .name = "p" };
+
+ p1.sibling = &p2;
+ p2.prev = &p1;
+
+ var root: NodeTest = .{ .child = &p1, .last = &p2 };
+ p1.par = &root;
+ p2.par = &root;
+
+ const testcases = [_]struct {
+ q: []const u8,
+ n: NodeTest,
+ exp: ?*const NodeTest,
+ }{
+ .{ .q = "a:nth-of-type(1)", .n = root, .exp = null },
+ .{ .q = "p:nth-of-type(1)", .n = root, .exp = &p1 },
+ .{ .q = "p:nth-of-type(2)", .n = root, .exp = &p2 },
+ .{ .q = "p:nth-of-type(0)", .n = root, .exp = null },
+ .{ .q = "p:nth-of-type(2n)", .n = root, .exp = &p2 },
+ .{ .q = "p:nth-last-child(1)", .n = root, .exp = &p2 },
+ .{ .q = "p:nth-last-child(2)", .n = root, .exp = &p1 },
+ .{ .q = "p:nth-child(1)", .n = root, .exp = &p1 },
+ .{ .q = "p:nth-child(2)", .n = root, .exp = &p2 },
+ .{ .q = "p:nth-child(odd)", .n = root, .exp = &p1 },
+ .{ .q = "p:nth-child(even)", .n = root, .exp = &p2 },
+ .{ .q = "p:nth-child(n+2)", .n = root, .exp = &p2 },
+ };
+
+ for (testcases) |tc| {
+ matcher.reset();
+
+ const s = try css.parse(alloc, tc.q, .{});
+ defer s.deinit(alloc);
+
+ css.matchAll(&s, &tc.n, &matcher) catch |e| {
+ std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
+ return e;
+ };
+
+ if (tc.exp) |exp_n| {
+ const exp: usize = 1;
+ std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
+ std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
+ return e;
+ };
+
+ std.testing.expectEqual(exp_n, matcher.nodes.items[0]) catch |e| {
+ std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
+ return e;
+ };
+
+ continue;
+ }
+
+ const exp: usize = 0;
+ std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
+ std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
+ return e;
+ };
+ }
+}