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 }