From db5d9332853d75153912136c9d725659001a67bf Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Mon, 25 Mar 2024 08:50:57 +0100 Subject: [PATCH] css: add nth- pseudo class --- src/css/libdom.zig | 18 ++++++ src/css/match_test.zig | 85 +++++++++++++++++++++++++++- src/css/parser.zig | 2 +- src/css/selector.zig | 124 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 225 insertions(+), 4 deletions(-) diff --git a/src/css/libdom.zig b/src/css/libdom.zig index 7c06cd1a..04c99a66 100644 --- a/src/css/libdom.zig +++ b/src/css/libdom.zig @@ -13,6 +13,13 @@ pub const Node = struct { return null; } + pub fn lastChild(n: Node) !?Node { + const c = try parser.nodeLastChild(n.node); + if (c) |cc| return .{ .node = cc }; + + return null; + } + pub fn nextSibling(n: Node) !?Node { const c = try parser.nodeNextSibling(n.node); if (c) |cc| return .{ .node = cc }; @@ -20,6 +27,13 @@ pub const Node = struct { return null; } + pub fn prevSibling(n: Node) !?Node { + const c = try parser.nodePreviousSibling(n.node); + if (c) |cc| return .{ .node = cc }; + + return null; + } + pub fn parent(n: Node) !?Node { const c = try parser.nodeParentNode(n.node); if (c) |cc| return .{ .node = cc }; @@ -39,4 +53,8 @@ pub const Node = struct { pub fn attr(n: Node, key: []const u8) !?[]const u8 { return try parser.elementGetAttribute(parser.nodeToElement(n.node), key); } + + pub fn eql(a: Node, b: Node) bool { + return a.node == b.node; + } }; diff --git a/src/css/match_test.zig b/src/css/match_test.zig index 254e9156..9aaeedbd 100644 --- a/src/css/match_test.zig +++ b/src/css/match_test.zig @@ -4,7 +4,9 @@ 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 = "", @@ -14,10 +16,18 @@ pub const Node = struct { 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; } @@ -33,6 +43,10 @@ pub const Node = struct { 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 { @@ -373,7 +387,7 @@ test "matchAll" { const s = try css.parse(alloc, tc.q, .{}); defer s.deinit(alloc); - _ = css.matchAll(s, &tc.n, &matcher) catch |e| { + css.matchAll(s, &tc.n, &matcher) catch |e| { std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s }); return e; }; @@ -384,3 +398,72 @@ test "matchAll" { }; } } + +test "nth pseudo class" { + const alloc = std.testing.allocator; + + var matcher = Matcher.init(alloc); + defer matcher.deinit(); + + var p1: Node = .{ .name = "p" }; + var p2: Node = .{ .name = "p" }; + + p1.sibling = &p2; + p2.prev = &p1; + + var root: Node = .{ .child = &p1, .last = &p2 }; + p1.par = &root; + p2.par = &root; + + const testcases = [_]struct { + q: []const u8, + n: Node, + exp: ?*const Node, + }{ + .{ .q = "a:nth-of-type(1)", .n = root, .exp = null }, + .{ .q = "p:nth-of-type(1)", .n = root, .exp = &p1 }, + .{ .q = "p:nth-of-type(2)", .n = root, .exp = &p2 }, + .{ .q = "p:nth-of-type(0)", .n = root, .exp = null }, + .{ .q = "p:nth-of-type(2n)", .n = root, .exp = &p2 }, + .{ .q = "p:nth-last-child(1)", .n = root, .exp = &p2 }, + .{ .q = "p:nth-last-child(2)", .n = root, .exp = &p1 }, + .{ .q = "p:nth-child(1)", .n = root, .exp = &p1 }, + .{ .q = "p:nth-child(2)", .n = root, .exp = &p2 }, + .{ .q = "p:nth-child(odd)", .n = root, .exp = &p1 }, + .{ .q = "p:nth-child(even)", .n = root, .exp = &p2 }, + .{ .q = "p:nth-child(n+2)", .n = root, .exp = &p2 }, + }; + + for (testcases) |tc| { + matcher.reset(); + + const s = try css.parse(alloc, tc.q, .{}); + defer s.deinit(alloc); + + css.matchAll(s, &tc.n, &matcher) catch |e| { + std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s }); + return e; + }; + + if (tc.exp) |exp_n| { + const exp: usize = 1; + std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| { + std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s }); + return e; + }; + + std.testing.expectEqual(exp_n, matcher.nodes.items[0]) catch |e| { + std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s }); + return e; + }; + + continue; + } + + const exp: usize = 0; + std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| { + std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s }); + return e; + }; + } +} diff --git a/src/css/parser.zig b/src/css/parser.zig index f0da6504..b23991c1 100644 --- a/src/css/parser.zig +++ b/src/css/parser.zig @@ -711,7 +711,7 @@ pub const Parser = struct { if (p.i >= p.s.len) return ParseError.ExpectedNthExpression; const c = p.s[p.i]; if (std.ascii.isDigit(c)) { - const a = try p.parseInteger() * -1; + const a = try p.parseInteger(); return p.parseNthReadA(a); } if (c == 'n' or c == 'N') { diff --git a/src/css/selector.zig b/src/css/selector.zig index df0788b0..381b1d67 100644 --- a/src/css/selector.zig +++ b/src/css/selector.zig @@ -144,6 +144,9 @@ pub const Selector = union(enum) { pub const Error = error{ UnknownCombinedCombinator, UnsupportedRelativePseudoClass, + UnsupportedContainsPseudoClass, + UnsupportedRegexpPseudoClass, + UnsupportedAttrRegexpOperator, }; compound: struct { @@ -222,6 +225,7 @@ pub const Selector = union(enum) { return std.mem.indexOf(u8, haystack, needle) != null; } + // match returns true if the node matches the selector query. pub fn match(s: Selector, n: anytype) !bool { return switch (s) { .tag => |v| n.isElement() and std.ascii.eqlIgnoreCase(v, try n.tag()), @@ -286,7 +290,7 @@ pub const Selector = union(enum) { return attr.?[val.len] == '-'; }, - .regexp => false, // TODO handle regexp attribute operator. + .regexp => return Error.UnsupportedAttrRegexpOperator, // TODO handle regexp attribute operator. }; }, .never_match => return false, @@ -300,10 +304,126 @@ pub const Selector = union(enum) { else => Error.UnsupportedRelativePseudoClass, }; }, - else => false, + .pseudo_class_contains => return Error.UnsupportedContainsPseudoClass, // TODO, need mem allocation. + .pseudo_class_regexp => return Error.UnsupportedRegexpPseudoClass, // TODO need mem allocation. + .pseudo_class_nth => |v| { + if (v.a == 0) { + if (v.last) { + return simpleNthLastChildMatch(v.b, v.of_type, n); + } + return simpleNthChildMatch(v.b, v.of_type, n); + } + return nthChildMatch(v.a, v.b, v.last, v.of_type, n); + }, + .pseudo_class => return false, + .pseudo_class_only_child => return false, + .pseudo_class_lang => return false, + .pseudo_element => return false, }; } + // simpleNthLastChildMatch implements :nth-last-child(b). + // If ofType is true, implements :nth-last-of-type instead. + fn simpleNthLastChildMatch(b: isize, of_type: bool, n: anytype) anyerror!bool { + if (!n.isElement()) return false; + + const p = try n.parent(); + if (p == null) return false; + + const ntag = try n.tag(); + + var count: isize = 0; + var c = try p.?.lastChild(); + // loop hover all n siblings. + while (c != null) { + // ignore non elements or others tags if of-type is true. + if (!c.?.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.?.tag()))) { + c = try c.?.prevSibling(); + continue; + } + + count += 1; + + if (n.eql(c.?)) return count == b; + if (count >= b) return false; + + c = try c.?.prevSibling(); + } + + return false; + } + + // simpleNthChildMatch implements :nth-child(b). + // If ofType is true, implements :nth-of-type instead. + fn simpleNthChildMatch(b: isize, of_type: bool, n: anytype) anyerror!bool { + if (!n.isElement()) return false; + + const p = try n.parent(); + if (p == null) return false; + + const ntag = try n.tag(); + + var count: isize = 0; + var c = try p.?.firstChild(); + // loop hover all n siblings. + while (c != null) { + // ignore non elements or others tags if of-type is true. + if (!c.?.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.?.tag()))) { + c = try c.?.nextSibling(); + continue; + } + + count += 1; + + if (n.eql(c.?)) return count == b; + if (count >= b) return false; + + c = try c.?.nextSibling(); + } + + return false; + } + + // nthChildMatch implements :nth-child(an+b). + // If last is true, implements :nth-last-child instead. + // If ofType is true, implements :nth-of-type instead. + fn nthChildMatch(a: isize, b: isize, last: bool, of_type: bool, n: anytype) anyerror!bool { + if (!n.isElement()) return false; + + const p = try n.parent(); + if (p == null) return false; + + const ntag = try n.tag(); + + var i: isize = -1; + var count: isize = 0; + var c = try p.?.firstChild(); + // loop hover all n siblings. + while (c != null) { + // ignore non elements or others tags if of-type is true. + if (!c.?.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.?.tag()))) { + c = try c.?.nextSibling(); + continue; + } + count += 1; + + if (n.eql(c.?)) { + i = count; + if (!last) break; + } + + c = try c.?.nextSibling(); + } + + if (i == -1) return false; + + if (last) i = count - i + 1; + + i -= b; + if (a == 0) return i == 0; + return @mod(i, a) == 0 and @divTrunc(i, a) >= 0; + } + fn hasDescendantMatch(s: *const Selector, n: anytype) anyerror!bool { var c = try n.firstChild(); while (c != null) {