From 75e80a47e6c22beee0215e75a64b48ebcb231b86 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Mon, 18 Mar 2024 21:21:28 +0100 Subject: [PATCH] css: implement group, compound and start combined match --- src/css/libdom.zig | 7 +++++ src/css/match_test.zig | 63 +++++++++++++++++++++++++++++++++++++++--- src/css/parser.zig | 15 ++++++---- src/css/selector.zig | 62 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 136 insertions(+), 11 deletions(-) diff --git a/src/css/libdom.zig b/src/css/libdom.zig index 318e401c..7c06cd1a 100644 --- a/src/css/libdom.zig +++ b/src/css/libdom.zig @@ -20,6 +20,13 @@ pub const Node = struct { return null; } + pub fn parent(n: Node) !?Node { + const c = try parser.nodeParentNode(n.node); + if (c) |cc| return .{ .node = cc }; + + return null; + } + pub fn isElement(n: Node) bool { const t = parser.nodeType(n.node) catch return false; return t == .element; diff --git a/src/css/match_test.zig b/src/css/match_test.zig index d0ac3c81..22694961 100644 --- a/src/css/match_test.zig +++ b/src/css/match_test.zig @@ -5,6 +5,7 @@ const css = @import("css.zig"); pub const Node = struct { child: ?*const Node = null, sibling: ?*const Node = null, + par: ?*const Node = null, name: []const u8 = "", att: ?[]const u8 = null, @@ -17,6 +18,10 @@ pub const Node = struct { return n.sibling; } + pub fn parent(n: *const Node) !?*const Node { + return n.par; + } + pub fn isElement(_: *const Node) bool { return true; } @@ -153,6 +158,24 @@ test "matchFirst" { .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, + }, }; for (testcases) |tc| { @@ -161,8 +184,15 @@ test "matchFirst" { const s = try css.parse(alloc, tc.q, .{}); defer s.deinit(alloc); - _ = try css.matchFirst(s, &tc.n, &matcher); - try std.testing.expectEqual(tc.exp, matcher.nodes.items.len); + _ = 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; + }; } } @@ -267,6 +297,24 @@ test "matchAll" { .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, + }, }; for (testcases) |tc| { @@ -275,7 +323,14 @@ test "matchAll" { const s = try css.parse(alloc, tc.q, .{}); defer s.deinit(alloc); - _ = try css.matchAll(s, &tc.n, &matcher); - try std.testing.expectEqual(tc.exp, matcher.nodes.items.len); + _ = 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; + }; } } diff --git a/src/css/parser.zig b/src/css/parser.zig index d2110883..f0da6504 100644 --- a/src/css/parser.zig +++ b/src/css/parser.zig @@ -9,6 +9,7 @@ const selector = @import("selector.zig"); const Selector = selector.Selector; const PseudoClass = selector.PseudoClass; const AttributeOP = selector.AttributeOP; +const Combinator = selector.Combinator; pub const ParseError = error{ ExpectedSelector, @@ -44,7 +45,7 @@ pub const ParseError = error{ NotHandled, UnknownPseudoSelector, InvalidNthExpression, -} || PseudoClass.Error || std.mem.Allocator.Error; +} || PseudoClass.Error || Combinator.Error || std.mem.Allocator.Error; pub const ParseOptions = struct { accept_pseudo_elts: bool = true, @@ -594,9 +595,9 @@ pub const Parser = struct { var s = try p.parseSimpleSelectorSequence(alloc); while (true) { - var combinator: u8 = undefined; + var combinator: Combinator = .empty; if (p.skipWhitespace()) { - combinator = ' '; + combinator = .descendant; } if (p.i >= p.s.len) { return s; @@ -604,16 +605,18 @@ pub const Parser = struct { switch (p.s[p.i]) { '+', '>', '~' => { - combinator = p.s[p.i]; + combinator = try Combinator.parse(p.s[p.i]); p.i += 1; _ = p.skipWhitespace(); }, // These characters can't begin a selector, but they can legally occur after one. - ',', ')' => return s, + ',', ')' => { + return s; + }, else => {}, } - if (combinator == 0) { + if (combinator == .empty) { return s; } diff --git a/src/css/selector.zig b/src/css/selector.zig index 8eae1aef..f94144b5 100644 --- a/src/css/selector.zig +++ b/src/css/selector.zig @@ -16,6 +16,28 @@ pub const AttributeOP = enum { } }; +pub const Combinator = enum { + empty, + descendant, // space + child, // > + next_sibling, // + + subsequent_sibling, // ~ + + pub const Error = error{ + InvalidCombinator, + }; + + pub fn parse(c: u8) Error!Combinator { + return switch (c) { + ' ' => .descendant, + '>' => .child, + '+' => .next_sibling, + '~' => .subsequent_sibling, + else => Error.InvalidCombinator, + }; + } +}; + pub const PseudoClass = enum { not, has, @@ -119,6 +141,10 @@ pub const PseudoClass = enum { }; pub const Selector = union(enum) { + pub const Error = error{ + UnknownCombinedCombinator, + }; + compound: struct { selectors: []Selector, pseudo_elt: ?PseudoClass, @@ -137,7 +163,7 @@ pub const Selector = union(enum) { combined: struct { first: *Selector, second: *Selector, - combinator: u8, + combinator: Combinator, }, never_match: PseudoClass, @@ -200,6 +226,40 @@ pub const Selector = union(enum) { .tag => |v| n.isElement() and std.ascii.eqlIgnoreCase(v, try n.tag()), .id => |v| return n.isElement() and std.mem.eql(u8, v, try n.attr("id") orelse return false), .class => |v| return n.isElement() and word(try n.attr("class") orelse return false, v, false), + .group => |v| { + for (v) |sel| { + if (try sel.match(n)) return true; + } + return false; + }, + .compound => |v| { + if (v.selectors.len == 0) return n.isElement(); + + for (v.selectors) |sel| { + if (!try sel.match(n)) return false; + } + return true; + }, + .combined => |v| { + return switch (v.combinator) { + .empty => try v.first.match(n), + .descendant => { + if (!try v.second.match(n)) return false; + + // The first must match a ascendent. + var p = try n.parent(); + while (p != null) { + if (try v.first.match(p.?)) { + return true; + } + p = try p.?.parent(); + } + + return false; + }, + else => return Error.UnknownCombinedCombinator, + }; + }, .attribute => |v| { const attr = try n.attr(v.key);