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 }