lib/css/src/selector/grammar.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 
  3 const atom = @import("../atom.zig");
  4 const nth = @import("nth.zig");
  5 const token = @import("../token/root.zig");
  6 
  7 /// How two compounds are joined. The list runs left to right, so
  8 /// `combinators[i]` joins `compounds[i]` to `compounds[i + 1]`.
  9 pub const Combinator = enum(u8) {
 10     descendant,
 11     child,
 12     next_sibling,
 13     subsequent_sibling,
 14 };
 15 
 16 /// The interaction and node state pseudo-classes, one bit each. An element
 17 /// interface reports them as one word, so `:hover` and `[state~=selected]` are
 18 /// the same test on the same bit.
 19 pub const PseudoClass = enum(u5) {
 20     hover = 0,
 21     focus = 1,
 22     focus_visible = 2,
 23     focus_within = 3,
 24     active = 4,
 25     target = 5,
 26     root = 6,
 27     empty = 7,
 28     enabled = 8,
 29     disabled = 9,
 30     checked = 10,
 31     indeterminate = 11,
 32     selected = 12,
 33     expanded = 13,
 34     pressed = 14,
 35     busy = 15,
 36     invalid = 16,
 37     required = 17,
 38     current = 18,
 39     mixed = 19,
 40     link = 20,
 41     visited = 21,
 42     read_only = 22,
 43     read_write = 23,
 44     placeholder_shown = 24,
 45     optional = 25,
 46     default = 26,
 47     open = 27,
 48     modal = 28,
 49     defined = 29,
 50 };
 51 
 52 /// The bit `class` occupies in an element's pseudo-class word.
 53 pub fn bit(class: PseudoClass) u32 {
 54     return @as(u32, 1) << @backingInt(class);
 55 }
 56 
 57 /// Resolves a pseudo-class written with a colon, ASCII case insensitively.
 58 pub fn pseudoByName(name: []const u8) ?PseudoClass {
 59     return byName(name);
 60 }
 61 
 62 /// Resolves one of the ten published node states, written `[state~=name]`.
 63 pub fn stateByName(name: []const u8) ?PseudoClass {
 64     const found = byName(name) orelse return null;
 65     for (states) |candidate| {
 66         if (candidate == found) return found;
 67     }
 68     return null;
 69 }
 70 
 71 /// The node states the published node record carries in its `state` word.
 72 pub const states = [_]PseudoClass{
 73     .selected, .disabled, .expanded, .checked,  .mixed,
 74     .pressed,  .busy,     .invalid,  .required, .current,
 75 };
 76 
 77 fn byName(name: []const u8) ?PseudoClass {
 78     if (name.len == 0 or name.len > 20) return null;
 79     var buffer: [20]u8 = undefined;
 80     for (name, 0..) |byte, index| {
 81         buffer[index] = if (byte == '-') '_' else std.ascii.toLower(byte);
 82     }
 83     return std.meta.stringToEnum(PseudoClass, buffer[0..name.len]);
 84 }
 85 
 86 /// The four specificity fields, ordered from strongest to weakest.
 87 pub const Specificity = struct {
 88     inline_style: u16 = 0,
 89     ids: u16 = 0,
 90     classes: u16 = 0,
 91     types: u16 = 0,
 92 
 93     pub fn compare(self: Specificity, other: Specificity) std.math.Order {
 94         if (self.inline_style != other.inline_style) {
 95             return std.math.order(self.inline_style, other.inline_style);
 96         }
 97         if (self.ids != other.ids) return std.math.order(self.ids, other.ids);
 98         if (self.classes != other.classes) return std.math.order(self.classes, other.classes);
 99         return std.math.order(self.types, other.types);
100     }
101 
102     pub fn inlineStyle() Specificity {
103         return .{ .inline_style = 1 };
104     }
105 };
106 
107 /// Set on a compound whose grammar named something the node model cannot
108 /// carry. Such a compound never matches, which is what CSS requires of a
109 /// selector over an attribute no element has.
110 pub const unmatchable: u16 = 1;
111 
112 /// One compound selector. Every identifier is an interned atom, so matching a
113 /// compound is a run of integer compares.
114 pub const Compound = extern struct {
115     kind: u16 = 0,
116     role: u16 = 0,
117     id: u32 = 0,
118     class_first: u32 = 0,
119     class_count: u16 = 0,
120     flags: u16 = 0,
121     pseudo: u32 = 0,
122     nth: u32 = 0,
123 };
124 
125 /// The strongest key of a selector's rightmost compound. The rule bucket index
126 /// groups selectors by this key so a node visits few candidates.
127 pub const BucketKey = union(enum(u8)) {
128     universal,
129     id: u32,
130     class: u32,
131     role: u16,
132     kind: u16,
133 };
134 
135 /// One compiled selector. Slices borrow the placement arrays the sheet owns.
136 pub const Selector = struct {
137     raw: []const u8 = &.{},
138     compounds: []const Compound = &.{},
139     combinators: []const Combinator = &.{},
140     specificity: Specificity = .{},
141     bucket: BucketKey = .universal,
142 
143     /// The compound a candidate node is tested against first.
144     pub fn rightmost(self: Selector) Compound {
145         std.debug.assert(self.compounds.len > 0);
146         return self.compounds[self.compounds.len - 1];
147     }
148 };
149 
150 /// The span of selectors one selector list contributed.
151 pub const Range = struct {
152     first: u32,
153     count: u32,
154 };
155 
156 /// Builds compiled selectors into placement arrays, or counts them when the
157 /// arrays are absent. One builder serves both passes so the inspection and
158 /// emission walks cannot drift.
159 pub const Builder = struct {
160     source: []const u8,
161     atoms: ?*atom.Table = null,
162     classes: ?[]u32 = null,
163     compounds: ?[]Compound = null,
164     combinators: ?[]Combinator = null,
165     nths: ?[]nth.Nth = null,
166     selectors: ?[]Selector = null,
167     diagnostics: ?[][]const u8 = null,
168 
169     class_used: u32 = 0,
170     compound_used: u32 = 0,
171     combinator_used: u32 = 0,
172     nth_used: u32 = 0,
173     selector_used: u32 = 0,
174     atom_used: u32 = 0,
175     atom_bytes: u32 = 0,
176     dependency_used: u32 = 0,
177     diagnostic_used: u32 = 0,
178 
179     /// Interns one identifier, folding it when the position is case
180     /// insensitive. Returns `atom.none` when the identifier cannot be held.
181     pub fn intern(self: *Builder, text: []const u8, folded: bool) error{CapacityOverflow}!u32 {
182         if (text.len == 0 or text.len > atom.max_text_bytes) return atom.none;
183         var buffer: [atom.max_text_bytes]u8 = undefined;
184         var body = text;
185         var copy = false;
186         if (token.hasEscape(body)) {
187             body = token.unescape(body, &buffer) orelse return atom.none;
188             copy = true;
189         }
190         if (folded and needsFold(body)) {
191             if (!copy) @memcpy(buffer[0..body.len], body);
192             for (buffer[0..body.len]) |*byte| byte.* = std.ascii.toLower(byte.*);
193             body = buffer[0..body.len];
194             copy = true;
195         }
196         if (body.len == 0) return atom.none;
197         if (self.atoms) |table| return table.intern(body, copy);
198         self.atom_used = try grow(self.atom_used, 1);
199         if (copy) self.atom_bytes = try grow(self.atom_bytes, @intCast(body.len));
200         return self.atom_used;
201     }
202 
203     fn pushClass(self: *Builder, id: u32) error{CapacityOverflow}!void {
204         if (self.classes) |slice| {
205             std.debug.assert(self.class_used < slice.len);
206             slice[self.class_used] = id;
207         }
208         self.class_used = try grow(self.class_used, 1);
209     }
210 
211     fn pushCompound(self: *Builder, compound: Compound) error{CapacityOverflow}!void {
212         if (self.compounds) |slice| {
213             std.debug.assert(self.compound_used < slice.len);
214             slice[self.compound_used] = compound;
215         }
216         self.compound_used = try grow(self.compound_used, 1);
217     }
218 
219     fn pushCombinator(self: *Builder, joint: Combinator) error{CapacityOverflow}!void {
220         if (self.combinators) |slice| {
221             std.debug.assert(self.combinator_used < slice.len);
222             slice[self.combinator_used] = joint;
223         }
224         self.combinator_used = try grow(self.combinator_used, 1);
225     }
226 
227     fn pushNth(self: *Builder, out: *Compound, record: nth.Nth) error{CapacityOverflow}!void {
228         const id = try grow(self.nth_used, 1);
229         if (self.nths) |slice| {
230             std.debug.assert(self.nth_used < slice.len);
231             slice[self.nth_used] = record;
232         }
233         self.nth_used = id;
234         if (out.nth == 0) {
235             out.nth = id;
236             return;
237         }
238         const slice = self.nths orelse return;
239         var walk = out.nth;
240         var guard: usize = 0;
241         while (slice[walk - 1].next != 0 and guard <= slice.len) : (guard += 1) {
242             walk = slice[walk - 1].next;
243         }
244         slice[walk - 1].next = id;
245     }
246 
247     /// Records one diagnostic against the shared diagnostic cursor.
248     pub fn note(self: *Builder, text: []const u8) error{CapacityOverflow}!void {
249         if (self.diagnostics) |slice| {
250             std.debug.assert(self.diagnostic_used < slice.len);
251             slice[self.diagnostic_used] = text;
252         }
253         self.diagnostic_used = try grow(self.diagnostic_used, 1);
254     }
255 
256     fn flagLast(self: *Builder) void {
257         const slice = self.compounds orelse return;
258         if (self.compound_used == 0) return;
259         slice[self.compound_used - 1].flags |= unmatchable;
260     }
261 };
262 
263 fn needsFold(text: []const u8) bool {
264     for (text) |byte| {
265         if (byte >= 'A' and byte <= 'Z') return true;
266     }
267     return false;
268 }
269 
270 fn grow(current: u32, amount: u32) error{CapacityOverflow}!u32 {
271     return std.math.add(u32, current, amount) catch error.CapacityOverflow;
272 }
273 
274 /// Parses a comma separated selector list over `source[start..end]` and
275 /// returns the span of selectors it produced.
276 pub fn parseList(builder: *Builder, start: u32, end: u32) error{CapacityOverflow}!Range {
277     std.debug.assert(start <= end);
278     std.debug.assert(end <= builder.source.len);
279     const first = builder.selector_used;
280     var cursor = start;
281     var guard: usize = 0;
282     while (cursor <= end and guard <= builder.source.len + 1) : (guard += 1) {
283         const comma = topLevelComma(builder.source, cursor, end) orelse end;
284         try parseSelector(builder, cursor, comma);
285         if (comma >= end) break;
286         cursor = comma + 1;
287     }
288     return .{ .first = first, .count = builder.selector_used - first };
289 }
290 
291 /// Publishes one selector that never matches an element. An at-rule
292 /// descriptor block keeps its prelude text this way without joining matching.
293 pub fn parseOpaque(builder: *Builder, start: u32, end: u32) error{CapacityOverflow}!Range {
294     std.debug.assert(start <= end);
295     std.debug.assert(end <= builder.source.len);
296     const first = builder.selector_used;
297     const compound_first = builder.compound_used;
298     const combinator_first = builder.combinator_used;
299     try builder.pushCompound(.{ .flags = unmatchable });
300     try publish(builder, .{
301         .compound_first = compound_first,
302         .combinator_first = combinator_first,
303         .count = 1,
304         .start = start,
305         .end = end,
306         .specificity = .{},
307     });
308     return .{ .first = first, .count = builder.selector_used - first };
309 }
310 
311 fn parseSelector(builder: *Builder, start: u32, end: u32) error{CapacityOverflow}!void {
312     var cursor = skipSpace(builder.source, start, end);
313     const trimmed_end = trimSpace(builder.source, cursor, end);
314     if (cursor >= trimmed_end) return;
315     const compound_first = builder.compound_used;
316     const combinator_first = builder.combinator_used;
317     var specificity = Specificity{};
318     var count: u32 = 0;
319     var guard: usize = 0;
320     while (cursor < trimmed_end and guard <= builder.source.len + 1) : (guard += 1) {
321         if (count > 0) {
322             const joint = readCombinator(builder.source, &cursor, trimmed_end) orelse break;
323             if (cursor >= trimmed_end) {
324                 builder.flagLast();
325                 break;
326             }
327             try builder.pushCombinator(joint);
328         }
329         var compound = Compound{};
330         try parseCompound(builder, &cursor, trimmed_end, &compound, &specificity);
331         try builder.pushCompound(compound);
332         count += 1;
333     }
334     if (count == 0) return;
335     try publish(builder, .{
336         .compound_first = compound_first,
337         .combinator_first = combinator_first,
338         .count = count,
339         .start = start,
340         .end = trimmed_end,
341         .specificity = specificity,
342     });
343 }
344 
345 const Publication = struct {
346     compound_first: u32,
347     combinator_first: u32,
348     count: u32,
349     start: u32,
350     end: u32,
351     specificity: Specificity,
352 };
353 
354 fn publish(builder: *Builder, record: Publication) error{CapacityOverflow}!void {
355     const index = builder.selector_used;
356     builder.selector_used = try grow(builder.selector_used, 1);
357     const slice = builder.selectors orelse return;
358     std.debug.assert(index < slice.len);
359     const compounds = builder.compounds.?[record.compound_first..][0..record.count];
360     const joints = builder.combinators.?[record.combinator_first..][0 .. record.count - 1];
361     slice[index] = .{
362         .raw = std.mem.trim(u8, builder.source[record.start..record.end], " \t\r\n\x0C"),
363         .compounds = compounds,
364         .combinators = joints,
365         .specificity = record.specificity,
366         .bucket = bucketOf(builder, compounds[compounds.len - 1]),
367     };
368 }
369 
370 fn bucketOf(builder: *Builder, compound: Compound) BucketKey {
371     if (compound.id != atom.none) return .{ .id = compound.id };
372     if (compound.class_count > 0) {
373         const classes = builder.classes.?;
374         return .{ .class = classes[compound.class_first] };
375     }
376     if (compound.role != 0) return .{ .role = compound.role };
377     if (compound.kind != 0) return .{ .kind = compound.kind };
378     return .universal;
379 }
380 
381 fn parseCompound(
382     builder: *Builder,
383     cursor: *u32,
384     end: u32,
385     out: *Compound,
386     specificity: *Specificity,
387 ) error{CapacityOverflow}!void {
388     out.class_first = builder.class_used;
389     var simple: u32 = 0;
390     var guard: usize = 0;
391     while (cursor.* < end and guard <= builder.source.len + 1) : (guard += 1) {
392         const byte = builder.source[cursor.*];
393         if (token.isWhitespace(byte)) break;
394         if (byte == '>' or byte == '+' or byte == '~' or byte == ',') break;
395         try parseSimple(builder, cursor, end, out, specificity, byte);
396         simple += 1;
397     }
398     out.class_count = @intCast(builder.class_used - out.class_first);
399     if (simple == 0) {
400         out.flags |= unmatchable;
401         try builder.note("selector-empty-compound");
402         cursor.* += 1;
403     }
404 }
405 
406 fn parseSimple(
407     builder: *Builder,
408     cursor: *u32,
409     end: u32,
410     out: *Compound,
411     specificity: *Specificity,
412     byte: u8,
413 ) error{CapacityOverflow}!void {
414     switch (byte) {
415         '*' => cursor.* += 1,
416         '#' => {
417             cursor.* += 1;
418             const id = try readAtom(builder, cursor, end, false);
419             if (id == atom.none) return reject(builder, out, cursor, "selector-empty-id");
420             out.id = id;
421             specificity.ids += 1;
422             builder.dependency_used = try grow(builder.dependency_used, 1);
423         },
424         '.' => {
425             cursor.* += 1;
426             const id = try readAtom(builder, cursor, end, false);
427             if (id == atom.none) return reject(builder, out, cursor, "selector-empty-class");
428             try builder.pushClass(id);
429             specificity.classes += 1;
430             builder.dependency_used = try grow(builder.dependency_used, 1);
431         },
432         '[' => try parseAttribute(builder, cursor, end, out, specificity),
433         ':' => try parsePseudo(builder, cursor, end, out, specificity),
434         else => {
435             if (!token.startsIdent(builder.source, cursor.*)) {
436                 return reject(builder, out, cursor, "selector-unexpected-byte");
437             }
438             const id = try readAtom(builder, cursor, end, true);
439             if (id == atom.none or id > atom.max_atoms) {
440                 return reject(builder, out, cursor, "selector-type-overflow");
441             }
442             out.kind = @intCast(id);
443             specificity.types += 1;
444             builder.dependency_used = try grow(builder.dependency_used, 1);
445         },
446     }
447 }
448 
449 fn reject(
450     builder: *Builder,
451     out: *Compound,
452     cursor: *u32,
453     text: []const u8,
454 ) error{CapacityOverflow}!void {
455     out.flags |= unmatchable;
456     cursor.* += 1;
457     try builder.note(text);
458 }
459 
460 fn parseAttribute(
461     builder: *Builder,
462     cursor: *u32,
463     end: u32,
464     out: *Compound,
465     specificity: *Specificity,
466 ) error{CapacityOverflow}!void {
467     const close = std.mem.indexOfScalarPos(u8, builder.source[0..end], cursor.*, ']') orelse {
468         cursor.* = end;
469         out.flags |= unmatchable;
470         return builder.note("selector-unclosed-attribute");
471     };
472     const body = builder.source[cursor.* + 1 .. close];
473     cursor.* = @intCast(close + 1);
474     const parsed = splitAttribute(body) orelse {
475         out.flags |= unmatchable;
476         return builder.note("selector-bad-attribute");
477     };
478     specificity.classes += 1;
479     try applyAttribute(builder, out, parsed);
480 }
481 
482 const Attribute = struct {
483     key: []const u8,
484     operator: []const u8,
485     value: []const u8,
486 };
487 
488 fn splitAttribute(body: []const u8) ?Attribute {
489     const trimmed = std.mem.trim(u8, body, " \t\r\n\x0C");
490     if (trimmed.len == 0) return null;
491     const stop = token.consumeIdent(trimmed, 0);
492     if (stop == 0) return null;
493     const key = trimmed[0..stop];
494     const rest = std.mem.trim(u8, trimmed[stop..], " \t\r\n\x0C");
495     if (rest.len == 0) return .{ .key = key, .operator = "", .value = "" };
496     const equals = std.mem.indexOfScalar(u8, rest, '=') orelse return null;
497     if (equals > 1) return null;
498     const operator = rest[0 .. equals + 1];
499     var value = std.mem.trim(u8, rest[equals + 1 ..], " \t\r\n\x0C");
500     if (value.len >= 2 and (value[0] == '"' or value[0] == '\'') and value[value.len - 1] == value[0]) {
501         value = value[1 .. value.len - 1];
502     } else {
503         const word = token.consumeIdent(value, 0);
504         value = value[0..word];
505     }
506     if (value.len == 0) return null;
507     return .{ .key = key, .operator = operator, .value = value };
508 }
509 
510 fn applyAttribute(
511     builder: *Builder,
512     out: *Compound,
513     parsed: Attribute,
514 ) error{CapacityOverflow}!void {
515     const exact = std.mem.eql(u8, parsed.operator, "=");
516     const includes = std.mem.eql(u8, parsed.operator, "~=");
517     if (std.ascii.eqlIgnoreCase(parsed.key, "id") and exact) {
518         out.id = try builder.intern(parsed.value, false);
519         if (out.id == atom.none) {
520             out.flags |= unmatchable;
521         } else {
522             builder.dependency_used = try grow(builder.dependency_used, 1);
523         }
524         return;
525     }
526     if (std.ascii.eqlIgnoreCase(parsed.key, "class") and includes) {
527         const id = try builder.intern(parsed.value, false);
528         if (id == atom.none) {
529             out.flags |= unmatchable;
530         } else {
531             try builder.pushClass(id);
532             builder.dependency_used = try grow(builder.dependency_used, 1);
533         }
534         return;
535     }
536     if (std.ascii.eqlIgnoreCase(parsed.key, "role") and (exact or includes)) {
537         const id = try builder.intern(parsed.value, true);
538         if (id == atom.none or id > atom.max_atoms) out.flags |= unmatchable else out.role = @intCast(id);
539         return;
540     }
541     if (std.ascii.eqlIgnoreCase(parsed.key, "state") and includes) {
542         const state = stateByName(parsed.value) orelse {
543             out.flags |= unmatchable;
544             return builder.note("selector-unknown-state");
545         };
546         out.pseudo |= bit(state);
547         return;
548     }
549     out.flags |= unmatchable;
550     try builder.note("selector-unmodelled-attribute");
551 }
552 
553 fn parsePseudo(
554     builder: *Builder,
555     cursor: *u32,
556     end: u32,
557     out: *Compound,
558     specificity: *Specificity,
559 ) error{CapacityOverflow}!void {
560     cursor.* += 1;
561     if (cursor.* < end and builder.source[cursor.*] == ':') {
562         cursor.* += 1;
563         _ = try readName(builder, cursor, end);
564         specificity.types += 1;
565         out.flags |= unmatchable;
566         return builder.note("selector-pseudo-element");
567     }
568     const name = try readName(builder, cursor, end);
569     if (name.len == 0) return reject(builder, out, cursor, "selector-empty-pseudo");
570     specificity.classes += 1;
571     if (cursor.* < end and builder.source[cursor.*] == '(') {
572         return parseFunctionalPseudo(builder, cursor, end, out, name);
573     }
574     if (pseudoByName(name)) |class| {
575         out.pseudo |= bit(class);
576         return;
577     }
578     if (structural(name)) |pair| {
579         try builder.pushNth(out, .{ .a = pair.a, .b = pair.b, .kind = pair.kind });
580         if (pair.second) |extra| {
581             try builder.pushNth(out, .{ .a = extra.a, .b = extra.b, .kind = extra.kind });
582         }
583         return;
584     }
585     out.flags |= unmatchable;
586     try builder.note("selector-unknown-pseudo");
587 }
588 
589 fn parseFunctionalPseudo(
590     builder: *Builder,
591     cursor: *u32,
592     end: u32,
593     out: *Compound,
594     name: []const u8,
595 ) error{CapacityOverflow}!void {
596     const close = std.mem.indexOfScalarPos(u8, builder.source[0..end], cursor.*, ')') orelse {
597         cursor.* = end;
598         out.flags |= unmatchable;
599         return builder.note("selector-unclosed-pseudo");
600     };
601     const body = builder.source[cursor.* + 1 .. close];
602     cursor.* = @intCast(close + 1);
603     const kind = functionalKind(name) orelse {
604         out.flags |= unmatchable;
605         return builder.note("selector-unsupported-pseudo");
606     };
607     const coefficients = nth.parse(body) orelse {
608         out.flags |= unmatchable;
609         return builder.note("selector-bad-nth");
610     };
611     try builder.pushNth(out, .{ .a = coefficients.a, .b = coefficients.b, .kind = kind });
612 }
613 
614 fn functionalKind(name: []const u8) ?nth.Kind {
615     if (std.ascii.eqlIgnoreCase(name, "nth-child")) return .child;
616     if (std.ascii.eqlIgnoreCase(name, "nth-last-child")) return .last_child;
617     if (std.ascii.eqlIgnoreCase(name, "nth-of-type")) return .of_type;
618     if (std.ascii.eqlIgnoreCase(name, "nth-last-of-type")) return .last_of_type;
619     return null;
620 }
621 
622 const Structural = struct {
623     a: i32,
624     b: i32,
625     kind: nth.Kind,
626     second: ?Extra = null,
627 
628     const Extra = struct {
629         a: i32,
630         b: i32,
631         kind: nth.Kind,
632     };
633 };
634 
635 fn structural(name: []const u8) ?Structural {
636     if (std.ascii.eqlIgnoreCase(name, "first-child")) return .{ .a = 0, .b = 1, .kind = .child };
637     if (std.ascii.eqlIgnoreCase(name, "last-child")) return .{ .a = 0, .b = 1, .kind = .last_child };
638     if (std.ascii.eqlIgnoreCase(name, "only-child")) return .{
639         .a = 0,
640         .b = 1,
641         .kind = .child,
642         .second = .{ .a = 0, .b = 1, .kind = .last_child },
643     };
644     if (std.ascii.eqlIgnoreCase(name, "first-of-type")) return .{ .a = 0, .b = 1, .kind = .of_type };
645     if (std.ascii.eqlIgnoreCase(name, "last-of-type")) return .{ .a = 0, .b = 1, .kind = .last_of_type };
646     if (std.ascii.eqlIgnoreCase(name, "only-of-type")) return .{
647         .a = 0,
648         .b = 1,
649         .kind = .of_type,
650         .second = .{ .a = 0, .b = 1, .kind = .last_of_type },
651     };
652     return null;
653 }
654 
655 fn readName(builder: *Builder, cursor: *u32, end: u32) error{CapacityOverflow}![]const u8 {
656     const start = cursor.*;
657     const stop = @min(token.consumeIdent(builder.source, start), end);
658     cursor.* = stop;
659     if (stop == start) return &.{};
660     return builder.source[start..stop];
661 }
662 
663 fn readAtom(
664     builder: *Builder,
665     cursor: *u32,
666     end: u32,
667     folded: bool,
668 ) error{CapacityOverflow}!u32 {
669     const start = cursor.*;
670     const stop = @min(token.consumeIdent(builder.source, start), end);
671     if (stop == start) return atom.none;
672     cursor.* = stop;
673     return builder.intern(builder.source[start..stop], folded);
674 }
675 
676 fn readCombinator(source: []const u8, cursor: *u32, end: u32) ?Combinator {
677     const before = cursor.*;
678     const index = skipSpace(source, before, end);
679     if (index >= end) {
680         cursor.* = index;
681         return null;
682     }
683     const explicit: ?Combinator = switch (source[index]) {
684         '>' => .child,
685         '+' => .next_sibling,
686         '~' => .subsequent_sibling,
687         else => null,
688     };
689     if (explicit) |joint| {
690         cursor.* = skipSpace(source, index + 1, end);
691         return joint;
692     }
693     if (index == before) return null;
694     cursor.* = index;
695     return .descendant;
696 }
697 
698 fn topLevelComma(source: []const u8, start: u32, end: u32) ?u32 {
699     var index = start;
700     var square: u32 = 0;
701     var paren: u32 = 0;
702     while (index < end) : (index += 1) {
703         switch (source[index]) {
704             '[' => square += 1,
705             ']' => square -|= 1,
706             '(' => paren += 1,
707             ')' => paren -|= 1,
708             ',' => if (square == 0 and paren == 0) return index,
709             else => {},
710         }
711     }
712     return null;
713 }
714 
715 fn skipSpace(source: []const u8, start: u32, end: u32) u32 {
716     var index = start;
717     while (index < end and token.isWhitespace(source[index])) index += 1;
718     return index;
719 }
720 
721 fn trimSpace(source: []const u8, start: u32, end: u32) u32 {
722     var index = end;
723     while (index > start and token.isWhitespace(source[index - 1])) index -= 1;
724     return index;
725 }
726 
727 const Fixture = struct {
728     records: [64]atom.Record = undefined,
729     slots: [256]u32 = @splat(0),
730     arena: [1024]u8 = undefined,
731     classes: [64]u32 = undefined,
732     compounds: [64]Compound = undefined,
733     combinators: [64]Combinator = undefined,
734     nths: [64]nth.Nth = undefined,
735     selectors: [32]Selector = undefined,
736     diagnostics: [32][]const u8 = undefined,
737     table: atom.Table = undefined,
738     builder: Builder = undefined,
739 
740     fn parse(self: *Fixture, source: []const u8) !Range {
741         self.table = .{ .records = &self.records, .slots = &self.slots, .arena = &self.arena };
742         self.builder = .{
743             .source = source,
744             .atoms = &self.table,
745             .classes = &self.classes,
746             .compounds = &self.compounds,
747             .combinators = &self.combinators,
748             .nths = &self.nths,
749             .selectors = &self.selectors,
750             .diagnostics = &self.diagnostics,
751         };
752         return parseList(&self.builder, 0, @intCast(source.len));
753     }
754 
755     fn only(self: *Fixture, source: []const u8) !Selector {
756         const range = try self.parse(source);
757         try std.testing.expectEqual(@as(u32, 1), range.count);
758         return self.selectors[range.first];
759     }
760 };
761 
762 test "a compound keeps every class instead of the last one" {
763     var fixture = Fixture{};
764     const selector = try fixture.only(".a.b");
765     try std.testing.expectEqual(@as(usize, 1), selector.compounds.len);
766     try std.testing.expectEqual(@as(u16, 2), selector.compounds[0].class_count);
767     const classes = fixture.classes[selector.compounds[0].class_first..][0..2];
768     try std.testing.expectEqualStrings("a", fixture.table.name(classes[0]));
769     try std.testing.expectEqualStrings("b", fixture.table.name(classes[1]));
770     try std.testing.expectEqual(@as(u16, 2), selector.specificity.classes);
771 }
772 
773 test "combinators join compounds left to right" {
774     var fixture = Fixture{};
775     const selector = try fixture.only("main > ul li + span ~ em");
776     try std.testing.expectEqual(@as(usize, 5), selector.compounds.len);
777     try std.testing.expectEqual(@as(usize, 4), selector.combinators.len);
778     try std.testing.expectEqual(Combinator.child, selector.combinators[0]);
779     try std.testing.expectEqual(Combinator.descendant, selector.combinators[1]);
780     try std.testing.expectEqual(Combinator.next_sibling, selector.combinators[2]);
781     try std.testing.expectEqual(Combinator.subsequent_sibling, selector.combinators[3]);
782     try std.testing.expectEqual(@as(u16, 5), selector.specificity.types);
783 }
784 
785 test "specificity counts ids, classes, and types over the whole chain" {
786     var fixture = Fixture{};
787     const selector = try fixture.only("#main .row button:hover");
788     try std.testing.expectEqual(@as(u16, 1), selector.specificity.ids);
789     try std.testing.expectEqual(@as(u16, 2), selector.specificity.classes);
790     try std.testing.expectEqual(@as(u16, 1), selector.specificity.types);
791     const weaker = try fixture.only(".row button");
792     try std.testing.expect(selector.specificity.compare(weaker.specificity) == .gt);
793 }
794 
795 test "a selector list becomes one selector per entry" {
796     var fixture = Fixture{};
797     const range = try fixture.parse("a, b > c , .d");
798     try std.testing.expectEqual(@as(u32, 3), range.count);
799     try std.testing.expectEqualStrings("a", fixture.selectors[0].raw);
800     try std.testing.expectEqualStrings("b > c", fixture.selectors[1].raw);
801     try std.testing.expectEqualStrings(".d", fixture.selectors[2].raw);
802 }
803 
804 test "the bucket key takes the strongest part of the rightmost compound" {
805     var fixture = Fixture{};
806     try std.testing.expectEqual(BucketKey.id, std.meta.activeTag((try fixture.only("div #a")).bucket));
807     try std.testing.expectEqual(BucketKey.class, std.meta.activeTag((try fixture.only("div .a")).bucket));
808     try std.testing.expectEqual(BucketKey.kind, std.meta.activeTag((try fixture.only(".a div")).bucket));
809     try std.testing.expectEqual(
810         BucketKey.role,
811         std.meta.activeTag((try fixture.only("[role=button]")).bucket),
812     );
813     try std.testing.expectEqual(BucketKey.universal, std.meta.activeTag((try fixture.only("*")).bucket));
814 }
815 
816 test "state attributes and pseudo classes reach the same bits" {
817     var fixture = Fixture{};
818     const attribute = try fixture.only("[state~=disabled]");
819     const pseudo = try fixture.only(":disabled");
820     try std.testing.expectEqual(bit(.disabled), attribute.compounds[0].pseudo);
821     try std.testing.expectEqual(bit(.disabled), pseudo.compounds[0].pseudo);
822     const combined = try fixture.only("button:hover:focus-visible");
823     try std.testing.expectEqual(bit(.hover) | bit(.focus_visible), combined.compounds[0].pseudo);
824 }
825 
826 test "an attribute over an unmodelled key never matches and is reported" {
827     var fixture = Fixture{};
828     const selector = try fixture.only("a[href]");
829     try std.testing.expect(selector.compounds[0].flags & unmatchable != 0);
830     try std.testing.expectEqual(@as(u32, 1), fixture.builder.diagnostic_used);
831     try std.testing.expectEqualStrings("selector-unmodelled-attribute", fixture.diagnostics[0]);
832     try std.testing.expectEqual(@as(u16, 1), selector.specificity.classes);
833 }
834 
835 test "structural pseudo classes chain nth records on one compound" {
836     var fixture = Fixture{};
837     const selector = try fixture.only("li:first-child:nth-of-type(2n+1)");
838     const head = selector.compounds[0].nth;
839     try std.testing.expect(head != 0);
840     const first = fixture.nths[head - 1];
841     try std.testing.expectEqual(nth.Kind.child, first.kind);
842     try std.testing.expectEqual(@as(i32, 1), first.b);
843     const second = fixture.nths[first.next - 1];
844     try std.testing.expectEqual(nth.Kind.of_type, second.kind);
845     try std.testing.expectEqual(@as(i32, 2), second.a);
846     try std.testing.expectEqual(@as(u32, 0), second.next);
847 }
848 
849 test "only-child expands into a first and a last test" {
850     var fixture = Fixture{};
851     const selector = try fixture.only(":only-child");
852     const head = fixture.nths[selector.compounds[0].nth - 1];
853     try std.testing.expectEqual(nth.Kind.child, head.kind);
854     try std.testing.expectEqual(nth.Kind.last_child, fixture.nths[head.next - 1].kind);
855 }
856 
857 test "type and role names fold while ids and classes keep their case" {
858     var fixture = Fixture{};
859     const upper = try fixture.only("BUTTON[role=Tab]#Save.Row");
860     try std.testing.expectEqualStrings("button", fixture.table.name(upper.compounds[0].kind));
861     try std.testing.expectEqualStrings("tab", fixture.table.name(upper.compounds[0].role));
862     try std.testing.expectEqualStrings("Save", fixture.table.name(upper.compounds[0].id));
863     const classes = fixture.classes[upper.compounds[0].class_first..][0..1];
864     try std.testing.expectEqualStrings("Row", fixture.table.name(classes[0]));
865 }
866 
867 test "a pseudo element and an unknown pseudo class never match" {
868     var fixture = Fixture{};
869     const element = try fixture.only("p::before");
870     try std.testing.expect(element.compounds[0].flags & unmatchable != 0);
871     try std.testing.expectEqualStrings("selector-pseudo-element", fixture.diagnostics[0]);
872     var second = Fixture{};
873     const unsupported = try second.only("p:not(.a)");
874     try std.testing.expect(unsupported.compounds[0].flags & unmatchable != 0);
875     try std.testing.expectEqualStrings("selector-unsupported-pseudo", second.diagnostics[0]);
876 }
877 
878 test "counting and filling produce identical totals" {
879     const source = "#a .b > button:hover, [role=tab]:nth-child(2n), *";
880     var fixture = Fixture{};
881     const range = try fixture.parse(source);
882     var counter = Builder{ .source = source };
883     const counted = try parseList(&counter, 0, @intCast(source.len));
884     try std.testing.expectEqual(range.count, counted.count);
885     try std.testing.expectEqual(fixture.builder.compound_used, counter.compound_used);
886     try std.testing.expectEqual(fixture.builder.combinator_used, counter.combinator_used);
887     try std.testing.expectEqual(fixture.builder.class_used, counter.class_used);
888     try std.testing.expectEqual(fixture.builder.nth_used, counter.nth_used);
889     try std.testing.expectEqual(fixture.builder.dependency_used, counter.dependency_used);
890     try std.testing.expectEqual(fixture.builder.diagnostic_used, counter.diagnostic_used);
891     try std.testing.expect(counter.atom_used >= fixture.table.count);
892 }
893 
894 test "an escaped identifier interns its decoded text" {
895     var fixture = Fixture{};
896     const selector = try fixture.only(".\\34 two");
897     const classes = fixture.classes[selector.compounds[0].class_first..][0..1];
898     try std.testing.expectEqualStrings("4two", fixture.table.name(classes[0]));
899 }
900 
901 test "a trailing combinator makes the selector unmatchable" {
902     var fixture = Fixture{};
903     const selector = try fixture.only("a >");
904     try std.testing.expectEqual(@as(usize, 1), selector.compounds.len);
905     try std.testing.expect(selector.compounds[0].flags & unmatchable != 0);
906 }