lib/css/src/selector/nth.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2
3 /// Which sibling sequence a structural pseudo-class counts over.
4 pub const Kind = enum(u8) {
5 child,
6 last_child,
7 of_type,
8 last_of_type,
9 };
10
11 /// One `An+B` test. `next` chains further structural tests on the same
12 /// compound, one based, so that `Compound.nth` stays a single word.
13 pub const Nth = struct {
14 a: i32,
15 b: i32,
16 kind: Kind,
17 next: u32 = 0,
18 };
19
20 /// The coefficients of an `An+B` expression.
21 pub const Coefficients = struct {
22 a: i32,
23 b: i32,
24 };
25
26 /// Parses `odd`, `even`, `An+B`, `n`, or a bare integer.
27 pub fn parse(text: []const u8) ?Coefficients {
28 const trimmed = std.mem.trim(u8, text, " \t\r\n\x0C");
29 if (trimmed.len == 0) return null;
30 if (std.ascii.eqlIgnoreCase(trimmed, "odd")) return .{ .a = 2, .b = 1 };
31 if (std.ascii.eqlIgnoreCase(trimmed, "even")) return .{ .a = 2, .b = 0 };
32 const split = std.mem.indexOfAny(u8, trimmed, "nN") orelse {
33 return .{ .a = 0, .b = integer(trimmed) orelse return null };
34 };
35 const head = std.mem.trim(u8, trimmed[0..split], " \t\r\n\x0C");
36 const tail = std.mem.trim(u8, trimmed[split + 1 ..], " \t\r\n\x0C");
37 const a = coefficient(head) orelse return null;
38 if (tail.len == 0) return .{ .a = a, .b = 0 };
39 const sign: i32 = switch (tail[0]) {
40 '+' => 1,
41 '-' => -1,
42 else => return null,
43 };
44 const rest = std.mem.trim(u8, tail[1..], " \t\r\n\x0C");
45 const magnitude = integer(rest) orelse return null;
46 if (magnitude < 0) return null;
47 return .{ .a = a, .b = sign * magnitude };
48 }
49
50 /// Whether a one based `index` out of `count` siblings satisfies `An+B`.
51 pub fn matches(coefficients: Coefficients, index: u32, count: u32, kind: Kind) bool {
52 std.debug.assert(index >= 1);
53 if (count != 0) std.debug.assert(index <= count);
54 const last = kind == .last_child or kind == .last_of_type;
55 const position: i64 = if (last)
56 @as(i64, count) - @as(i64, index) + 1
57 else
58 @as(i64, index);
59 const b: i64 = coefficients.b;
60 if (coefficients.a == 0) return position == b;
61 const offset = position - b;
62 const a: i64 = coefficients.a;
63 if (@rem(offset, a) != 0) return false;
64 return @divTrunc(offset, a) >= 0;
65 }
66
67 fn coefficient(head: []const u8) ?i32 {
68 if (head.len == 0) return 1;
69 if (head.len == 1 and head[0] == '+') return 1;
70 if (head.len == 1 and head[0] == '-') return -1;
71 return integer(head);
72 }
73
74 fn integer(text: []const u8) ?i32 {
75 if (text.len == 0) return null;
76 var index: usize = 0;
77 var sign: i32 = 1;
78 if (text[0] == '+' or text[0] == '-') {
79 sign = if (text[0] == '-') -1 else 1;
80 index = 1;
81 }
82 if (index >= text.len) return null;
83 var magnitude: i32 = 0;
84 while (index < text.len) : (index += 1) {
85 if (!std.ascii.isDigit(text[index])) return null;
86 magnitude = std.math.mul(i32, magnitude, 10) catch return null;
87 magnitude = std.math.add(i32, magnitude, text[index] - '0') catch return null;
88 }
89 return sign * magnitude;
90 }
91
92 test "an nth expression parses its keywords, coefficients, and offsets" {
93 try std.testing.expectEqual(Coefficients{ .a = 2, .b = 1 }, parse("odd").?);
94 try std.testing.expectEqual(Coefficients{ .a = 2, .b = 0 }, parse("EVEN").?);
95 try std.testing.expectEqual(Coefficients{ .a = 0, .b = 3 }, parse("3").?);
96 try std.testing.expectEqual(Coefficients{ .a = 3, .b = 1 }, parse("3n + 1").?);
97 try std.testing.expectEqual(Coefficients{ .a = -1, .b = 3 }, parse("-n+3").?);
98 try std.testing.expectEqual(Coefficients{ .a = 1, .b = 0 }, parse("n").?);
99 try std.testing.expectEqual(Coefficients{ .a = 1, .b = -2 }, parse("+n-2").?);
100 try std.testing.expect(parse("") == null);
101 try std.testing.expect(parse("2x+1") == null);
102 try std.testing.expect(parse("2n*1") == null);
103 }
104
105 test "a fixed index matches only its own position" {
106 const first = Coefficients{ .a = 0, .b = 1 };
107 try std.testing.expect(matches(first, 1, 5, .child));
108 try std.testing.expect(!matches(first, 2, 5, .child));
109 try std.testing.expect(matches(first, 5, 5, .last_child));
110 try std.testing.expect(!matches(first, 4, 5, .last_child));
111 }
112
113 test "a stepped expression matches its arithmetic sequence forward only" {
114 const odd = Coefficients{ .a = 2, .b = 1 };
115 try std.testing.expect(matches(odd, 1, 6, .child));
116 try std.testing.expect(!matches(odd, 2, 6, .child));
117 try std.testing.expect(matches(odd, 5, 6, .child));
118 const leading = Coefficients{ .a = -1, .b = 3 };
119 try std.testing.expect(matches(leading, 1, 9, .child));
120 try std.testing.expect(matches(leading, 3, 9, .child));
121 try std.testing.expect(!matches(leading, 4, 9, .child));
122 }