mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 23:23:28 +00:00
Merge pull request #1036 from lightpanda-io/css-contains
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
CSS: move tests + implement :containsOwn
This commit is contained in:
@@ -19,6 +19,8 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
|
const css = @import("css.zig");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
// Node implementation with Netsurf Libdom C lib.
|
// Node implementation with Netsurf Libdom C lib.
|
||||||
pub const Node = struct {
|
pub const Node = struct {
|
||||||
@@ -79,6 +81,14 @@ pub const Node = struct {
|
|||||||
return t == .text;
|
return t == .text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn text(n: Node) !?[]const u8 {
|
||||||
|
const data = try parser.nodeTextContent(n.node);
|
||||||
|
if (data == null) return null;
|
||||||
|
if (data.?.len == 0) return null;
|
||||||
|
|
||||||
|
return std.mem.trim(u8, data.?, &std.ascii.whitespace);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn isEmptyText(n: Node) !bool {
|
pub fn isEmptyText(n: Node) !bool {
|
||||||
const data = try parser.nodeTextContent(n.node);
|
const data = try parser.nodeTextContent(n.node);
|
||||||
if (data == null) return true;
|
if (data == null) return true;
|
||||||
@@ -100,3 +110,318 @@ pub const Node = struct {
|
|||||||
return a.node == b.node;
|
return a.node == b.node;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MatcherTest = struct {
|
||||||
|
const Nodes = std.ArrayListUnmanaged(Node);
|
||||||
|
|
||||||
|
nodes: Nodes,
|
||||||
|
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: Node) !void {
|
||||||
|
try m.nodes.append(m.allocator, n);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test "Browser.CSS.Libdom: matchFirst" {
|
||||||
|
const alloc = std.testing.allocator;
|
||||||
|
|
||||||
|
try parser.init();
|
||||||
|
defer parser.deinit();
|
||||||
|
|
||||||
|
var matcher = MatcherTest.init(alloc);
|
||||||
|
defer matcher.deinit();
|
||||||
|
|
||||||
|
const testcases = [_]struct {
|
||||||
|
q: []const u8,
|
||||||
|
html: []const u8,
|
||||||
|
exp: usize,
|
||||||
|
}{
|
||||||
|
.{ .q = "address", .html = "<body><address>This address...</address></body>", .exp = 1 },
|
||||||
|
.{ .q = "*", .html = "<!-- comment --><html><head></head><body>text</body></html>", .exp = 1 },
|
||||||
|
.{ .q = "*", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||||
|
.{ .q = "#foo", .html = "<p id=\"foo\"><p id=\"bar\">", .exp = 1 },
|
||||||
|
.{ .q = "li#t1", .html = "<ul><li id=\"t1\"><p id=\"t1\">", .exp = 1 },
|
||||||
|
.{ .q = ".t3", .html = "<ul><li class=\"t1\"><li class=\"t2 t3\">", .exp = 1 },
|
||||||
|
.{ .q = "*#t4", .html = "<ol><li id=\"t4\"><li id=\"t44\">", .exp = 1 },
|
||||||
|
.{ .q = ".t1", .html = "<ul><li class=\"t1\"><li class=\"t2\">", .exp = 1 },
|
||||||
|
.{ .q = "p.t1", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
||||||
|
.{ .q = "div.teST", .html = "<div class=\"test\">", .exp = 0 },
|
||||||
|
.{ .q = ".t1.fail", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
||||||
|
.{ .q = "p.t1.t2", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
||||||
|
.{ .q = "p.--t1", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
||||||
|
.{ .q = "p.--t1.--t2", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
||||||
|
.{ .q = "p[title]", .html = "<p><p title=\"title\">", .exp = 1 },
|
||||||
|
.{ .q = "div[class=\"red\" i]", .html = "<div><div class=\"Red\">", .exp = 1 },
|
||||||
|
.{ .q = "address[title=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
|
||||||
|
.{ .q = "address[title=\"FoOIgnoRECaSe\" i]", .html = "<address><address title=\"fooIgnoreCase\"><address title=\"bar\">", .exp = 1 },
|
||||||
|
.{ .q = "address[title!=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
|
||||||
|
.{ .q = "address[title!=\"foo\" i]", .html = "<address><address title=\"FOO\"><address title=\"bar\">", .exp = 1 },
|
||||||
|
.{ .q = "p[title!=\"FooBarUFoo\" i]", .html = "<p title=\"fooBARuFOO\"><p title=\"varfoo\">", .exp = 1 },
|
||||||
|
.{ .q = "[ title ~= foo ]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
||||||
|
.{ .q = "p[title~=\"FOO\" i]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
||||||
|
.{ .q = "p[title~=toofoo i]", .html = "<p title=\"tot foo bar\">", .exp = 0 },
|
||||||
|
.{ .q = "[title~=\"hello world\"]", .html = "<p title=\"hello world\">", .exp = 0 },
|
||||||
|
.{ .q = "[title~=\"hello\" i]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
||||||
|
.{ .q = "[title~=\"hello\" I]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
||||||
|
.{ .q = "[lang|=\"en\"]", .html = "<p lang=\"en\"><p lang=\"en-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
|
||||||
|
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
|
||||||
|
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
|
||||||
|
.{ .q = "[title^=\"foo\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
||||||
|
.{ .q = "[title^=\"foo\" i]", .html = "<p title=\"FooBAR\"><p title=\"barfoo\">", .exp = 1 },
|
||||||
|
.{ .q = "[title$=\"bar\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
||||||
|
.{ .q = "[title$=\"BAR\" i]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
||||||
|
.{ .q = "[title*=\"bar\"]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
||||||
|
.{ .q = "[title*=\"BaRu\" i]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
||||||
|
.{ .q = "[title*=\"BaRu\" I]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
||||||
|
.{ .q = "p[class$=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||||
|
.{ .q = "p[class$=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||||
|
.{ .q = "p[class^=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||||
|
.{ .q = "p[class^=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||||
|
.{ .q = "p[class*=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||||
|
.{ .q = "p[class*=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||||
|
.{ .q = "input[name=Sex][value=F]", .html = "<input type=\"radio\" name=\"Sex\" value=\"F\"/>", .exp = 1 },
|
||||||
|
.{ .q = "table[border=\"0\"][cellpadding=\"0\"][cellspacing=\"0\"]", .html = "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"table-layout: fixed; width: 100%; border: 0 dashed; border-color: #FFFFFF\"><tr style=\"height:64px\">aaa</tr></table>", .exp = 1 },
|
||||||
|
.{ .q = ".t1:not(.t2)", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
||||||
|
.{ .q = "div:not(.t1)", .html = "<div class=\"t3\">", .exp = 1 },
|
||||||
|
.{ .q = "div:not([class=\"t2\"])", .html = "<div><div class=\"t2\"><div class=\"t3\">", .exp = 1 },
|
||||||
|
.{ .q = "li:nth-child(odd)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
||||||
|
.{ .q = "li:nth-child(even)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
||||||
|
.{ .q = "li:nth-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
||||||
|
.{ .q = "li:nth-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
||||||
|
.{ .q = "li:nth-last-child(odd)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
||||||
|
.{ .q = "li:nth-last-child(even)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
||||||
|
.{ .q = "li:nth-last-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
||||||
|
.{ .q = "li:nth-last-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
||||||
|
.{ .q = "span:first-child", .html = "<p>some text <span id=\"1\">and a span</span><span id=\"2\"> and another</span></p>", .exp = 1 },
|
||||||
|
.{ .q = "span:last-child", .html = "<span>a span</span> and some text", .exp = 1 },
|
||||||
|
.{ .q = "p:nth-of-type(2)", .html = "<address></address><p id=1><p id=2>", .exp = 1 },
|
||||||
|
.{ .q = "p:nth-last-of-type(2)", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
||||||
|
.{ .q = "p:last-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
||||||
|
.{ .q = "p:first-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
||||||
|
.{ .q = "p:only-child", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p></div>", .exp = 1 },
|
||||||
|
.{ .q = "p:only-of-type", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p><p id=\"3\"></p></div>", .exp = 1 },
|
||||||
|
.{ .q = ":empty", .html = "<p id=\"1\"><!-- --><p id=\"2\">Hello<p id=\"3\"><span>", .exp = 1 },
|
||||||
|
.{ .q = "div p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
|
||||||
|
.{ .q = "div table p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
|
||||||
|
.{ .q = "div > p", .html = "<div><p id=\"1\"><div><p id=\"2\"></div><table><tr><td><p id=\"3\"></table></div>", .exp = 1 },
|
||||||
|
.{ .q = "p ~ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
||||||
|
.{ .q = "p + p", .html = "<p id=\"1\"></p> <!--comment--> <p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
||||||
|
.{ .q = "li, p", .html = "<ul><li></li><li></li></ul><p>", .exp = 1 },
|
||||||
|
.{ .q = "p +/*This is a comment*/ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
||||||
|
// .{ .q = "p:contains(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
||||||
|
.{ .q = "p:containsOwn(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
|
||||||
|
.{ .q = ":containsOwn(\"inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
||||||
|
.{ .q = ":containsOwn(\"Inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
|
||||||
|
.{ .q = "p:containsOwn(\"block\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
||||||
|
// .{ .q = "div:has(#p1)", .html = "<div id=\"d1\"><p id=\"p1\"><span>text content</span></p></div><div id=\"d2\"/>", .exp = 1 },
|
||||||
|
.{ .q = "div:has(:containsOwn(\"2\"))", .html = "<div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p>contents <em>2</em></p></div>", .exp = 1 },
|
||||||
|
.{ .q = "body :has(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
|
||||||
|
.{ .q = "body :haschild(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
|
||||||
|
// .{ .q = "p:matches([\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||||
|
// .{ .q = "p:matches([a-z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||||
|
// .{ .q = "p:matches([a-zA-Z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||||
|
// .{ .q = "p:matches([^\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||||
|
// .{ .q = "p:matches(^(0|a))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||||
|
// .{ .q = "p:matches(^\\d+$)", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||||
|
// .{ .q = "p:not(:matches(^\\d+$))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||||
|
// .{ .q = "div :matchesOwn(^\\d+$)", .html = "<div><p id=\"p1\">01234<em>567</em>89</p><div>", .exp = 1 },
|
||||||
|
// .{ .q = "[href#=(fina)]:not([href#=(\\/\\/[^\\/]+untrusted)])", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"></a> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"></a> <li><a id=\"a2\" href=\"http://finance.untrusted.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"/> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
|
||||||
|
// .{ .q = "[href#=(^https:\\/\\/[^\\/]*\\/?news)]", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"/> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"></a> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
|
||||||
|
.{ .q = ":input", .html = "<form> <label>Username <input type=\"text\" name=\"username\" /></label> <label>Password <input type=\"password\" name=\"password\" /></label> <label>Country <select name=\"country\"> <option value=\"ca\">Canada</option> <option value=\"us\">United States</option> </select> </label> <label>Bio <textarea name=\"bio\"></textarea></label> <button>Sign up</button> </form>", .exp = 1 },
|
||||||
|
.{ .q = ":root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||||
|
.{ .q = "*:root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||||
|
.{ .q = "html:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||||
|
.{ .q = "*:root:first-child", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||||
|
.{ .q = "*:root:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||||
|
.{ .q = "a:not(:root)", .html = "<html><head></head><body><a href=\"http://www.foo.com\"></a></body></html>", .exp = 1 },
|
||||||
|
.{ .q = "body > *:nth-child(3n+2)", .html = "<html><head></head><body><p></p><div></div><span></span><a></a><form></form></body></html>", .exp = 1 },
|
||||||
|
.{ .q = "input:disabled", .html = "<html><head></head><body><fieldset disabled><legend id=\"1\"><input id=\"i1\"/></legend><legend id=\"2\"><input id=\"i2\"/></legend></fieldset></body></html>", .exp = 1 },
|
||||||
|
.{ .q = ":disabled", .html = "<html><head></head><body><fieldset disabled></fieldset></body></html>", .exp = 1 },
|
||||||
|
.{ .q = ":enabled", .html = "<html><head></head><body><fieldset></fieldset></body></html>", .exp = 1 },
|
||||||
|
.{ .q = "div.class1, div.class2", .html = "<div class=class1></div><div class=class2></div><div class=class3></div>", .exp = 1 },
|
||||||
|
};
|
||||||
|
|
||||||
|
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.matchFirst(&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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Browser.CSS.Libdom: matchAll" {
|
||||||
|
const alloc = std.testing.allocator;
|
||||||
|
|
||||||
|
try parser.init();
|
||||||
|
defer parser.deinit();
|
||||||
|
|
||||||
|
var matcher = MatcherTest.init(alloc);
|
||||||
|
defer matcher.deinit();
|
||||||
|
|
||||||
|
const testcases = [_]struct {
|
||||||
|
q: []const u8,
|
||||||
|
html: []const u8,
|
||||||
|
exp: usize,
|
||||||
|
}{
|
||||||
|
.{ .q = "address", .html = "<body><address>This address...</address></body>", .exp = 1 },
|
||||||
|
.{ .q = "*", .html = "<!-- comment --><html><head></head><body>text</body></html>", .exp = 3 },
|
||||||
|
.{ .q = "*", .html = "<html><head></head><body></body></html>", .exp = 3 },
|
||||||
|
.{ .q = "#foo", .html = "<p id=\"foo\"><p id=\"bar\">", .exp = 1 },
|
||||||
|
.{ .q = "li#t1", .html = "<ul><li id=\"t1\"><p id=\"t1\">", .exp = 1 },
|
||||||
|
.{ .q = ".t3", .html = "<ul><li class=\"t1\"><li class=\"t2 t3\">", .exp = 1 },
|
||||||
|
.{ .q = "*#t4", .html = "<ol><li id=\"t4\"><li id=\"t44\">", .exp = 1 },
|
||||||
|
.{ .q = ".t1", .html = "<ul><li class=\"t1\"><li class=\"t2\">", .exp = 1 },
|
||||||
|
.{ .q = "p.t1", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
||||||
|
.{ .q = "div.teST", .html = "<div class=\"test\">", .exp = 0 },
|
||||||
|
.{ .q = ".t1.fail", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
||||||
|
.{ .q = "p.t1.t2", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
||||||
|
.{ .q = "p.--t1", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
||||||
|
.{ .q = "p.--t1.--t2", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
||||||
|
.{ .q = "p[title]", .html = "<p><p title=\"title\">", .exp = 1 },
|
||||||
|
.{ .q = "div[class=\"red\" i]", .html = "<div><div class=\"Red\">", .exp = 1 },
|
||||||
|
.{ .q = "address[title=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
|
||||||
|
.{ .q = "address[title=\"FoOIgnoRECaSe\" i]", .html = "<address><address title=\"fooIgnoreCase\"><address title=\"bar\">", .exp = 1 },
|
||||||
|
.{ .q = "address[title!=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 2 },
|
||||||
|
.{ .q = "address[title!=\"foo\" i]", .html = "<address><address title=\"FOO\"><address title=\"bar\">", .exp = 2 },
|
||||||
|
.{ .q = "p[title!=\"FooBarUFoo\" i]", .html = "<p title=\"fooBARuFOO\"><p title=\"varfoo\">", .exp = 1 },
|
||||||
|
.{ .q = "[ title ~= foo ]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
||||||
|
.{ .q = "p[title~=\"FOO\" i]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
||||||
|
.{ .q = "p[title~=toofoo i]", .html = "<p title=\"tot foo bar\">", .exp = 0 },
|
||||||
|
.{ .q = "[title~=\"hello world\"]", .html = "<p title=\"hello world\">", .exp = 0 },
|
||||||
|
.{ .q = "[title~=\"hello\" i]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
||||||
|
.{ .q = "[title~=\"hello\" I]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
||||||
|
.{ .q = "[lang|=\"en\"]", .html = "<p lang=\"en\"><p lang=\"en-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
|
||||||
|
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
|
||||||
|
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
|
||||||
|
.{ .q = "[title^=\"foo\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
||||||
|
.{ .q = "[title^=\"foo\" i]", .html = "<p title=\"FooBAR\"><p title=\"barfoo\">", .exp = 1 },
|
||||||
|
.{ .q = "[title$=\"bar\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
||||||
|
.{ .q = "[title$=\"BAR\" i]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
||||||
|
.{ .q = "[title*=\"bar\"]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
||||||
|
.{ .q = "[title*=\"BaRu\" i]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
||||||
|
.{ .q = "[title*=\"BaRu\" I]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
||||||
|
.{ .q = "p[class$=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||||
|
.{ .q = "p[class$=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||||
|
.{ .q = "p[class^=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||||
|
.{ .q = "p[class^=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||||
|
.{ .q = "p[class*=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||||
|
.{ .q = "p[class*=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||||
|
.{ .q = "input[name=Sex][value=F]", .html = "<input type=\"radio\" name=\"Sex\" value=\"F\"/>", .exp = 1 },
|
||||||
|
.{ .q = "table[border=\"0\"][cellpadding=\"0\"][cellspacing=\"0\"]", .html = "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"table-layout: fixed; width: 100%; border: 0 dashed; border-color: #FFFFFF\"><tr style=\"height:64px\">aaa</tr></table>", .exp = 1 },
|
||||||
|
.{ .q = ".t1:not(.t2)", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
||||||
|
.{ .q = "div:not(.t1)", .html = "<div class=\"t3\">", .exp = 1 },
|
||||||
|
.{ .q = "div:not([class=\"t2\"])", .html = "<div><div class=\"t2\"><div class=\"t3\">", .exp = 2 },
|
||||||
|
.{ .q = "li:nth-child(odd)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 2 },
|
||||||
|
.{ .q = "li:nth-child(even)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
||||||
|
.{ .q = "li:nth-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 2 },
|
||||||
|
.{ .q = "li:nth-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
||||||
|
.{ .q = "li:nth-last-child(odd)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
||||||
|
.{ .q = "li:nth-last-child(even)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
||||||
|
.{ .q = "li:nth-last-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
||||||
|
.{ .q = "li:nth-last-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
||||||
|
.{ .q = "span:first-child", .html = "<p>some text <span id=\"1\">and a span</span><span id=\"2\"> and another</span></p>", .exp = 1 },
|
||||||
|
.{ .q = "span:last-child", .html = "<span>a span</span> and some text", .exp = 1 },
|
||||||
|
.{ .q = "p:nth-of-type(2)", .html = "<address></address><p id=1><p id=2>", .exp = 1 },
|
||||||
|
.{ .q = "p:nth-last-of-type(2)", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
||||||
|
.{ .q = "p:last-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
||||||
|
.{ .q = "p:first-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
||||||
|
.{ .q = "p:only-child", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p></div>", .exp = 1 },
|
||||||
|
.{ .q = "p:only-of-type", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p><p id=\"3\"></p></div>", .exp = 1 },
|
||||||
|
.{ .q = ":empty", .html = "<p id=\"1\"><!-- --><p id=\"2\">Hello<p id=\"3\"><span>", .exp = 3 },
|
||||||
|
.{ .q = "div p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 2 },
|
||||||
|
.{ .q = "div table p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
|
||||||
|
.{ .q = "div > p", .html = "<div><p id=\"1\"><div><p id=\"2\"></div><table><tr><td><p id=\"3\"></table></div>", .exp = 2 },
|
||||||
|
.{ .q = "p ~ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 2 },
|
||||||
|
.{ .q = "p + p", .html = "<p id=\"1\"></p> <!--comment--> <p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
||||||
|
.{ .q = "li, p", .html = "<ul><li></li><li></li></ul><p>", .exp = 3 },
|
||||||
|
.{ .q = "p +/*This is a comment*/ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
||||||
|
// .{ .q = "p:contains(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
||||||
|
.{ .q = "p:containsOwn(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
|
||||||
|
.{ .q = ":containsOwn(\"inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
||||||
|
.{ .q = ":containsOwn(\"Inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
|
||||||
|
.{ .q = "p:containsOwn(\"block\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
||||||
|
.{ .q = "div:has(#p1)", .html = "<div id=\"d1\"><p id=\"p1\"><span>text content</span></p></div><div id=\"d2\"/>", .exp = 1 },
|
||||||
|
.{ .q = "div:has(:containsOwn(\"2\"))", .html = "<div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p>contents <em>2</em></p></div>", .exp = 1 },
|
||||||
|
.{ .q = "body :has(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 2 },
|
||||||
|
.{ .q = "body :haschild(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
|
||||||
|
// .{ .q = "p:matches([\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
||||||
|
// .{ .q = "p:matches([a-z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||||
|
// .{ .q = "p:matches([a-zA-Z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
||||||
|
// .{ .q = "p:matches([^\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
||||||
|
// .{ .q = "p:matches(^(0|a))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 3 },
|
||||||
|
// .{ .q = "p:matches(^\\d+$)", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||||
|
// .{ .q = "p:not(:matches(^\\d+$))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
||||||
|
// .{ .q = "div :matchesOwn(^\\d+$)", .html = "<div><p id=\"p1\">01234<em>567</em>89</p><div>", .exp = 2 },
|
||||||
|
// .{ .q = "[href#=(fina)]:not([href#=(\\/\\/[^\\/]+untrusted)])", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"></a> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"></a> <li><a id=\"a2\" href=\"http://finance.untrusted.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"/> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 2 },
|
||||||
|
// .{ .q = "[href#=(^https:\\/\\/[^\\/]*\\/?news)]", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"/> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"></a> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
|
||||||
|
.{ .q = ":input", .html = "<form> <label>Username <input type=\"text\" name=\"username\" /></label> <label>Password <input type=\"password\" name=\"password\" /></label> <label>Country <select name=\"country\"> <option value=\"ca\">Canada</option> <option value=\"us\">United States</option> </select> </label> <label>Bio <textarea name=\"bio\"></textarea></label> <button>Sign up</button> </form>", .exp = 5 },
|
||||||
|
.{ .q = ":root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||||
|
.{ .q = "*:root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||||
|
.{ .q = "html:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||||
|
.{ .q = "*:root:first-child", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||||
|
.{ .q = "*:root:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||||
|
.{ .q = "a:not(:root)", .html = "<html><head></head><body><a href=\"http://www.foo.com\"></a></body></html>", .exp = 1 },
|
||||||
|
.{ .q = "body > *:nth-child(3n+2)", .html = "<html><head></head><body><p></p><div></div><span></span><a></a><form></form></body></html>", .exp = 2 },
|
||||||
|
.{ .q = "input:disabled", .html = "<html><head></head><body><fieldset disabled><legend id=\"1\"><input id=\"i1\"/></legend><legend id=\"2\"><input id=\"i2\"/></legend></fieldset></body></html>", .exp = 1 },
|
||||||
|
.{ .q = ":disabled", .html = "<html><head></head><body><fieldset disabled></fieldset></body></html>", .exp = 1 },
|
||||||
|
.{ .q = ":enabled", .html = "<html><head></head><body><fieldset></fieldset></body></html>", .exp = 1 },
|
||||||
|
.{ .q = "div.class1, div.class2", .html = "<div class=class1></div><div class=class2></div><div class=class3></div>", .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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,330 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// 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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const css = @import("css.zig");
|
|
||||||
const Node = @import("libdom.zig").Node;
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
const Matcher = struct {
|
|
||||||
const Nodes = std.ArrayListUnmanaged(Node);
|
|
||||||
|
|
||||||
nodes: Nodes,
|
|
||||||
allocator: Allocator,
|
|
||||||
|
|
||||||
fn init(allocator: Allocator) Matcher {
|
|
||||||
return .{
|
|
||||||
.nodes = .empty,
|
|
||||||
.allocator = allocator,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deinit(m: *Matcher) void {
|
|
||||||
m.nodes.deinit(m.allocator);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reset(m: *Matcher) void {
|
|
||||||
m.nodes.clearRetainingCapacity();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn match(m: *Matcher, n: Node) !void {
|
|
||||||
try m.nodes.append(m.allocator, n);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
test "matchFirst" {
|
|
||||||
const alloc = std.testing.allocator;
|
|
||||||
|
|
||||||
var matcher = Matcher.init(alloc);
|
|
||||||
defer matcher.deinit();
|
|
||||||
|
|
||||||
const testcases = [_]struct {
|
|
||||||
q: []const u8,
|
|
||||||
html: []const u8,
|
|
||||||
exp: usize,
|
|
||||||
}{
|
|
||||||
.{ .q = "address", .html = "<body><address>This address...</address></body>", .exp = 1 },
|
|
||||||
.{ .q = "*", .html = "<!-- comment --><html><head></head><body>text</body></html>", .exp = 1 },
|
|
||||||
.{ .q = "*", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "#foo", .html = "<p id=\"foo\"><p id=\"bar\">", .exp = 1 },
|
|
||||||
.{ .q = "li#t1", .html = "<ul><li id=\"t1\"><p id=\"t1\">", .exp = 1 },
|
|
||||||
.{ .q = ".t3", .html = "<ul><li class=\"t1\"><li class=\"t2 t3\">", .exp = 1 },
|
|
||||||
.{ .q = "*#t4", .html = "<ol><li id=\"t4\"><li id=\"t44\">", .exp = 1 },
|
|
||||||
.{ .q = ".t1", .html = "<ul><li class=\"t1\"><li class=\"t2\">", .exp = 1 },
|
|
||||||
.{ .q = "p.t1", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
|
||||||
.{ .q = "div.teST", .html = "<div class=\"test\">", .exp = 0 },
|
|
||||||
.{ .q = ".t1.fail", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
|
||||||
.{ .q = "p.t1.t2", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
|
||||||
.{ .q = "p.--t1", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
|
||||||
.{ .q = "p.--t1.--t2", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
|
||||||
.{ .q = "p[title]", .html = "<p><p title=\"title\">", .exp = 1 },
|
|
||||||
.{ .q = "div[class=\"red\" i]", .html = "<div><div class=\"Red\">", .exp = 1 },
|
|
||||||
.{ .q = "address[title=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
|
|
||||||
.{ .q = "address[title=\"FoOIgnoRECaSe\" i]", .html = "<address><address title=\"fooIgnoreCase\"><address title=\"bar\">", .exp = 1 },
|
|
||||||
.{ .q = "address[title!=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
|
|
||||||
.{ .q = "address[title!=\"foo\" i]", .html = "<address><address title=\"FOO\"><address title=\"bar\">", .exp = 1 },
|
|
||||||
.{ .q = "p[title!=\"FooBarUFoo\" i]", .html = "<p title=\"fooBARuFOO\"><p title=\"varfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[ title ~= foo ]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
|
||||||
.{ .q = "p[title~=\"FOO\" i]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
|
||||||
.{ .q = "p[title~=toofoo i]", .html = "<p title=\"tot foo bar\">", .exp = 0 },
|
|
||||||
.{ .q = "[title~=\"hello world\"]", .html = "<p title=\"hello world\">", .exp = 0 },
|
|
||||||
.{ .q = "[title~=\"hello\" i]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
|
||||||
.{ .q = "[title~=\"hello\" I]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
|
||||||
.{ .q = "[lang|=\"en\"]", .html = "<p lang=\"en\"><p lang=\"en-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
|
|
||||||
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
|
|
||||||
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
|
|
||||||
.{ .q = "[title^=\"foo\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title^=\"foo\" i]", .html = "<p title=\"FooBAR\"><p title=\"barfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title$=\"bar\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title$=\"BAR\" i]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title*=\"bar\"]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title*=\"BaRu\" i]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title*=\"BaRu\" I]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
|
||||||
.{ .q = "p[class$=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class$=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class^=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class^=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class*=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class*=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "input[name=Sex][value=F]", .html = "<input type=\"radio\" name=\"Sex\" value=\"F\"/>", .exp = 1 },
|
|
||||||
.{ .q = "table[border=\"0\"][cellpadding=\"0\"][cellspacing=\"0\"]", .html = "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"table-layout: fixed; width: 100%; border: 0 dashed; border-color: #FFFFFF\"><tr style=\"height:64px\">aaa</tr></table>", .exp = 1 },
|
|
||||||
.{ .q = ".t1:not(.t2)", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
|
||||||
.{ .q = "div:not(.t1)", .html = "<div class=\"t3\">", .exp = 1 },
|
|
||||||
.{ .q = "div:not([class=\"t2\"])", .html = "<div><div class=\"t2\"><div class=\"t3\">", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-child(odd)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-child(even)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-last-child(odd)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-last-child(even)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-last-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-last-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
|
||||||
.{ .q = "span:first-child", .html = "<p>some text <span id=\"1\">and a span</span><span id=\"2\"> and another</span></p>", .exp = 1 },
|
|
||||||
.{ .q = "span:last-child", .html = "<span>a span</span> and some text", .exp = 1 },
|
|
||||||
.{ .q = "p:nth-of-type(2)", .html = "<address></address><p id=1><p id=2>", .exp = 1 },
|
|
||||||
.{ .q = "p:nth-last-of-type(2)", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
|
||||||
.{ .q = "p:last-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
|
||||||
.{ .q = "p:first-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
|
||||||
.{ .q = "p:only-child", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p></div>", .exp = 1 },
|
|
||||||
.{ .q = "p:only-of-type", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p><p id=\"3\"></p></div>", .exp = 1 },
|
|
||||||
.{ .q = ":empty", .html = "<p id=\"1\"><!-- --><p id=\"2\">Hello<p id=\"3\"><span>", .exp = 1 },
|
|
||||||
.{ .q = "div p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
|
|
||||||
.{ .q = "div table p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
|
|
||||||
.{ .q = "div > p", .html = "<div><p id=\"1\"><div><p id=\"2\"></div><table><tr><td><p id=\"3\"></table></div>", .exp = 1 },
|
|
||||||
.{ .q = "p ~ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
|
||||||
.{ .q = "p + p", .html = "<p id=\"1\"></p> <!--comment--> <p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
|
||||||
.{ .q = "li, p", .html = "<ul><li></li><li></li></ul><p>", .exp = 1 },
|
|
||||||
.{ .q = "p +/*This is a comment*/ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
|
||||||
// .{ .q = "p:contains(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:containsOwn(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
|
|
||||||
// .{ .q = ":containsOwn(\"inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:containsOwn(\"block\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
|
||||||
// .{ .q = "div:has(#p1)", .html = "<div id=\"d1\"><p id=\"p1\"><span>text content</span></p></div><div id=\"d2\"/>", .exp = 1 },
|
|
||||||
// .{ .q = "div:has(:containsOwn(\"2\"))", .html = "<div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p>contents <em>2</em></p></div>", .exp = 1 },
|
|
||||||
// .{ .q = "body :has(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
|
|
||||||
// .{ .q = "body :haschild(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
|
|
||||||
// .{ .q = "p:matches([\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:matches([a-z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:matches([a-zA-Z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:matches([^\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:matches(^(0|a))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:matches(^\\d+$)", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:not(:matches(^\\d+$))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "div :matchesOwn(^\\d+$)", .html = "<div><p id=\"p1\">01234<em>567</em>89</p><div>", .exp = 1 },
|
|
||||||
// .{ .q = "[href#=(fina)]:not([href#=(\\/\\/[^\\/]+untrusted)])", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"></a> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"></a> <li><a id=\"a2\" href=\"http://finance.untrusted.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"/> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
|
|
||||||
// .{ .q = "[href#=(^https:\\/\\/[^\\/]*\\/?news)]", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"/> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"></a> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
|
|
||||||
.{ .q = ":input", .html = "<form> <label>Username <input type=\"text\" name=\"username\" /></label> <label>Password <input type=\"password\" name=\"password\" /></label> <label>Country <select name=\"country\"> <option value=\"ca\">Canada</option> <option value=\"us\">United States</option> </select> </label> <label>Bio <textarea name=\"bio\"></textarea></label> <button>Sign up</button> </form>", .exp = 1 },
|
|
||||||
.{ .q = ":root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "*:root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "html:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "*:root:first-child", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "*:root:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "a:not(:root)", .html = "<html><head></head><body><a href=\"http://www.foo.com\"></a></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "body > *:nth-child(3n+2)", .html = "<html><head></head><body><p></p><div></div><span></span><a></a><form></form></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "input:disabled", .html = "<html><head></head><body><fieldset disabled><legend id=\"1\"><input id=\"i1\"/></legend><legend id=\"2\"><input id=\"i2\"/></legend></fieldset></body></html>", .exp = 1 },
|
|
||||||
.{ .q = ":disabled", .html = "<html><head></head><body><fieldset disabled></fieldset></body></html>", .exp = 1 },
|
|
||||||
.{ .q = ":enabled", .html = "<html><head></head><body><fieldset></fieldset></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "div.class1, div.class2", .html = "<div class=class1></div><div class=class2></div><div class=class3></div>", .exp = 1 },
|
|
||||||
};
|
|
||||||
|
|
||||||
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.matchFirst(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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test "matchAll" {
|
|
||||||
const alloc = std.testing.allocator;
|
|
||||||
|
|
||||||
var matcher = Matcher.init(alloc);
|
|
||||||
defer matcher.deinit();
|
|
||||||
|
|
||||||
const testcases = [_]struct {
|
|
||||||
q: []const u8,
|
|
||||||
html: []const u8,
|
|
||||||
exp: usize,
|
|
||||||
}{
|
|
||||||
.{ .q = "address", .html = "<body><address>This address...</address></body>", .exp = 1 },
|
|
||||||
.{ .q = "*", .html = "<!-- comment --><html><head></head><body>text</body></html>", .exp = 3 },
|
|
||||||
.{ .q = "*", .html = "<html><head></head><body></body></html>", .exp = 3 },
|
|
||||||
.{ .q = "#foo", .html = "<p id=\"foo\"><p id=\"bar\">", .exp = 1 },
|
|
||||||
.{ .q = "li#t1", .html = "<ul><li id=\"t1\"><p id=\"t1\">", .exp = 1 },
|
|
||||||
.{ .q = ".t3", .html = "<ul><li class=\"t1\"><li class=\"t2 t3\">", .exp = 1 },
|
|
||||||
.{ .q = "*#t4", .html = "<ol><li id=\"t4\"><li id=\"t44\">", .exp = 1 },
|
|
||||||
.{ .q = ".t1", .html = "<ul><li class=\"t1\"><li class=\"t2\">", .exp = 1 },
|
|
||||||
.{ .q = "p.t1", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
|
||||||
.{ .q = "div.teST", .html = "<div class=\"test\">", .exp = 0 },
|
|
||||||
.{ .q = ".t1.fail", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
|
||||||
.{ .q = "p.t1.t2", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
|
||||||
.{ .q = "p.--t1", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
|
||||||
.{ .q = "p.--t1.--t2", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
|
||||||
.{ .q = "p[title]", .html = "<p><p title=\"title\">", .exp = 1 },
|
|
||||||
.{ .q = "div[class=\"red\" i]", .html = "<div><div class=\"Red\">", .exp = 1 },
|
|
||||||
.{ .q = "address[title=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
|
|
||||||
.{ .q = "address[title=\"FoOIgnoRECaSe\" i]", .html = "<address><address title=\"fooIgnoreCase\"><address title=\"bar\">", .exp = 1 },
|
|
||||||
.{ .q = "address[title!=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 2 },
|
|
||||||
.{ .q = "address[title!=\"foo\" i]", .html = "<address><address title=\"FOO\"><address title=\"bar\">", .exp = 2 },
|
|
||||||
.{ .q = "p[title!=\"FooBarUFoo\" i]", .html = "<p title=\"fooBARuFOO\"><p title=\"varfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[ title ~= foo ]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
|
||||||
.{ .q = "p[title~=\"FOO\" i]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
|
||||||
.{ .q = "p[title~=toofoo i]", .html = "<p title=\"tot foo bar\">", .exp = 0 },
|
|
||||||
.{ .q = "[title~=\"hello world\"]", .html = "<p title=\"hello world\">", .exp = 0 },
|
|
||||||
.{ .q = "[title~=\"hello\" i]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
|
||||||
.{ .q = "[title~=\"hello\" I]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
|
||||||
.{ .q = "[lang|=\"en\"]", .html = "<p lang=\"en\"><p lang=\"en-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
|
|
||||||
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
|
|
||||||
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
|
|
||||||
.{ .q = "[title^=\"foo\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title^=\"foo\" i]", .html = "<p title=\"FooBAR\"><p title=\"barfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title$=\"bar\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title$=\"BAR\" i]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title*=\"bar\"]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title*=\"BaRu\" i]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title*=\"BaRu\" I]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
|
||||||
.{ .q = "p[class$=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class$=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class^=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class^=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class*=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class*=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "input[name=Sex][value=F]", .html = "<input type=\"radio\" name=\"Sex\" value=\"F\"/>", .exp = 1 },
|
|
||||||
.{ .q = "table[border=\"0\"][cellpadding=\"0\"][cellspacing=\"0\"]", .html = "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"table-layout: fixed; width: 100%; border: 0 dashed; border-color: #FFFFFF\"><tr style=\"height:64px\">aaa</tr></table>", .exp = 1 },
|
|
||||||
.{ .q = ".t1:not(.t2)", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
|
||||||
.{ .q = "div:not(.t1)", .html = "<div class=\"t3\">", .exp = 1 },
|
|
||||||
.{ .q = "div:not([class=\"t2\"])", .html = "<div><div class=\"t2\"><div class=\"t3\">", .exp = 2 },
|
|
||||||
.{ .q = "li:nth-child(odd)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 2 },
|
|
||||||
.{ .q = "li:nth-child(even)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 2 },
|
|
||||||
.{ .q = "li:nth-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-last-child(odd)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
|
||||||
.{ .q = "li:nth-last-child(even)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
|
||||||
.{ .q = "li:nth-last-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
|
||||||
.{ .q = "li:nth-last-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
|
||||||
.{ .q = "span:first-child", .html = "<p>some text <span id=\"1\">and a span</span><span id=\"2\"> and another</span></p>", .exp = 1 },
|
|
||||||
.{ .q = "span:last-child", .html = "<span>a span</span> and some text", .exp = 1 },
|
|
||||||
.{ .q = "p:nth-of-type(2)", .html = "<address></address><p id=1><p id=2>", .exp = 1 },
|
|
||||||
.{ .q = "p:nth-last-of-type(2)", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
|
||||||
.{ .q = "p:last-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
|
||||||
.{ .q = "p:first-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
|
||||||
.{ .q = "p:only-child", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p></div>", .exp = 1 },
|
|
||||||
.{ .q = "p:only-of-type", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p><p id=\"3\"></p></div>", .exp = 1 },
|
|
||||||
.{ .q = ":empty", .html = "<p id=\"1\"><!-- --><p id=\"2\">Hello<p id=\"3\"><span>", .exp = 3 },
|
|
||||||
.{ .q = "div p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 2 },
|
|
||||||
.{ .q = "div table p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
|
|
||||||
.{ .q = "div > p", .html = "<div><p id=\"1\"><div><p id=\"2\"></div><table><tr><td><p id=\"3\"></table></div>", .exp = 2 },
|
|
||||||
.{ .q = "p ~ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 2 },
|
|
||||||
.{ .q = "p + p", .html = "<p id=\"1\"></p> <!--comment--> <p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
|
||||||
.{ .q = "li, p", .html = "<ul><li></li><li></li></ul><p>", .exp = 3 },
|
|
||||||
.{ .q = "p +/*This is a comment*/ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
|
||||||
// .{ .q = "p:contains(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:containsOwn(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
|
|
||||||
// .{ .q = ":containsOwn(\"inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:containsOwn(\"block\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
|
||||||
.{ .q = "div:has(#p1)", .html = "<div id=\"d1\"><p id=\"p1\"><span>text content</span></p></div><div id=\"d2\"/>", .exp = 1 },
|
|
||||||
// .{ .q = "div:has(:containsOwn(\"2\"))", .html = "<div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p>contents <em>2</em></p></div>", .exp = 1 },
|
|
||||||
// .{ .q = "body :has(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 2 },
|
|
||||||
// .{ .q = "body :haschild(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
|
|
||||||
// .{ .q = "p:matches([\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
|
||||||
// .{ .q = "p:matches([a-z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:matches([a-zA-Z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
|
||||||
// .{ .q = "p:matches([^\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
|
||||||
// .{ .q = "p:matches(^(0|a))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 3 },
|
|
||||||
// .{ .q = "p:matches(^\\d+$)", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:not(:matches(^\\d+$))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
|
||||||
// .{ .q = "div :matchesOwn(^\\d+$)", .html = "<div><p id=\"p1\">01234<em>567</em>89</p><div>", .exp = 2 },
|
|
||||||
// .{ .q = "[href#=(fina)]:not([href#=(\\/\\/[^\\/]+untrusted)])", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"></a> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"></a> <li><a id=\"a2\" href=\"http://finance.untrusted.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"/> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 2 },
|
|
||||||
// .{ .q = "[href#=(^https:\\/\\/[^\\/]*\\/?news)]", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"/> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"></a> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
|
|
||||||
.{ .q = ":input", .html = "<form> <label>Username <input type=\"text\" name=\"username\" /></label> <label>Password <input type=\"password\" name=\"password\" /></label> <label>Country <select name=\"country\"> <option value=\"ca\">Canada</option> <option value=\"us\">United States</option> </select> </label> <label>Bio <textarea name=\"bio\"></textarea></label> <button>Sign up</button> </form>", .exp = 5 },
|
|
||||||
.{ .q = ":root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "*:root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "html:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "*:root:first-child", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "*:root:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "a:not(:root)", .html = "<html><head></head><body><a href=\"http://www.foo.com\"></a></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "body > *:nth-child(3n+2)", .html = "<html><head></head><body><p></p><div></div><span></span><a></a><form></form></body></html>", .exp = 2 },
|
|
||||||
.{ .q = "input:disabled", .html = "<html><head></head><body><fieldset disabled><legend id=\"1\"><input id=\"i1\"/></legend><legend id=\"2\"><input id=\"i2\"/></legend></fieldset></body></html>", .exp = 1 },
|
|
||||||
.{ .q = ":disabled", .html = "<html><head></head><body><fieldset disabled></fieldset></body></html>", .exp = 1 },
|
|
||||||
.{ .q = ":enabled", .html = "<html><head></head><body><fieldset></fieldset></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "div.class1, div.class2", .html = "<div class=class1></div><div class=class2></div><div class=class3></div>", .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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,591 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// 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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -557,8 +557,6 @@ pub const Parser = struct {
|
|||||||
const val = try buf.toOwnedSlice(allocator);
|
const val = try buf.toOwnedSlice(allocator);
|
||||||
errdefer allocator.free(val);
|
errdefer allocator.free(val);
|
||||||
|
|
||||||
lowerstr(val);
|
|
||||||
|
|
||||||
return .{ .pseudo_class_contains = .{ .own = pseudo_class == .containsown, .val = val } };
|
return .{ .pseudo_class_contains = .{ .own = pseudo_class == .containsown, .val = val } };
|
||||||
},
|
},
|
||||||
.matches, .matchesown => {
|
.matches, .matchesown => {
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const css = @import("css.zig");
|
||||||
|
|
||||||
pub const AttributeOP = enum {
|
pub const AttributeOP = enum {
|
||||||
eql, // =
|
eql, // =
|
||||||
@@ -432,7 +434,25 @@ pub const Selector = union(enum) {
|
|||||||
else => Error.UnsupportedRelativePseudoClass,
|
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_regexp => return Error.UnsupportedRegexpPseudoClass, // TODO need mem allocation.
|
||||||
.pseudo_class_nth => |v| {
|
.pseudo_class_nth => |v| {
|
||||||
if (v.a == 0) {
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user