lib/css/src/sheet.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 //! The stylesheet: rule parsing, two phase storage, and the tables matching and
2 //! the cascade read.
3 //!
4 //! One inspection pass counts every array a sheet needs and one emission pass
5 //! fills them. Both passes run the same code over the same `Sink`, so the
6 //! counted bound and the filled result cannot drift.
7 //!
8 //! A rule keeps two views of its block. `declarations` holds the authored
9 //! property and value text, which resource discovery and serialization read.
10 //! `lowered` holds the typed sixteen byte records the cascade reads, with
11 //! shorthands already expanded and custom properties split into `customs`.
12 //! Nothing recomputes the lowering per element.
13 //!
14 //! Identifiers are interned into one table per sheet, so a selector compound
15 //! compares integers. A consumer resolves its own element type and role tags
16 //! once through `token`, never per match.
17
18 const std = @import("std");
19 const alloc_phase = @import("alloc_phase");
20
21 const atom = @import("atom.zig");
22 const cascade = @import("cascade/root.zig");
23 const media_queries = @import("media.zig");
24 const property = @import("property/root.zig");
25 const matching = @import("match/root.zig");
26 const scan = @import("scan.zig");
27 const selector = @import("selector/root.zig");
28 const value = @import("value/root.zig");
29
30 const Allocator = std.mem.Allocator;
31
32 pub const MediaEnvironment = media_queries.MediaEnvironment;
33 pub const MediaType = media_queries.MediaType;
34
35 /// One authored declaration, borrowed from the sheet source.
36 pub const Declaration = struct {
37 property: []const u8,
38 value: []const u8,
39 important: bool = false,
40 };
41
42 /// One style rule. The selector list, the authored declarations, and the
43 /// lowered records all borrow from the sheet placement.
44 pub const Rule = struct {
45 selectors: []const selector.Selector = &.{},
46 source_order: usize = 0,
47 declarations: []const Declaration = &.{},
48 lowered: []const value.Declaration = &.{},
49 customs: []const cascade.Custom = &.{},
50
51 /// The first selector, which every rule has and which carries the raw
52 /// prelude text an at-rule descriptor block keeps.
53 pub fn head(self: Rule) selector.Selector {
54 std.debug.assert(self.selectors.len > 0);
55 return self.selectors[0];
56 }
57 };
58
59 /// A parsed stylesheet. Every slice borrows from one placement and from the
60 /// caller owned source bytes.
61 pub const StyleSheet = struct {
62 source: []const u8 = &.{},
63 rules: []const Rule = &.{},
64 diagnostics: []const []const u8 = &.{},
65 atoms: atom.Table = .{ .records = &.{}, .slots = &.{}, .arena = &.{} },
66 classes: []const u32 = &.{},
67 nths: []const selector.nth.Nth = &.{},
68
69 /// The atom this sheet gave `text`, or `atom.none` when no selector in the
70 /// sheet named it. A consumer resolves its element tags through this once.
71 pub fn token(self: *const StyleSheet, text: []const u8) u32 {
72 return self.atoms.lookup(text);
73 }
74
75 /// The text behind an atom this sheet interned.
76 pub fn name(self: *const StyleSheet, id: u32) []const u8 {
77 return self.atoms.name(id);
78 }
79
80 /// The matching context the selector engine reads for this sheet.
81 pub fn context(self: *const StyleSheet) matching.Context {
82 return .{ .classes = self.classes, .nths = self.nths };
83 }
84 };
85
86 const Array = struct {
87 name: []const u8,
88 Element: type,
89 };
90
91 /// The placement arrays a sheet owns, in the order `Capacity` lays them out.
92 const arrays = [_]Array{
93 .{ .name = "rules", .Element = Rule },
94 .{ .name = "selectors", .Element = selector.Selector },
95 .{ .name = "compounds", .Element = selector.Compound },
96 .{ .name = "combinators", .Element = selector.Combinator },
97 .{ .name = "classes", .Element = u32 },
98 .{ .name = "nths", .Element = selector.nth.Nth },
99 .{ .name = "atoms", .Element = atom.Record },
100 .{ .name = "atom_slots", .Element = u32 },
101 .{ .name = "atom_bytes", .Element = u8 },
102 .{ .name = "declarations", .Element = Declaration },
103 .{ .name = "lowered", .Element = value.Declaration },
104 .{ .name = "customs", .Element = cascade.Custom },
105 .{ .name = "diagnostics", .Element = []const u8 },
106 };
107
108 /// The number of placement arrays one sheet holds.
109 pub const array_count: usize = arrays.len;
110
111 /// The alignment the whole sheet block is acquired at.
112 pub const storage_alignment: usize = blk: {
113 var wanted: usize = 1;
114 for (arrays) |entry| wanted = @max(wanted, @alignOf(entry.Element));
115 break :blk wanted;
116 };
117
118 /// The exact bound one source and one media environment produce.
119 pub const Limits = struct {
120 rules: usize = 0,
121 selectors: usize = 0,
122 compounds: usize = 0,
123 combinators: usize = 0,
124 classes: usize = 0,
125 nths: usize = 0,
126 atoms: usize = 0,
127 atom_slots: usize = 0,
128 atom_bytes: usize = 0,
129 declarations: usize = 0,
130 lowered: usize = 0,
131 customs: usize = 0,
132 diagnostics: usize = 0,
133 selector_dependencies: usize = 0,
134 max_nesting: usize = 0,
135
136 /// Counts every array without writing one, which is the pass a caller runs
137 /// before it acquires storage.
138 pub fn inspect(source: []const u8, media: MediaEnvironment) error{CapacityOverflow}!Limits {
139 var sink = Sink{ .media = media, .builder = .{ .source = source } };
140 try parseRules(&sink, 0, cast(source.len), 1);
141 return sink.limits();
142 }
143
144 /// The bound that admits both operands, which is how a workspace grows.
145 pub fn grow(left: Limits, right: Limits) Limits {
146 var out: Limits = .{};
147 inline for (comptime std.meta.fieldNames(Limits)) |field| {
148 @field(out, field) = @max(@field(left, field), @field(right, field));
149 }
150 return out;
151 }
152
153 /// Whether `self` fits inside `capacity` field by field.
154 pub fn fits(self: Limits, capacity: Limits) bool {
155 inline for (comptime std.meta.fieldNames(Limits)) |field| {
156 if (@field(self, field) > @field(capacity, field)) return false;
157 }
158 return true;
159 }
160 };
161
162 /// The aligned byte layout one `Limits` produces.
163 pub const Capacity = struct {
164 limits: Limits = .{},
165 offsets: [array_count]usize = @splat(0),
166 total_bytes: usize = 0,
167
168 /// Lays the arrays out in declaration order, failing closed on overflow.
169 pub fn derive(limits: Limits) error{CapacityOverflow}!Capacity {
170 var out = Capacity{ .limits = limits };
171 var cursor: usize = 0;
172 inline for (arrays, 0..) |entry, index| {
173 const region = try placed(entry.Element, cursor, @field(limits, entry.name));
174 out.offsets[index] = region.start;
175 cursor = region.end;
176 }
177 out.total_bytes = cursor;
178 return out;
179 }
180 };
181
182 const LimitsType = Limits;
183 const CapacityType = Capacity;
184
185 /// The typed slices one acquired block resolves into.
186 pub const Placement = struct {
187 capacity: Capacity = .{},
188 rules: []Rule = &.{},
189 selectors: []selector.Selector = &.{},
190 compounds: []selector.Compound = &.{},
191 combinators: []selector.Combinator = &.{},
192 classes: []u32 = &.{},
193 nths: []selector.nth.Nth = &.{},
194 atoms: []atom.Record = &.{},
195 atom_slots: []u32 = &.{},
196 atom_bytes: []u8 = &.{},
197 declarations: []Declaration = &.{},
198 lowered: []value.Declaration = &.{},
199 customs: []cascade.Custom = &.{},
200 diagnostics: [][]const u8 = &.{},
201
202 pub fn init(bytes: []u8, capacity: Capacity) Placement {
203 std.debug.assert(bytes.len == capacity.total_bytes);
204 var out = Placement{ .capacity = capacity };
205 inline for (arrays, 0..) |entry, index| {
206 @field(out, entry.name) = typedSlice(
207 entry.Element,
208 bytes,
209 capacity.offsets[index],
210 @field(capacity.limits, entry.name),
211 );
212 }
213 return out;
214 }
215
216 pub fn admits(self: Placement, limits: Limits) bool {
217 return limits.fits(self.capacity.limits);
218 }
219
220 fn release(self: *Placement) void {
221 inline for (arrays) |entry| @field(self, entry.name) = &.{};
222 }
223 };
224
225 /// The single aligned block one sheet owns for the whole of its life.
226 pub const Storage = struct {
227 pub const Limits = LimitsType;
228 pub const Capacity = CapacityType;
229
230 pub const claim: alloc_phase.capacity.Declaration = .{
231 .source = .{
232 .id = "css.storage",
233 .kind = .phase_static,
234 .limit_source = .caller,
235 .storage = .{
236 .covered = &.{
237 .{
238 .id = "flat_active_css_rule_output",
239 .lifetime = .steady,
240 .detail = "flat active CSS rule output",
241 },
242 .{
243 .id = "flat_selector_output_shared_by_rule_slices",
244 .lifetime = .steady,
245 .detail = "flat selector, compound, combinator, class, and nth output shared by rule slices",
246 },
247 .{
248 .id = "flat_identifier_interning_table",
249 .lifetime = .steady,
250 .detail = "flat identifier interning records, probe slots, and folded text arena",
251 },
252 .{
253 .id = "flat_declaration_output_shared_by_rule_slices",
254 .lifetime = .steady,
255 .detail = "flat authored, lowered, and custom declaration output shared by rule slices",
256 },
257 .{
258 .id = "flat_parser_diagnostic_output",
259 .lifetime = .steady,
260 .detail = "flat parser diagnostic output",
261 },
262 },
263 .excluded = &.{
264 "caller-owned CSS source bytes borrowed by selectors and declarations",
265 "author source discovery and invalidation dependency storage",
266 "caller-owned cascade resolver and computed style records",
267 },
268 },
269 .capacity = .{
270 .inputs = &.{
271 alloc_phase.capacity.bindInput(LimitsType, "rules", "rules"),
272 alloc_phase.capacity.bindInput(LimitsType, "selectors", "selectors"),
273 alloc_phase.capacity.bindInput(LimitsType, "compounds", "compounds"),
274 alloc_phase.capacity.bindInput(LimitsType, "combinators", "combinators"),
275 alloc_phase.capacity.bindInput(LimitsType, "classes", "classes"),
276 alloc_phase.capacity.bindInput(LimitsType, "nths", "nths"),
277 alloc_phase.capacity.bindInput(LimitsType, "atoms", "atoms"),
278 alloc_phase.capacity.bindInput(LimitsType, "atom_slots", "atom_slots"),
279 alloc_phase.capacity.bindInput(LimitsType, "atom_bytes", "atom_bytes"),
280 alloc_phase.capacity.bindInput(LimitsType, "declarations", "declarations"),
281 alloc_phase.capacity.bindInput(LimitsType, "lowered", "lowered"),
282 alloc_phase.capacity.bindInput(LimitsType, "customs", "customs"),
283 alloc_phase.capacity.bindInput(LimitsType, "diagnostics", "diagnostics"),
284 },
285 .type_selectors = &.{
286 alloc_phase.capacity.bindType(Rule, "rule"),
287 alloc_phase.capacity.bindType(selector.Selector, "selector"),
288 alloc_phase.capacity.bindType(selector.Compound, "compound"),
289 alloc_phase.capacity.bindType(selector.Combinator, "combinator"),
290 alloc_phase.capacity.bindType(u32, "u32"),
291 alloc_phase.capacity.bindType(selector.nth.Nth, "nth"),
292 alloc_phase.capacity.bindType(atom.Record, "atom_record"),
293 alloc_phase.capacity.bindType(u8, "u8"),
294 alloc_phase.capacity.bindType(Declaration, "declaration"),
295 alloc_phase.capacity.bindType(value.Declaration, "lowered_declaration"),
296 alloc_phase.capacity.bindType(cascade.Custom, "custom"),
297 alloc_phase.capacity.bindType([]const u8, "const_u8"),
298 },
299 .nodes = &.{
300 .{ .input = 0 },
301 .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 0 } } },
302 .{ .input = 1 },
303 .{ .scale = .{ .node = 2, .coefficient = .{ .size_of_concrete_type = 1 } } },
304 .{ .input = 2 },
305 .{ .scale = .{ .node = 4, .coefficient = .{ .size_of_concrete_type = 2 } } },
306 .{ .input = 3 },
307 .{ .scale = .{ .node = 6, .coefficient = .{ .size_of_concrete_type = 3 } } },
308 .{ .input = 4 },
309 .{ .scale = .{ .node = 8, .coefficient = .{ .size_of_concrete_type = 4 } } },
310 .{ .input = 5 },
311 .{ .scale = .{ .node = 10, .coefficient = .{ .size_of_concrete_type = 5 } } },
312 .{ .input = 6 },
313 .{ .scale = .{ .node = 12, .coefficient = .{ .size_of_concrete_type = 6 } } },
314 .{ .input = 7 },
315 .{ .scale = .{ .node = 14, .coefficient = .{ .size_of_concrete_type = 4 } } },
316 .{ .input = 8 },
317 .{ .scale = .{ .node = 16, .coefficient = .{ .size_of_concrete_type = 7 } } },
318 .{ .input = 9 },
319 .{ .scale = .{ .node = 18, .coefficient = .{ .size_of_concrete_type = 8 } } },
320 .{ .input = 10 },
321 .{ .scale = .{ .node = 20, .coefficient = .{ .size_of_concrete_type = 9 } } },
322 .{ .input = 11 },
323 .{ .scale = .{ .node = 22, .coefficient = .{ .size_of_concrete_type = 10 } } },
324 .{ .input = 12 },
325 .{ .scale = .{ .node = 24, .coefficient = .{ .size_of_concrete_type = 11 } } },
326 .{ .constant = 0 },
327 .{ .alignment = .{ .node = 26, .alignment = .{ .concrete_type = 0 } } },
328 .{ .add = .{ .left = 27, .right = 1 } },
329 .{ .alignment = .{ .node = 28, .alignment = .{ .concrete_type = 1 } } },
330 .{ .add = .{ .left = 29, .right = 3 } },
331 .{ .alignment = .{ .node = 30, .alignment = .{ .concrete_type = 2 } } },
332 .{ .add = .{ .left = 31, .right = 5 } },
333 .{ .alignment = .{ .node = 32, .alignment = .{ .concrete_type = 3 } } },
334 .{ .add = .{ .left = 33, .right = 7 } },
335 .{ .alignment = .{ .node = 34, .alignment = .{ .concrete_type = 4 } } },
336 .{ .add = .{ .left = 35, .right = 9 } },
337 .{ .alignment = .{ .node = 36, .alignment = .{ .concrete_type = 5 } } },
338 .{ .add = .{ .left = 37, .right = 11 } },
339 .{ .alignment = .{ .node = 38, .alignment = .{ .concrete_type = 6 } } },
340 .{ .add = .{ .left = 39, .right = 13 } },
341 .{ .alignment = .{ .node = 40, .alignment = .{ .concrete_type = 4 } } },
342 .{ .add = .{ .left = 41, .right = 15 } },
343 .{ .alignment = .{ .node = 42, .alignment = .{ .concrete_type = 7 } } },
344 .{ .add = .{ .left = 43, .right = 17 } },
345 .{ .alignment = .{ .node = 44, .alignment = .{ .concrete_type = 8 } } },
346 .{ .add = .{ .left = 45, .right = 19 } },
347 .{ .alignment = .{ .node = 46, .alignment = .{ .concrete_type = 9 } } },
348 .{ .add = .{ .left = 47, .right = 21 } },
349 .{ .alignment = .{ .node = 48, .alignment = .{ .concrete_type = 10 } } },
350 .{ .add = .{ .left = 49, .right = 23 } },
351 .{ .alignment = .{ .node = 50, .alignment = .{ .concrete_type = 11 } } },
352 .{ .add = .{ .left = 51, .right = 25 } },
353 .{ .alignment = .{ .node = 52, .alignment = .{ .literal = 16 } } },
354 },
355 .assertions = &.{.{
356 .scope = .closure_total,
357 .measure = .retained,
358 .relation = .exact,
359 .expression = 53,
360 }},
361 },
362 .overload = .{
363 .kind = .reject_before_seal,
364 .detail = "a count-only parse and checked aligned capacity reject before acquisition; filling the admitted flat slices performs no allocation",
365 },
366 .risks = .{
367 .transitive = .{
368 .status = .open,
369 .detail = "the parser helpers are allocation-free and witnessed but lack a machine-checked call-graph closure certificate",
370 },
371 .foreign = .{
372 .status = .excluded,
373 .detail = "CSS parsing is a process-local transformation with no callback or operating-system edge",
374 },
375 },
376 .obligations = &.{
377 .{ .key = "css_capacity_capacity_model", .role = .capacity_model },
378 .{ .key = "css_capacity_overload", .role = .overload },
379 .{ .key = "css_acquisition", .role = .custom },
380 .{ .key = "css_steady_overload", .role = .overload },
381 .{ .key = "css_steady_foreign_risk", .role = .foreign_risk },
382 },
383 },
384 .bindings = .{
385 .owner = @This(),
386 .seal = .{
387 .family = alloc_phase.capacity.selector(@This().activate),
388 .premise = .{
389 .class = .checked_semantic_fact,
390 .authority = .checker,
391 },
392 },
393 .teardown = .{
394 .family = alloc_phase.capacity.selector(@This().deinit),
395 .premise = .{
396 .class = .checked_semantic_fact,
397 .authority = .checker,
398 },
399 },
400 },
401 };
402
403 phase: alloc_phase.capacity.Phase,
404 limits: LimitsType,
405 capacity: CapacityType,
406 bytes: []align(storage_alignment) u8,
407 placement: Placement,
408
409 pub fn init(allocator: Allocator, limits: LimitsType) !Storage {
410 const capacity = try CapacityType.derive(limits);
411 const bytes = try allocator.alignedAlloc(u8, .fromByteUnits(storage_alignment), capacity.total_bytes);
412 return .{
413 .phase = .initialization,
414 .limits = limits,
415 .capacity = capacity,
416 .bytes = bytes,
417 .placement = Placement.init(bytes, capacity),
418 };
419 }
420
421 pub fn activate(self: *Storage) void {
422 std.debug.assert(self.phase == .initialization);
423 self.assertStorage();
424 self.phase = .steady;
425 }
426
427 pub fn admits(self: *const Storage, limits: LimitsType) bool {
428 std.debug.assert(self.phase == .steady);
429 return self.placement.admits(limits);
430 }
431
432 pub fn parse(self: *Storage, source: []const u8, media: MediaEnvironment) StyleSheet {
433 std.debug.assert(self.phase == .steady);
434 const limits = LimitsType.inspect(source, media) catch unreachable;
435 std.debug.assert(self.admits(limits));
436 return build(self.placement, source, media, limits);
437 }
438
439 pub fn deinit(self: *Storage, allocator: Allocator) void {
440 std.debug.assert(self.phase != .teardown);
441 self.assertStorage();
442 self.phase = .teardown;
443 allocator.free(self.bytes);
444 self.bytes = &.{};
445 self.placement.release();
446 }
447
448 fn assertStorage(self: *const Storage) void {
449 const expected = CapacityType.derive(self.limits) catch unreachable;
450 std.debug.assert(std.meta.eql(expected, self.capacity));
451 std.debug.assert(self.bytes.len == self.capacity.total_bytes);
452 inline for (arrays) |entry| {
453 std.debug.assert(@field(self.placement, entry.name).len ==
454 @field(self.capacity.limits, entry.name));
455 }
456 }
457 };
458
459 comptime {
460 alloc_phase.capacity.requireAllocatorExactOwnerShape(Storage);
461 }
462
463 /// A sheet parser that grows its storage to the largest source it has seen.
464 /// Every previously returned `StyleSheet` is invalidated by the next parse.
465 pub const Workspace = struct {
466 allocator: Allocator,
467 storage: ?Storage = null,
468
469 pub fn init(allocator: Allocator) Workspace {
470 return .{ .allocator = allocator };
471 }
472
473 pub fn deinit(self: *Workspace) void {
474 if (self.storage) |*storage| storage.deinit(self.allocator);
475 self.* = undefined;
476 }
477
478 pub fn parse(self: *Workspace, source: []const u8, media: MediaEnvironment) !StyleSheet {
479 const limits = try Limits.inspect(source, media);
480 const storage = try self.ensureStorage(limits);
481 return storage.parse(source, media);
482 }
483
484 fn ensureStorage(self: *Workspace, limits: Limits) !*Storage {
485 if (self.storage) |*storage| {
486 if (storage.admits(limits)) return storage;
487 }
488 const grown = if (self.storage) |storage| Limits.grow(storage.limits, limits) else limits;
489 var next = try Storage.init(self.allocator, grown);
490 next.activate();
491 if (self.storage) |*storage| storage.deinit(self.allocator);
492 self.storage = next;
493 return &self.storage.?;
494 }
495 };
496
497 /// The largest source one sheet parses, bounded by the `u32` spans selectors
498 /// and lowered values carry.
499 pub const max_source_bytes: usize = std.math.maxInt(u32);
500
501 const Span = struct {
502 start: u32,
503 end: u32,
504 };
505
506 const Cut = struct {
507 start: u32,
508 end: u32,
509 important: bool = false,
510 };
511
512 const Sink = struct {
513 builder: selector.Builder,
514 media: MediaEnvironment,
515 placement: ?Placement = null,
516 rules: u32 = 0,
517 declarations: u32 = 0,
518 lowered: u32 = 0,
519 customs: u32 = 0,
520 max_nesting: usize = 0,
521
522 fn enter(self: *Sink, depth: usize) void {
523 self.max_nesting = @max(self.max_nesting, depth);
524 }
525
526 fn diagnostic(self: *Sink, text: []const u8) error{CapacityOverflow}!void {
527 try self.builder.note(text);
528 }
529
530 fn rule(self: *Sink, prelude: Span, body: Span, descriptor: bool) error{CapacityOverflow}!void {
531 const trimmed = trimSpan(self.builder.source, prelude.start, prelude.end);
532 if (trimmed.end == trimmed.start) return;
533 const list = if (descriptor)
534 try selector.parseOpaque(&self.builder, trimmed.start, trimmed.end)
535 else
536 try selector.parseList(&self.builder, trimmed.start, trimmed.end);
537 if (list.count == 0) return;
538 const first_declaration = self.declarations;
539 const first_lowered = self.lowered;
540 const first_custom = self.customs;
541 try scanDeclarations(self, body.start, body.end);
542 const index = self.rules;
543 self.rules = try bump(self.rules, 1);
544 const placement = self.placement orelse return;
545 std.debug.assert(index < placement.rules.len);
546 placement.rules[index] = .{
547 .selectors = placement.selectors[list.first..][0..list.count],
548 .source_order = index,
549 .declarations = placement.declarations[first_declaration..self.declarations],
550 .lowered = placement.lowered[first_lowered..self.lowered],
551 .customs = placement.customs[first_custom..self.customs],
552 };
553 }
554
555 fn declaration(self: *Sink, name_span: Span, value_span: Span) error{CapacityOverflow}!void {
556 const source = self.builder.source;
557 const name = trimSpan(source, name_span.start, name_span.end);
558 const cut = importantCut(source, value_span.start, value_span.end);
559 const index = self.declarations;
560 self.declarations = try bump(self.declarations, 1);
561 if (self.placement) |placement| {
562 std.debug.assert(index < placement.declarations.len);
563 placement.declarations[index] = .{
564 .property = source[name.start..name.end],
565 .value = source[cut.start..cut.end],
566 .important = cut.important,
567 };
568 }
569 try self.lower(source[name.start..name.end], cut);
570 }
571
572 fn lower(self: *Sink, name: []const u8, cut: Cut) error{CapacityOverflow}!void {
573 if (name.len == 0) return;
574 if (property.isCustom(name)) {
575 const id = try self.builder.intern(name, false);
576 if (id == atom.none) return;
577 try self.pushCustom(.{
578 .name = id,
579 .start = cut.start,
580 .end = cut.end,
581 .important = cut.important,
582 });
583 return;
584 }
585 if (property.lookup(name)) |id| {
586 const item = self.typed(property.metadata(id).grammar, cut) orelse return;
587 try self.pushLowered(item.declare(@backingInt(id), cut.important));
588 return;
589 }
590 try self.lowerShorthand(name, cut);
591 }
592
593 fn lowerShorthand(self: *Sink, name: []const u8, cut: Cut) error{CapacityOverflow}!void {
594 const kind = property.shorthandOf(name) orelse return;
595 if (self.deferred(cut)) |item| {
596 for (property.longhands(kind)) |id| {
597 try self.pushLowered(item.declare(@backingInt(id), cut.important));
598 }
599 return;
600 }
601 var out: [property.max_expansions]property.Expansion = undefined;
602 const produced = property.expand(kind, self.builder.source, cut.start, cut.end, &out);
603 for (out[0..produced]) |item| {
604 try self.pushLowered(item.value.declare(@backingInt(item.id), cut.important));
605 }
606 }
607
608 fn typed(self: *Sink, grammar: value.Grammar, cut: Cut) ?value.Value {
609 if (self.deferred(cut)) |item| return item;
610 const parsed = value.parse(grammar, self.builder.source, cut.start, cut.end);
611 if (!parsed.present()) return null;
612 return parsed;
613 }
614
615 fn deferred(self: *Sink, cut: Cut) ?value.Value {
616 if (!value.hasVariable(self.builder.source, cut.start, cut.end)) return null;
617 return .{ .flags = cascade.flag_pending, .a = cut.start, .b = cut.end - cut.start };
618 }
619
620 fn pushLowered(self: *Sink, item: value.Declaration) error{CapacityOverflow}!void {
621 const index = self.lowered;
622 self.lowered = try bump(self.lowered, 1);
623 const placement = self.placement orelse return;
624 std.debug.assert(index < placement.lowered.len);
625 placement.lowered[index] = item;
626 }
627
628 fn pushCustom(self: *Sink, item: cascade.Custom) error{CapacityOverflow}!void {
629 const index = self.customs;
630 self.customs = try bump(self.customs, 1);
631 const placement = self.placement orelse return;
632 std.debug.assert(index < placement.customs.len);
633 placement.customs[index] = item;
634 }
635
636 fn limits(self: Sink) Limits {
637 return .{
638 .rules = self.rules,
639 .selectors = self.builder.selector_used,
640 .compounds = self.builder.compound_used,
641 .combinators = self.builder.combinator_used,
642 .classes = self.builder.class_used,
643 .nths = self.builder.nth_used,
644 .atoms = self.builder.atom_used,
645 .atom_slots = atom.slotCount(self.builder.atom_used),
646 .atom_bytes = self.builder.atom_bytes,
647 .declarations = self.declarations,
648 .lowered = self.lowered,
649 .customs = self.customs,
650 .diagnostics = self.builder.diagnostic_used,
651 .selector_dependencies = self.builder.dependency_used,
652 .max_nesting = self.max_nesting,
653 };
654 }
655 };
656
657 /// Fills an admitted placement from `source` and publishes the sheet.
658 pub fn build(placement: Placement, source: []const u8, media: MediaEnvironment, limits: Limits) StyleSheet {
659 std.debug.assert(placement.admits(limits));
660 std.debug.assert(source.len <= max_source_bytes);
661 var table = atom.Table{
662 .records = placement.atoms,
663 .slots = placement.atom_slots,
664 .arena = placement.atom_bytes,
665 };
666 table.reset();
667 var sink = Sink{ .media = media, .placement = placement, .builder = .{
668 .source = source,
669 .atoms = &table,
670 .classes = placement.classes,
671 .compounds = placement.compounds,
672 .combinators = placement.combinators,
673 .nths = placement.nths,
674 .selectors = placement.selectors,
675 .diagnostics = placement.diagnostics,
676 } };
677 parseRules(&sink, 0, cast(source.len), 1) catch unreachable;
678 std.debug.assert(sink.rules == limits.rules);
679 std.debug.assert(sink.declarations == limits.declarations);
680 std.debug.assert(sink.lowered == limits.lowered);
681 std.debug.assert(sink.customs == limits.customs);
682 std.debug.assert(sink.builder.selector_used == limits.selectors);
683 std.debug.assert(sink.builder.diagnostic_used == limits.diagnostics);
684 return .{
685 .source = source,
686 .rules = placement.rules[0..sink.rules],
687 .diagnostics = placement.diagnostics[0..sink.builder.diagnostic_used],
688 .atoms = table,
689 .classes = placement.classes[0..sink.builder.class_used],
690 .nths = placement.nths[0..sink.builder.nth_used],
691 };
692 }
693
694 fn parseRules(sink: *Sink, start: u32, end: u32, depth: usize) error{CapacityOverflow}!void {
695 if (sink.builder.source.len > max_source_bytes) return error.CapacityOverflow;
696 sink.enter(depth);
697 const source = sink.builder.source[0..end];
698 var index: usize = start;
699 var guard: usize = 0;
700 while (index < end and guard <= source.len + 1) : (guard += 1) {
701 index = scan.skipSpaceAndComments(source, index);
702 if (index >= end) break;
703 if (source[index] == '@') {
704 index = try parseAtRule(sink, index, end, depth);
705 continue;
706 }
707 const open = scan.findTopLevelByte(source, index, '{') orelse break;
708 const close = scan.findBlockEnd(source, open) orelse {
709 try sink.diagnostic("missing-close-brace");
710 break;
711 };
712 try sink.rule(
713 .{ .start = cast(index), .end = cast(open) },
714 .{ .start = cast(open + 1), .end = cast(close) },
715 false,
716 );
717 index = close + 1;
718 }
719 }
720
721 fn parseAtRule(sink: *Sink, start: usize, end: u32, depth: usize) error{CapacityOverflow}!usize {
722 const source = sink.builder.source[0..end];
723 const boundary = scan.findAtRuleBoundary(source, start) orelse return end;
724 if (boundary.kind == .semicolon) return boundary.index + 1;
725 const close = scan.findBlockEnd(source, boundary.index) orelse {
726 try sink.diagnostic("missing-close-brace");
727 return end;
728 };
729 const prelude = Span{ .start = cast(start), .end = cast(boundary.index) };
730 const body = Span{ .start = cast(boundary.index + 1), .end = cast(close) };
731 if (descriptorAtRule(source, start + 1, boundary.index, "font-face")) {
732 try sink.rule(prelude, body, true);
733 } else if (media_queries.mediaApplies(source[start + 1 .. boundary.index], sink.media)) {
734 const nested = std.math.add(usize, depth, 1) catch return error.CapacityOverflow;
735 try parseRules(sink, body.start, body.end, nested);
736 }
737 return close + 1;
738 }
739
740 fn scanDeclarations(sink: *Sink, start: u32, end: u32) error{CapacityOverflow}!void {
741 const source = sink.builder.source[0..end];
742 var index: usize = start;
743 var guard: usize = 0;
744 while (index < end and guard <= source.len + 1) : (guard += 1) {
745 const stop = scan.findDeclarationEnd(source, index);
746 const item = trimSpan(source, cast(index), cast(stop));
747 index = if (stop < end) stop + 1 else end;
748 if (item.end == item.start) continue;
749 const colon = scan.findTopLevelByte(source[0..item.end], item.start, ':') orelse {
750 try sink.diagnostic("missing-declaration-colon");
751 continue;
752 };
753 try sink.declaration(
754 .{ .start = item.start, .end = cast(colon) },
755 .{ .start = cast(colon + 1), .end = item.end },
756 );
757 }
758 }
759
760 fn descriptorAtRule(source: []const u8, start: usize, end: usize, name: []const u8) bool {
761 const trimmed = trimSpan(source, cast(start), cast(end));
762 const word_end = scan.readIdent(source[0..trimmed.end], trimmed.start);
763 if (word_end == trimmed.start) return false;
764 return std.ascii.eqlIgnoreCase(source[trimmed.start..word_end], name);
765 }
766
767 /// Splits an authored value into its body and its `!important` flag. The
768 /// returned `property` is empty because the caller already holds the name.
769 pub fn parseDeclarationValue(text: []const u8) Declaration {
770 std.debug.assert(text.len <= max_source_bytes);
771 const cut = importantCut(text, 0, cast(text.len));
772 return .{
773 .property = "",
774 .value = text[cut.start..cut.end],
775 .important = cut.important,
776 };
777 }
778
779 /// Whether any query in `query_list` selects `media`.
780 pub fn mediaListApplies(query_list: []const u8, media: MediaEnvironment) bool {
781 return media_queries.mediaListApplies(query_list, media);
782 }
783
784 fn importantCut(source: []const u8, start: u32, end: u32) Cut {
785 const marker = "!important";
786 const trimmed = trimSpan(source, start, end);
787 const text = source[trimmed.start..trimmed.end];
788 const found = std.mem.lastIndexOf(u8, text, marker) orelse
789 return .{ .start = trimmed.start, .end = trimmed.end };
790 const after = trimmed.start + cast(found + marker.len);
791 const tail = trimSpan(source, after, trimmed.end);
792 if (tail.end > tail.start) return .{ .start = trimmed.start, .end = trimmed.end };
793 const body = trimSpan(source, trimmed.start, trimmed.start + cast(found));
794 return .{ .start = body.start, .end = body.end, .important = true };
795 }
796
797 fn trimSpan(source: []const u8, start: u32, end: u32) Span {
798 std.debug.assert(start <= end);
799 std.debug.assert(end <= source.len);
800 var low = start;
801 var high = end;
802 while (low < high and isSpace(source[low])) low += 1;
803 while (high > low and isSpace(source[high - 1])) high -= 1;
804 return .{ .start = low, .end = high };
805 }
806
807 fn isSpace(byte: u8) bool {
808 return byte == ' ' or byte == '\t' or byte == '\r' or byte == '\n' or byte == 0x0C;
809 }
810
811 fn bump(current: u32, amount: u32) error{CapacityOverflow}!u32 {
812 return std.math.add(u32, current, amount) catch error.CapacityOverflow;
813 }
814
815 fn cast(count: usize) u32 {
816 return @intCast(count);
817 }
818
819 const Region = struct {
820 start: usize,
821 end: usize,
822 };
823
824 fn placed(comptime T: type, offset: usize, count: usize) error{CapacityOverflow}!Region {
825 const padded = std.math.add(usize, offset, @alignOf(T) - 1) catch return error.CapacityOverflow;
826 const mask: usize = @alignOf(T) - 1;
827 const start = padded & ~mask;
828 const bytes = std.math.mul(usize, count, @sizeOf(T)) catch return error.CapacityOverflow;
829 return .{
830 .start = start,
831 .end = std.math.add(usize, start, bytes) catch return error.CapacityOverflow,
832 };
833 }
834
835 fn typedSlice(comptime T: type, bytes: []u8, offset: usize, count: usize) []T {
836 if (count == 0) return &.{};
837 const byte_count = count * @sizeOf(T);
838 const region: []align(@alignOf(T)) u8 = @alignCast(bytes[offset..][0..byte_count]);
839 return std.mem.bytesAsSlice(T, region);
840 }
841
842 const TestSheet = struct {
843 workspace: Workspace,
844 sheet: StyleSheet,
845
846 fn init(source: []const u8, media: MediaEnvironment) !TestSheet {
847 var workspace = Workspace.init(std.testing.allocator);
848 errdefer workspace.deinit();
849 const parsed = try workspace.parse(source, media);
850 return .{ .workspace = workspace, .sheet = parsed };
851 }
852
853 fn deinit(self: *TestSheet) void {
854 self.workspace.deinit();
855 }
856
857 fn raw(self: TestSheet, index: usize) []const u8 {
858 return self.sheet.rules[index].head().raw;
859 }
860 };
861
862 fn modelStart(comptime T: type, offset: u128) u128 {
863 const alignment: u128 = @alignOf(T);
864 return (offset + alignment - 1) & ~(alignment - 1);
865 }
866
867 fn modelCapacity(limits: Limits) error{CapacityOverflow}!Capacity {
868 var out = Capacity{ .limits = limits };
869 var cursor: u128 = 0;
870 inline for (arrays, 0..) |entry, index| {
871 const start = modelStart(entry.Element, cursor);
872 if (start > std.math.maxInt(usize)) return error.CapacityOverflow;
873 out.offsets[index] = @intCast(start);
874 cursor = start + @as(u128, @field(limits, entry.name)) * @sizeOf(entry.Element);
875 }
876 if (cursor > std.math.maxInt(usize)) return error.CapacityOverflow;
877 out.total_bytes = @intCast(cursor);
878 return out;
879 }
880
881 fn checkCssStorageInit(allocator: Allocator, limits: Limits) !void {
882 var storage = try Storage.init(allocator, limits);
883 defer storage.deinit(allocator);
884 try std.testing.expectEqual(alloc_phase.capacity.Phase.initialization, storage.phase);
885 }
886
887 test "CSS storage capacity matches an independent aligned model" {
888 comptime {
889 alloc_phase.capacity.record(
890 alloc_phase.capacity.witness(Storage, "css_capacity_capacity_model"),
891 );
892 }
893 comptime {
894 alloc_phase.capacity.record(alloc_phase.capacity.witness(Storage, "css_capacity_overload"));
895 }
896
897 const cases = [_]Limits{
898 .{},
899 .{ .rules = 1, .selectors = 1, .compounds = 2, .combinators = 1, .classes = 1, .nths = 1, .atoms = 3, .atom_slots = 8, .atom_bytes = 7, .declarations = 2, .lowered = 5, .customs = 1, .diagnostics = 3 },
900 .{ .rules = 257, .selectors = 401, .compounds = 909, .combinators = 508, .classes = 333, .nths = 19, .atoms = 611, .atom_slots = 2048, .atom_bytes = 4097, .declarations = 511, .lowered = 1301, .customs = 37, .diagnostics = 17 },
901 };
902 for (cases) |limits| try std.testing.expectEqual(try modelCapacity(limits), try Capacity.derive(limits));
903 try std.testing.expectError(error.CapacityOverflow, Capacity.derive(.{ .rules = std.math.maxInt(usize) }));
904 try std.testing.expectError(error.CapacityOverflow, Capacity.derive(.{ .lowered = std.math.maxInt(usize) }));
905 }
906
907 test "CSS storage acquisition retries after every allocation failure" {
908 comptime {
909 alloc_phase.capacity.record(alloc_phase.capacity.witness(Storage, "css_acquisition"));
910 }
911
912 try std.testing.checkAllAllocationFailures(
913 std.testing.allocator,
914 checkCssStorageInit,
915 .{Limits{ .rules = 32, .selectors = 48, .compounds = 96, .combinators = 48, .classes = 24, .nths = 4, .atoms = 64, .atom_slots = 128, .atom_bytes = 256, .declarations = 96, .lowered = 192, .customs = 8, .diagnostics = 8 }},
916 );
917 }
918
919 test "CSS storage parses admitted source without backing allocation" {
920 comptime {
921 alloc_phase.capacity.record(alloc_phase.capacity.witness(Storage, "css_steady_overload"));
922 }
923 comptime {
924 alloc_phase.capacity.record(
925 alloc_phase.capacity.witness(Storage, "css_steady_foreign_risk"),
926 );
927 }
928
929 const source = "button.primary#save { color: red; display: block } @media screen { article { margin: 1px } } a { bad }";
930 const limits = try Limits.inspect(source, .default());
931 var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{});
932 var storage = try Storage.init(failing.allocator(), limits);
933 defer storage.deinit(failing.allocator());
934 storage.activate();
935 failing.fail_index = failing.alloc_index;
936 failing.resize_fail_index = failing.resize_index;
937 const parsed = storage.parse(source, .default());
938 try std.testing.expectEqual(limits.rules, parsed.rules.len);
939 try std.testing.expectEqual(limits.diagnostics, parsed.diagnostics.len);
940 try std.testing.expect(!failing.has_induced_failure);
941 }
942
943 test "CSS parser recovers declarations and diagnostics" {
944 var parsed = try TestSheet.init("body { display: block; color: red } a { bad }", .default());
945 defer parsed.deinit();
946 try std.testing.expectEqual(@as(usize, 2), parsed.sheet.rules.len);
947 try std.testing.expectEqualStrings("body", parsed.raw(0));
948 try std.testing.expectEqualStrings("display", parsed.sheet.rules[0].declarations[0].property);
949 try std.testing.expectEqual(@as(usize, 1), parsed.sheet.diagnostics.len);
950 }
951
952 test "CSS parser records selector specificity and important declarations" {
953 var parsed = try TestSheet.init("button.primary#save { color: blue !important; display: inline } .primary { color: red }", .default());
954 defer parsed.deinit();
955 const rules = parsed.sheet.rules;
956 try std.testing.expectEqualStrings("button.primary#save", parsed.raw(0));
957 try std.testing.expectEqual(@as(u16, 1), rules[0].head().specificity.ids);
958 try std.testing.expectEqual(@as(u16, 1), rules[0].head().specificity.classes);
959 try std.testing.expectEqual(@as(u16, 1), rules[0].head().specificity.types);
960 try std.testing.expect(rules[0].declarations[0].important);
961 try std.testing.expectEqualStrings("blue", rules[0].declarations[0].value);
962 try std.testing.expect(rules[0].lowered[0].important());
963 try std.testing.expect(rules[0].head().specificity.compare(rules[1].head().specificity) == .gt);
964 }
965
966 test "CSS parser skips inactive print media blocks" {
967 var parsed = try TestSheet.init(
968 \\main { display: block }
969 \\@media print { main { display: none !important } }
970 \\article { color: blue }
971 , .default());
972 defer parsed.deinit();
973 try std.testing.expectEqual(@as(usize, 2), parsed.sheet.rules.len);
974 try std.testing.expectEqualStrings("main", parsed.raw(0));
975 try std.testing.expectEqualStrings("article", parsed.raw(1));
976 try std.testing.expectEqualStrings("block", parsed.sheet.rules[0].declarations[0].value);
977 }
978
979 test "CSS parser flattens active media rule lists" {
980 var parsed = try TestSheet.init(
981 \\@media screen and (max-width: 840px) { .attempt { display: block; max-width: 100% } }
982 \\@media all { p { color: green } }
983 \\@media not print { a { display: inline } }
984 , .default());
985 defer parsed.deinit();
986 try std.testing.expectEqual(@as(usize, 3), parsed.sheet.rules.len);
987 try std.testing.expectEqualStrings(".attempt", parsed.raw(0));
988 try std.testing.expectEqualStrings("max-width", parsed.sheet.rules[0].declarations[1].property);
989 try std.testing.expectEqualStrings("p", parsed.raw(1));
990 try std.testing.expectEqualStrings("a", parsed.raw(2));
991 }
992
993 test "CSS parser evaluates viewport width media features" {
994 const source =
995 \\@media screen and (max-width: 840px) { .narrow { display: block } }
996 \\@media screen and (min-width: 841px) { .wide { display: block } }
997 \\@media (400px <= width <= 900px) { .middle { color: green } }
998 ;
999 var narrow = try TestSheet.init(source, .screen(800, 600));
1000 defer narrow.deinit();
1001 try std.testing.expectEqual(@as(usize, 2), narrow.sheet.rules.len);
1002 try std.testing.expectEqualStrings(".narrow", narrow.raw(0));
1003 try std.testing.expectEqualStrings(".middle", narrow.raw(1));
1004
1005 var wide = try TestSheet.init(source, .screen(1000, 600));
1006 defer wide.deinit();
1007 try std.testing.expectEqual(@as(usize, 1), wide.sheet.rules.len);
1008 try std.testing.expectEqualStrings(".wide", wide.raw(0));
1009 }
1010
1011 test "CSS parser evaluates viewport height orientation and query lists" {
1012 var parsed = try TestSheet.init(
1013 \\@media (min-height: 40em) { .tall { display: block } }
1014 \\@media (orientation: portrait) { .portrait { display: block } }
1015 \\@media print, (max-width: 640px) { .small { display: block } }
1016 , .screen(640, 700));
1017 defer parsed.deinit();
1018 try std.testing.expectEqual(@as(usize, 3), parsed.sheet.rules.len);
1019 try std.testing.expectEqualStrings(".tall", parsed.raw(0));
1020 try std.testing.expectEqualStrings(".portrait", parsed.raw(1));
1021 try std.testing.expectEqualStrings(".small", parsed.raw(2));
1022 }
1023
1024 test "CSS parser keeps unknown media features inactive under negation" {
1025 var parsed = try TestSheet.init(
1026 \\@media not (unsupported-feature: enabled) { .unsupported { display: block } }
1027 \\@media not print { .screen { display: block } }
1028 , .default());
1029 defer parsed.deinit();
1030 try std.testing.expectEqual(@as(usize, 1), parsed.sheet.rules.len);
1031 try std.testing.expectEqualStrings(".screen", parsed.raw(0));
1032 }
1033
1034 test "CSS parser skips unsupported conditional at-rule blocks" {
1035 var parsed = try TestSheet.init(
1036 \\@supports (display: grid) { main { display: none } }
1037 \\@page { margin: 0 }
1038 \\main { display: block }
1039 , .default());
1040 defer parsed.deinit();
1041 try std.testing.expectEqual(@as(usize, 1), parsed.sheet.rules.len);
1042 try std.testing.expectEqualStrings("main", parsed.raw(0));
1043 try std.testing.expectEqualStrings("block", parsed.sheet.rules[0].declarations[0].value);
1044 }
1045
1046 test "CSS parser preserves font face descriptor blocks as unmatchable rules" {
1047 var parsed = try TestSheet.init("@font-face { font-family: test; src: url(font.woff2) } p { color: red }", .default());
1048 defer parsed.deinit();
1049 try std.testing.expectEqual(@as(usize, 2), parsed.sheet.rules.len);
1050 try std.testing.expectEqualStrings("@font-face", parsed.raw(0));
1051 try std.testing.expectEqualStrings("src", parsed.sheet.rules[0].declarations[1].property);
1052 try std.testing.expectEqualStrings("p", parsed.raw(1));
1053 const compounds = parsed.sheet.rules[0].head().compounds;
1054 try std.testing.expectEqual(@as(usize, 1), compounds.len);
1055 try std.testing.expect(compounds[0].flags & selector.unmatchable != 0);
1056 }
1057
1058 test "CSS parser keeps nested declaration delimiters inside values" {
1059 var parsed = try TestSheet.init("div { background-image: url(\"data:image/svg+xml;utf8,<svg>{}</svg>\"); color: red }", .default());
1060 defer parsed.deinit();
1061 try std.testing.expectEqual(@as(usize, 1), parsed.sheet.rules.len);
1062 try std.testing.expectEqual(@as(usize, 2), parsed.sheet.rules[0].declarations.len);
1063 try std.testing.expectEqualStrings("background-image", parsed.sheet.rules[0].declarations[0].property);
1064 try std.testing.expectEqualStrings("color", parsed.sheet.rules[0].declarations[1].property);
1065 }
1066
1067 test "a stylesheet lowers longhands shorthands and custom properties" {
1068 var parsed = try TestSheet.init(".card { margin: 1px 2px; color: red; --brand: blue; width: var(--brand) }", .default());
1069 defer parsed.deinit();
1070 const rule = parsed.sheet.rules[0];
1071 try std.testing.expectEqual(@as(usize, 4), rule.declarations.len);
1072 try std.testing.expectEqual(@as(usize, 1), rule.customs.len);
1073 try std.testing.expectEqual(@as(usize, 6), rule.lowered.len);
1074 try std.testing.expectEqual(@backingInt(property.Id.margin_top), rule.lowered[0].property);
1075 try std.testing.expectEqual(@as(f32, 1), rule.lowered[0].value().asNumber());
1076 try std.testing.expectEqual(@backingInt(property.Id.margin_right), rule.lowered[1].property);
1077 try std.testing.expectEqual(@as(f32, 2), rule.lowered[1].value().asNumber());
1078 try std.testing.expectEqual(@backingInt(property.Id.color), rule.lowered[4].property);
1079 try std.testing.expectEqual(value.Kind.color, rule.lowered[4].value().valueKind());
1080 try std.testing.expectEqual(@backingInt(property.Id.width), rule.lowered[5].property);
1081 try std.testing.expect(rule.lowered[5].flags & cascade.flag_pending != 0);
1082 try std.testing.expectEqualStrings("--brand", parsed.sheet.name(rule.customs[0].name));
1083 try std.testing.expectEqualStrings("blue", parsed.sheet.source[rule.customs[0].start..rule.customs[0].end]);
1084 }
1085
1086 test "a selector list publishes one rule carrying every selector" {
1087 var parsed = try TestSheet.init("h1, .lead p { color: red }", .default());
1088 defer parsed.deinit();
1089 const rule = parsed.sheet.rules[0];
1090 try std.testing.expectEqual(@as(usize, 1), parsed.sheet.rules.len);
1091 try std.testing.expectEqual(@as(usize, 2), rule.selectors.len);
1092 try std.testing.expectEqualStrings("h1", rule.selectors[0].raw);
1093 try std.testing.expectEqualStrings(".lead p", rule.selectors[1].raw);
1094 try std.testing.expectEqual(@as(usize, 2), rule.selectors[1].compounds.len);
1095 try std.testing.expectEqual(@as(usize, 1), rule.selectors[1].combinators.len);
1096 }
1097
1098 test "one interning table answers every identifier in the sheet" {
1099 var parsed = try TestSheet.init("article.card#main { color: red } SPAN { color: blue }", .default());
1100 defer parsed.deinit();
1101 const article = parsed.sheet.token("article");
1102 try std.testing.expect(article != atom.none);
1103 try std.testing.expectEqualStrings("article", parsed.sheet.name(article));
1104 try std.testing.expect(parsed.sheet.token("card") != atom.none);
1105 try std.testing.expect(parsed.sheet.token("main") != atom.none);
1106 try std.testing.expect(parsed.sheet.token("span") != atom.none);
1107 try std.testing.expectEqual(atom.none, parsed.sheet.token("absent"));
1108 try std.testing.expectEqual(@as(u16, @intCast(article)), parsed.sheet.rules[0].head().compounds[0].kind);
1109 }