css: implement group, compound and start combined match

This commit is contained in:
Pierre Tachoire
2024-03-18 21:21:28 +01:00
parent d0dbbacd69
commit 75e80a47e6
4 changed files with 136 additions and 11 deletions

View File

@@ -20,6 +20,13 @@ pub const Node = struct {
return null; 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 { pub fn isElement(n: Node) bool {
const t = parser.nodeType(n.node) catch return false; const t = parser.nodeType(n.node) catch return false;
return t == .element; return t == .element;

View File

@@ -5,6 +5,7 @@ const css = @import("css.zig");
pub const Node = struct { pub const Node = struct {
child: ?*const Node = null, child: ?*const Node = null,
sibling: ?*const Node = null, sibling: ?*const Node = null,
par: ?*const Node = null,
name: []const u8 = "", name: []const u8 = "",
att: ?[]const u8 = null, att: ?[]const u8 = null,
@@ -17,6 +18,10 @@ pub const Node = struct {
return n.sibling; return n.sibling;
} }
pub fn parent(n: *const Node) !?*const Node {
return n.par;
}
pub fn isElement(_: *const Node) bool { pub fn isElement(_: *const Node) bool {
return true; return true;
} }
@@ -153,6 +158,24 @@ test "matchFirst" {
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "ba" } } }, .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "ba" } } },
.exp = 0, .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| { for (testcases) |tc| {
@@ -161,8 +184,15 @@ test "matchFirst" {
const s = try css.parse(alloc, tc.q, .{}); const s = try css.parse(alloc, tc.q, .{});
defer s.deinit(alloc); defer s.deinit(alloc);
_ = try css.matchFirst(s, &tc.n, &matcher); _ = css.matchFirst(s, &tc.n, &matcher) catch |e| {
try std.testing.expectEqual(tc.exp, matcher.nodes.items.len); 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" } } }, .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "ba" } } },
.exp = 0, .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| { for (testcases) |tc| {
@@ -275,7 +323,14 @@ test "matchAll" {
const s = try css.parse(alloc, tc.q, .{}); const s = try css.parse(alloc, tc.q, .{});
defer s.deinit(alloc); defer s.deinit(alloc);
_ = try css.matchAll(s, &tc.n, &matcher); _ = css.matchAll(s, &tc.n, &matcher) catch |e| {
try std.testing.expectEqual(tc.exp, matcher.nodes.items.len); 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;
};
} }
} }

View File

@@ -9,6 +9,7 @@ const selector = @import("selector.zig");
const Selector = selector.Selector; const Selector = selector.Selector;
const PseudoClass = selector.PseudoClass; const PseudoClass = selector.PseudoClass;
const AttributeOP = selector.AttributeOP; const AttributeOP = selector.AttributeOP;
const Combinator = selector.Combinator;
pub const ParseError = error{ pub const ParseError = error{
ExpectedSelector, ExpectedSelector,
@@ -44,7 +45,7 @@ pub const ParseError = error{
NotHandled, NotHandled,
UnknownPseudoSelector, UnknownPseudoSelector,
InvalidNthExpression, InvalidNthExpression,
} || PseudoClass.Error || std.mem.Allocator.Error; } || PseudoClass.Error || Combinator.Error || std.mem.Allocator.Error;
pub const ParseOptions = struct { pub const ParseOptions = struct {
accept_pseudo_elts: bool = true, accept_pseudo_elts: bool = true,
@@ -594,9 +595,9 @@ pub const Parser = struct {
var s = try p.parseSimpleSelectorSequence(alloc); var s = try p.parseSimpleSelectorSequence(alloc);
while (true) { while (true) {
var combinator: u8 = undefined; var combinator: Combinator = .empty;
if (p.skipWhitespace()) { if (p.skipWhitespace()) {
combinator = ' '; combinator = .descendant;
} }
if (p.i >= p.s.len) { if (p.i >= p.s.len) {
return s; return s;
@@ -604,16 +605,18 @@ pub const Parser = struct {
switch (p.s[p.i]) { switch (p.s[p.i]) {
'+', '>', '~' => { '+', '>', '~' => {
combinator = p.s[p.i]; combinator = try Combinator.parse(p.s[p.i]);
p.i += 1; p.i += 1;
_ = p.skipWhitespace(); _ = p.skipWhitespace();
}, },
// These characters can't begin a selector, but they can legally occur after one. // These characters can't begin a selector, but they can legally occur after one.
',', ')' => return s, ',', ')' => {
return s;
},
else => {}, else => {},
} }
if (combinator == 0) { if (combinator == .empty) {
return s; return s;
} }

View File

@@ -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 { pub const PseudoClass = enum {
not, not,
has, has,
@@ -119,6 +141,10 @@ pub const PseudoClass = enum {
}; };
pub const Selector = union(enum) { pub const Selector = union(enum) {
pub const Error = error{
UnknownCombinedCombinator,
};
compound: struct { compound: struct {
selectors: []Selector, selectors: []Selector,
pseudo_elt: ?PseudoClass, pseudo_elt: ?PseudoClass,
@@ -137,7 +163,7 @@ pub const Selector = union(enum) {
combined: struct { combined: struct {
first: *Selector, first: *Selector,
second: *Selector, second: *Selector,
combinator: u8, combinator: Combinator,
}, },
never_match: PseudoClass, never_match: PseudoClass,
@@ -200,6 +226,40 @@ pub const Selector = union(enum) {
.tag => |v| n.isElement() and std.ascii.eqlIgnoreCase(v, try n.tag()), .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), .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), .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| { .attribute => |v| {
const attr = try n.attr(v.key); const attr = try n.attr(v.key);