css: add nth- pseudo class

This commit is contained in:
Pierre Tachoire
2024-03-25 08:50:57 +01:00
parent 9c997ec86d
commit db5d933285
4 changed files with 225 additions and 4 deletions

View File

@@ -13,6 +13,13 @@ pub const Node = struct {
return null; 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 { pub fn nextSibling(n: Node) !?Node {
const c = try parser.nodeNextSibling(n.node); const c = try parser.nodeNextSibling(n.node);
if (c) |cc| return .{ .node = cc }; if (c) |cc| return .{ .node = cc };
@@ -20,6 +27,13 @@ pub const Node = struct {
return null; 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 { pub fn parent(n: Node) !?Node {
const c = try parser.nodeParentNode(n.node); const c = try parser.nodeParentNode(n.node);
if (c) |cc| return .{ .node = cc }; if (c) |cc| return .{ .node = cc };
@@ -39,4 +53,8 @@ pub const Node = struct {
pub fn attr(n: Node, key: []const u8) !?[]const u8 { pub fn attr(n: Node, key: []const u8) !?[]const u8 {
return try parser.elementGetAttribute(parser.nodeToElement(n.node), key); return try parser.elementGetAttribute(parser.nodeToElement(n.node), key);
} }
pub fn eql(a: Node, b: Node) bool {
return a.node == b.node;
}
}; };

View File

@@ -4,7 +4,9 @@ const css = @import("css.zig");
// Node mock implementation for test only. // Node mock implementation for test only.
pub const Node = struct { pub const Node = struct {
child: ?*const Node = null, child: ?*const Node = null,
last: ?*const Node = null,
sibling: ?*const Node = null, sibling: ?*const Node = null,
prev: ?*const Node = null,
par: ?*const Node = null, par: ?*const Node = null,
name: []const u8 = "", name: []const u8 = "",
@@ -14,10 +16,18 @@ pub const Node = struct {
return n.child; return n.child;
} }
pub fn lastChild(n: *const Node) !?*const Node {
return n.last;
}
pub fn nextSibling(n: *const Node) !?*const Node { pub fn nextSibling(n: *const Node) !?*const Node {
return n.sibling; return n.sibling;
} }
pub fn prevSibling(n: *const Node) !?*const Node {
return n.prev;
}
pub fn parent(n: *const Node) !?*const Node { pub fn parent(n: *const Node) !?*const Node {
return n.par; return n.par;
} }
@@ -33,6 +43,10 @@ pub const Node = struct {
pub fn attr(n: *const Node, _: []const u8) !?[]const u8 { pub fn attr(n: *const Node, _: []const u8) !?[]const u8 {
return n.att; return n.att;
} }
pub fn eql(a: *const Node, b: *const Node) bool {
return a == b;
}
}; };
const Matcher = struct { const Matcher = struct {
@@ -373,7 +387,7 @@ 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);
_ = 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 }); std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e; 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;
};
}
}

View File

@@ -711,7 +711,7 @@ pub const Parser = struct {
if (p.i >= p.s.len) return ParseError.ExpectedNthExpression; if (p.i >= p.s.len) return ParseError.ExpectedNthExpression;
const c = p.s[p.i]; const c = p.s[p.i];
if (std.ascii.isDigit(c)) { if (std.ascii.isDigit(c)) {
const a = try p.parseInteger() * -1; const a = try p.parseInteger();
return p.parseNthReadA(a); return p.parseNthReadA(a);
} }
if (c == 'n' or c == 'N') { if (c == 'n' or c == 'N') {

View File

@@ -144,6 +144,9 @@ pub const Selector = union(enum) {
pub const Error = error{ pub const Error = error{
UnknownCombinedCombinator, UnknownCombinedCombinator,
UnsupportedRelativePseudoClass, UnsupportedRelativePseudoClass,
UnsupportedContainsPseudoClass,
UnsupportedRegexpPseudoClass,
UnsupportedAttrRegexpOperator,
}; };
compound: struct { compound: struct {
@@ -222,6 +225,7 @@ pub const Selector = union(enum) {
return std.mem.indexOf(u8, haystack, needle) != null; 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 { pub fn match(s: Selector, n: anytype) !bool {
return switch (s) { return switch (s) {
.tag => |v| n.isElement() and std.ascii.eqlIgnoreCase(v, try n.tag()), .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] == '-'; return attr.?[val.len] == '-';
}, },
.regexp => false, // TODO handle regexp attribute operator. .regexp => return Error.UnsupportedAttrRegexpOperator, // TODO handle regexp attribute operator.
}; };
}, },
.never_match => return false, .never_match => return false,
@@ -300,10 +304,126 @@ pub const Selector = union(enum) {
else => Error.UnsupportedRelativePseudoClass, 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 { fn hasDescendantMatch(s: *const Selector, n: anytype) anyerror!bool {
var c = try n.firstChild(); var c = try n.firstChild();
while (c != null) { while (c != null) {