mirror of
				https://github.com/lightpanda-io/browser.git
				synced 2025-10-29 15:13: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 parser = @import("../netsurf.zig"); | ||||
| const css = @import("css.zig"); | ||||
| const Allocator = std.mem.Allocator; | ||||
|  | ||||
| // Node implementation with Netsurf Libdom C lib. | ||||
| pub const Node = struct { | ||||
| @@ -79,6 +81,14 @@ pub const Node = struct { | ||||
|         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 { | ||||
|         const data = try parser.nodeTextContent(n.node); | ||||
|         if (data == null) return true; | ||||
| @@ -100,3 +110,318 @@ pub const Node = struct { | ||||
|         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); | ||||
|                 errdefer allocator.free(val); | ||||
|  | ||||
|                 lowerstr(val); | ||||
|  | ||||
|                 return .{ .pseudo_class_contains = .{ .own = pseudo_class == .containsown, .val = val } }; | ||||
|             }, | ||||
|             .matches, .matchesown => { | ||||
|   | ||||
| @@ -17,6 +17,8 @@ | ||||
| // along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
|  | ||||
| const std = @import("std"); | ||||
| const Allocator = std.mem.Allocator; | ||||
| const css = @import("css.zig"); | ||||
|  | ||||
| pub const AttributeOP = enum { | ||||
|     eql, // = | ||||
| @@ -432,7 +434,25 @@ pub const Selector = union(enum) { | ||||
|                     else => Error.UnsupportedRelativePseudoClass, | ||||
|                 }; | ||||
|             }, | ||||
|             .pseudo_class_contains => return Error.UnsupportedContainsPseudoClass, // TODO, need mem allocation. | ||||
|             .pseudo_class_contains => |v| { | ||||
|                 // Only containsOwn is implemented. | ||||
|                 if (v.own == false) return Error.UnsupportedContainsPseudoClass; | ||||
|  | ||||
|                 var c = try n.firstChild(); | ||||
|                 while (c != null) { | ||||
|                     if (c.?.isText()) { | ||||
|                         const text = try c.?.text(); | ||||
|                         if (text) |_text| { | ||||
|                             if (contains(_text, v.val, false)) { // we are case sensitive. Is this correct behavior? | ||||
|                                 return true; | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     c = try c.?.nextSibling(); | ||||
|                 } | ||||
|                 return false; | ||||
|             }, | ||||
|             .pseudo_class_regexp => return Error.UnsupportedRegexpPseudoClass, // TODO need mem allocation. | ||||
|             .pseudo_class_nth => |v| { | ||||
|                 if (v.a == 0) { | ||||
| @@ -827,3 +847,578 @@ pub const Selector = union(enum) { | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|  | ||||
| // NodeTest mock implementation for test only. | ||||
| pub const NodeTest = struct { | ||||
|     child: ?*const NodeTest = null, | ||||
|     last: ?*const NodeTest = null, | ||||
|     sibling: ?*const NodeTest = null, | ||||
|     prev: ?*const NodeTest = null, | ||||
|     par: ?*const NodeTest = null, | ||||
|  | ||||
|     name: []const u8 = "", | ||||
|     att: ?[]const u8 = null, | ||||
|  | ||||
|     pub fn firstChild(n: *const NodeTest) !?*const NodeTest { | ||||
|         return n.child; | ||||
|     } | ||||
|  | ||||
|     pub fn lastChild(n: *const NodeTest) !?*const NodeTest { | ||||
|         return n.last; | ||||
|     } | ||||
|  | ||||
|     pub fn nextSibling(n: *const NodeTest) !?*const NodeTest { | ||||
|         return n.sibling; | ||||
|     } | ||||
|  | ||||
|     pub fn prevSibling(n: *const NodeTest) !?*const NodeTest { | ||||
|         return n.prev; | ||||
|     } | ||||
|  | ||||
|     pub fn parent(n: *const NodeTest) !?*const NodeTest { | ||||
|         return n.par; | ||||
|     } | ||||
|  | ||||
|     pub fn isElement(_: *const NodeTest) bool { | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     pub fn isDocument(_: *const NodeTest) bool { | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     pub fn isComment(_: *const NodeTest) bool { | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     pub fn text(_: *const NodeTest) !?[]const u8 { | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     pub fn isText(_: *const NodeTest) bool { | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     pub fn isEmptyText(_: *const NodeTest) !bool { | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     pub fn tag(n: *const NodeTest) ![]const u8 { | ||||
|         return n.name; | ||||
|     } | ||||
|  | ||||
|     pub fn attr(n: *const NodeTest, _: []const u8) !?[]const u8 { | ||||
|         return n.att; | ||||
|     } | ||||
|  | ||||
|     pub fn eql(a: *const NodeTest, b: *const NodeTest) bool { | ||||
|         return a == b; | ||||
|     } | ||||
| }; | ||||
|  | ||||
| const MatcherTest = struct { | ||||
|     const NodeTests = std.ArrayListUnmanaged(*const NodeTest); | ||||
|  | ||||
|     nodes: NodeTests, | ||||
|     allocator: Allocator, | ||||
|  | ||||
|     fn init(allocator: Allocator) MatcherTest { | ||||
|         return .{ | ||||
|             .nodes = .empty, | ||||
|             .allocator = allocator, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     fn deinit(m: *MatcherTest) void { | ||||
|         m.nodes.deinit(m.allocator); | ||||
|     } | ||||
|  | ||||
|     fn reset(m: *MatcherTest) void { | ||||
|         m.nodes.clearRetainingCapacity(); | ||||
|     } | ||||
|  | ||||
|     pub fn match(m: *MatcherTest, n: *const NodeTest) !void { | ||||
|         try m.nodes.append(m.allocator, n); | ||||
|     } | ||||
| }; | ||||
|  | ||||
| test "Browser.CSS.Selector: matchFirst" { | ||||
|     const alloc = std.testing.allocator; | ||||
|  | ||||
|     var matcher = MatcherTest.init(alloc); | ||||
|     defer matcher.deinit(); | ||||
|  | ||||
|     const testcases = [_]struct { | ||||
|         q: []const u8, | ||||
|         n: NodeTest, | ||||
|         exp: usize, | ||||
|     }{ | ||||
|         .{ | ||||
|             .q = "address", | ||||
|             .n = .{ .child = &.{ .name = "body", .child = &.{ .name = "address" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "#foo", | ||||
|             .n = .{ .child = &.{ .name = "p", .att = "foo", .child = &.{ .name = "p" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = ".t1", | ||||
|             .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "t1" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = ".t1", | ||||
|             .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "foo t1" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "[foo]", | ||||
|             .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p" } } }, | ||||
|             .exp = 0, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "[foo]", | ||||
|             .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "[foo=bar]", | ||||
|             .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "[foo=baz]", | ||||
|             .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } }, | ||||
|             .exp = 0, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "[foo!=bar]", | ||||
|             .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "[foo!=baz]", | ||||
|             .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "[foo~=bar]", | ||||
|             .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "baz bar" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "[foo~=bar]", | ||||
|             .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } }, | ||||
|             .exp = 0, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "[foo^=bar]", | ||||
|             .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "[foo$=baz]", | ||||
|             .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "[foo*=rb]", | ||||
|             .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "[foo|=bar]", | ||||
|             .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "[foo|=bar]", | ||||
|             .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar-baz" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "[foo|=bar]", | ||||
|             .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "ba" } } }, | ||||
|             .exp = 0, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "strong, a", | ||||
|             .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "p a", | ||||
|             .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .name = "p" } }, .sibling = &.{ .name = "a" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "p a", | ||||
|             .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "span", .child = &.{ | ||||
|                 .name = "a", | ||||
|                 .par = &.{ .name = "span", .par = &.{ .name = "p" } }, | ||||
|             } } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = ":not(p)", | ||||
|             .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "p:has(a)", | ||||
|             .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "p:has(strong)", | ||||
|             .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } }, | ||||
|             .exp = 0, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "p:haschild(a)", | ||||
|             .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "p:haschild(strong)", | ||||
|             .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } }, | ||||
|             .exp = 0, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "p:lang(en)", | ||||
|             .n = .{ .child = &.{ .name = "p", .att = "en-US", .child = &.{ .name = "a" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "a:lang(en)", | ||||
|             .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .att = "en-US" } } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|     }; | ||||
|  | ||||
|     for (testcases) |tc| { | ||||
|         matcher.reset(); | ||||
|  | ||||
|         const s = try css.parse(alloc, tc.q, .{}); | ||||
|         defer s.deinit(alloc); | ||||
|  | ||||
|         _ = css.matchFirst(&s, &tc.n, &matcher) catch |e| { | ||||
|             std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s }); | ||||
|             return e; | ||||
|         }; | ||||
|  | ||||
|         std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| { | ||||
|             std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s }); | ||||
|             return e; | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|  | ||||
| test "Browser.CSS.Selector: matchAll" { | ||||
|     const alloc = std.testing.allocator; | ||||
|  | ||||
|     var matcher = MatcherTest.init(alloc); | ||||
|     defer matcher.deinit(); | ||||
|  | ||||
|     const testcases = [_]struct { | ||||
|         q: []const u8, | ||||
|         n: NodeTest, | ||||
|         exp: usize, | ||||
|     }{ | ||||
|         .{ | ||||
|             .q = "address", | ||||
|             .n = .{ .child = &.{ .name = "body", .child = &.{ .name = "address" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "#foo", | ||||
|             .n = .{ .child = &.{ .name = "p", .att = "foo", .child = &.{ .name = "p" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = ".t1", | ||||
|             .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "t1" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = ".t1", | ||||
|             .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "foo t1" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "[foo]", | ||||
|             .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p" } } }, | ||||
|             .exp = 0, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "[foo]", | ||||
|             .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "[foo=bar]", | ||||
|             .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "[foo=baz]", | ||||
|             .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } }, | ||||
|             .exp = 0, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "[foo!=bar]", | ||||
|             .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "[foo!=baz]", | ||||
|             .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } }, | ||||
|             .exp = 2, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "[foo~=bar]", | ||||
|             .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "baz bar" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "[foo~=bar]", | ||||
|             .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } }, | ||||
|             .exp = 0, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "[foo^=bar]", | ||||
|             .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "[foo$=baz]", | ||||
|             .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "[foo*=rb]", | ||||
|             .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "[foo|=bar]", | ||||
|             .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "[foo|=bar]", | ||||
|             .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar-baz" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "[foo|=bar]", | ||||
|             .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "ba" } } }, | ||||
|             .exp = 0, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "strong, a", | ||||
|             .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } }, | ||||
|             .exp = 2, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "p a", | ||||
|             .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .name = "p" } }, .sibling = &.{ .name = "a" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "p a", | ||||
|             .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "span", .child = &.{ | ||||
|                 .name = "a", | ||||
|                 .par = &.{ .name = "span", .par = &.{ .name = "p" } }, | ||||
|             } } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = ":not(p)", | ||||
|             .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } }, | ||||
|             .exp = 2, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "p:has(a)", | ||||
|             .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "p:has(strong)", | ||||
|             .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } }, | ||||
|             .exp = 0, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "p:haschild(a)", | ||||
|             .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "p:haschild(strong)", | ||||
|             .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } }, | ||||
|             .exp = 0, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "p:lang(en)", | ||||
|             .n = .{ .child = &.{ .name = "p", .att = "en-US", .child = &.{ .name = "a" } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|         .{ | ||||
|             .q = "a:lang(en)", | ||||
|             .n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .att = "en-US" } } } }, | ||||
|             .exp = 1, | ||||
|         }, | ||||
|     }; | ||||
|  | ||||
|     for (testcases) |tc| { | ||||
|         matcher.reset(); | ||||
|  | ||||
|         const s = try css.parse(alloc, tc.q, .{}); | ||||
|         defer s.deinit(alloc); | ||||
|  | ||||
|         css.matchAll(&s, &tc.n, &matcher) catch |e| { | ||||
|             std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s }); | ||||
|             return e; | ||||
|         }; | ||||
|  | ||||
|         std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| { | ||||
|             std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s }); | ||||
|             return e; | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|  | ||||
| test "Browser.CSS.Selector: pseudo class" { | ||||
|     const alloc = std.testing.allocator; | ||||
|  | ||||
|     var matcher = MatcherTest.init(alloc); | ||||
|     defer matcher.deinit(); | ||||
|  | ||||
|     var p1: NodeTest = .{ .name = "p" }; | ||||
|     var p2: NodeTest = .{ .name = "p" }; | ||||
|     var a1: NodeTest = .{ .name = "a" }; | ||||
|  | ||||
|     p1.sibling = &p2; | ||||
|     p2.prev = &p1; | ||||
|  | ||||
|     p2.sibling = &a1; | ||||
|     a1.prev = &p2; | ||||
|  | ||||
|     var root: NodeTest = .{ .child = &p1, .last = &a1 }; | ||||
|     p1.par = &root; | ||||
|     p2.par = &root; | ||||
|     a1.par = &root; | ||||
|  | ||||
|     const testcases = [_]struct { | ||||
|         q: []const u8, | ||||
|         n: NodeTest, | ||||
|         exp: ?*const NodeTest, | ||||
|     }{ | ||||
|         .{ .q = "p:only-child", .n = root, .exp = null }, | ||||
|         .{ .q = "a:only-of-type", .n = root, .exp = &a1 }, | ||||
|     }; | ||||
|  | ||||
|     for (testcases) |tc| { | ||||
|         matcher.reset(); | ||||
|  | ||||
|         const s = try css.parse(alloc, tc.q, .{}); | ||||
|         defer s.deinit(alloc); | ||||
|  | ||||
|         css.matchAll(&s, &tc.n, &matcher) catch |e| { | ||||
|             std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s }); | ||||
|             return e; | ||||
|         }; | ||||
|  | ||||
|         if (tc.exp) |exp_n| { | ||||
|             const exp: usize = 1; | ||||
|             std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| { | ||||
|                 std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s }); | ||||
|                 return e; | ||||
|             }; | ||||
|  | ||||
|             std.testing.expectEqual(exp_n, matcher.nodes.items[0]) catch |e| { | ||||
|                 std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s }); | ||||
|                 return e; | ||||
|             }; | ||||
|  | ||||
|             continue; | ||||
|         } | ||||
|  | ||||
|         const exp: usize = 0; | ||||
|         std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| { | ||||
|             std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s }); | ||||
|             return e; | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|  | ||||
| test "Browser.CSS.Selector: nth pseudo class" { | ||||
|     const alloc = std.testing.allocator; | ||||
|  | ||||
|     var matcher = MatcherTest.init(alloc); | ||||
|     defer matcher.deinit(); | ||||
|  | ||||
|     var p1: NodeTest = .{ .name = "p" }; | ||||
|     var p2: NodeTest = .{ .name = "p" }; | ||||
|  | ||||
|     p1.sibling = &p2; | ||||
|     p2.prev = &p1; | ||||
|  | ||||
|     var root: NodeTest = .{ .child = &p1, .last = &p2 }; | ||||
|     p1.par = &root; | ||||
|     p2.par = &root; | ||||
|  | ||||
|     const testcases = [_]struct { | ||||
|         q: []const u8, | ||||
|         n: NodeTest, | ||||
|         exp: ?*const NodeTest, | ||||
|     }{ | ||||
|         .{ .q = "a:nth-of-type(1)", .n = root, .exp = null }, | ||||
|         .{ .q = "p:nth-of-type(1)", .n = root, .exp = &p1 }, | ||||
|         .{ .q = "p:nth-of-type(2)", .n = root, .exp = &p2 }, | ||||
|         .{ .q = "p:nth-of-type(0)", .n = root, .exp = null }, | ||||
|         .{ .q = "p:nth-of-type(2n)", .n = root, .exp = &p2 }, | ||||
|         .{ .q = "p:nth-last-child(1)", .n = root, .exp = &p2 }, | ||||
|         .{ .q = "p:nth-last-child(2)", .n = root, .exp = &p1 }, | ||||
|         .{ .q = "p:nth-child(1)", .n = root, .exp = &p1 }, | ||||
|         .{ .q = "p:nth-child(2)", .n = root, .exp = &p2 }, | ||||
|         .{ .q = "p:nth-child(odd)", .n = root, .exp = &p1 }, | ||||
|         .{ .q = "p:nth-child(even)", .n = root, .exp = &p2 }, | ||||
|         .{ .q = "p:nth-child(n+2)", .n = root, .exp = &p2 }, | ||||
|     }; | ||||
|  | ||||
|     for (testcases) |tc| { | ||||
|         matcher.reset(); | ||||
|  | ||||
|         const s = try css.parse(alloc, tc.q, .{}); | ||||
|         defer s.deinit(alloc); | ||||
|  | ||||
|         css.matchAll(&s, &tc.n, &matcher) catch |e| { | ||||
|             std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s }); | ||||
|             return e; | ||||
|         }; | ||||
|  | ||||
|         if (tc.exp) |exp_n| { | ||||
|             const exp: usize = 1; | ||||
|             std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| { | ||||
|                 std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s }); | ||||
|                 return e; | ||||
|             }; | ||||
|  | ||||
|             std.testing.expectEqual(exp_n, matcher.nodes.items[0]) catch |e| { | ||||
|                 std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s }); | ||||
|                 return e; | ||||
|             }; | ||||
|  | ||||
|             continue; | ||||
|         } | ||||
|  | ||||
|         const exp: usize = 0; | ||||
|         std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| { | ||||
|             std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s }); | ||||
|             return e; | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Pierre Tachoire
					Pierre Tachoire