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 }