tiny.css.cascade
Defined in tiny.css.
The cascade: origin, importance, specificity, source order, inheritance, the four CSS wide keywords, custom properties, and var() substitution.
API (23)
Actions
Public operations.
Computed.getComputed.initial: The computed style of an element with no declarations and no parent.Computed.setCustomTable.findResolver.add: Offers one declaration to the cascade.Resolver.addCustom: Offers one custom property declaration to the cascade.Resolver.begin: Starts a fresh element.Resolver.finish: Resolves every property againstparentand writes the computed style.levelOf: The cascade level a declaration lands in.originOf: The origin behind a cascade level.
Types and contracts
Public types and contracts.
Computed: A computed style: one value per property, dense and indexed by identifier.Custom: One custom property declaration.CustomSlot: One custom property's winning declaration.CustomTable: The custom properties in scope for one element.Level: The cascade levels in increasing precedence.Origin: The three cascade origins this engine carries.Resolver: Resolves one element's cascade.Slot: The winning declaration at one cascade level.
Values and defaults
Public values and defaults.
flag_pending: Bit one of a value's flag word marks a declaration whose text still holds avar()reference.level_count: The number of cascade levels.max_custom_properties: The most custom properties one element may carry.max_substitution_bytes: The substitution buffer one element'svar()expansions share.max_substitution_depth: The deepestvar()nesting the resolver expands before failing closed.
Source
Source: lib/css/src/cascade/resolve.zig:64
/// A computed style: one value per property, dense and indexed by identifier.pub const Computed = struct { values: [property.count]Value = @splat(.{}), pub fn get(self: *const Computed, id: property.Id) Value { std.debug.assert(id != .invalid); return self.values[@backingInt(id)]; } pub fn set(self: *Computed, id: property.Id, item: Value) void { std.debug.assert(id != .invalid); self.values[@backingInt(id)] = item; } /// The computed style of an element with no declarations and no parent. pub fn initial() Computed { var result = Computed{}; for (property.entries, 1..) |entry, index| result.values[index] = entry.initial; return result; }};Source: lib/css/src/cascade/resolve.zig:48
/// One custom property declaration. Its value stays raw text, because a custom/// property's value is a token sequence rather than a typed value.pub const Custom = struct { name: u32, start: u32, end: u32, important: bool = false,};Source: lib/css/src/cascade/resolve.zig:86
/// One custom property's winning declaration.pub const CustomSlot = struct { name: u32 = 0, start: u32 = 0, end: u32 = 0, level: Level = .user_agent_normal, specificity: Specificity = .{}, order: u32 = 0, inherited: bool = false,};Source: lib/css/src/cascade/resolve.zig:98
/// The custom properties in scope for one element. Overflow rejects the extra/// declaration and counts it rather than evicting a resolved one.pub const CustomTable = struct { slots: [max_custom_properties]CustomSlot = @splat(.{}), count: u32 = 0, rejected: u32 = 0, pub fn find(self: *const CustomTable, name: u32) ?usize { var index: usize = 0; while (index < self.count) : (index += 1) { if (self.slots[index].name == name) return index; } return null; } fn clear(self: *CustomTable) void { self.count = 0; self.rejected = 0; }};Source: lib/css/src/cascade/resolve.zig:21
/// The cascade levels in increasing precedence. Importance inverts the origin/// order, which is the whole reason the levels are named rather than derived.pub const Level = enum(u8) { user_agent_normal = 0, user_normal = 1, author_normal = 2, author_important = 3, user_important = 4, user_agent_important = 5,};Source: lib/css/src/cascade/resolve.zig:13
/// The three cascade origins this engine carries.pub const Origin = enum(u8) { user_agent = 0, user = 1, author = 2,};Source: lib/css/src/cascade/resolve.zig:122
/// Resolves one element's cascade. The caller owns it and reuses it across/// elements, so the cascade allocates nothing.////// A substituted string value borrows the resolver's substitution buffer and/// stays valid until the next `begin`.pub const Resolver = struct { slots: [property.count][level_count]Slot = @splat(@splat(.{})), customs: CustomTable = .{}, buffer: [max_substitution_bytes]u8 = undefined, names: ?*const atom.Table = null, used: u32 = 0, source: []const u8 = &.{}, substitution_rejected: u32 = 0, /// Starts a fresh element. `parent_customs` seeds the inherited custom /// properties, which is how a custom property reaches a subtree. pub fn begin( self: *Resolver, source: []const u8, names: ?*const atom.Table, parent_customs: ?*const CustomTable, ) void { self.slots = @splat(@splat(.{})); self.names = names; self.customs.clear(); self.used = 0; self.source = source; self.substitution_rejected = 0; if (parent_customs) |table| { var index: usize = 0; while (index < table.count and index < max_custom_properties) : (index += 1) { self.customs.slots[index] = table.slots[index]; self.customs.slots[index].inherited = true; } self.customs.count = @intCast(index); } } /// Offers one declaration to the cascade. pub fn add( self: *Resolver, declaration: value.Declaration, origin: Origin, specificity: Specificity, order: u32, ) void { const id: property.Id = @fromBackingInt(@intCast(declaration.property)); if (id == .invalid) return; const level = levelOf(origin, declaration.important()); const slot = &self.slots[declaration.property][@backingInt(level)]; const candidate = Slot{ .value = declaration.value(), .specificity = specificity, .order = order, .present = true, }; if (!slot.present or wins(candidate, slot.*)) slot.* = candidate; } /// Offers one custom property declaration to the cascade. pub fn addCustom( self: *Resolver, custom: Custom, origin: Origin, specificity: Specificity, order: u32, ) void { const level = levelOf(origin, custom.important); const candidate = CustomSlot{ .name = custom.name, .start = custom.start, .end = custom.end, .level = level, .specificity = specificity, .order = order, }; if (self.customs.find(custom.name)) |index| { const existing = self.customs.slots[index]; if (existing.inherited or customWins(candidate, existing)) { self.customs.slots[index] = candidate; } return; } if (self.customs.count == max_custom_properties) { self.customs.rejected += 1; return; } self.customs.slots[self.customs.count] = candidate; self.customs.count += 1; } /// Resolves every property against `parent` and writes the computed style. pub fn finish(self: *Resolver, parent: ?*const Computed, out: *Computed) void { out.values[0] = .{}; for (property.entries, 1..) |entry, index| { const id: property.Id = @fromBackingInt(@intCast(index)); const from_parent = if (parent) |source| source.get(id) else entry.initial; const winner = self.resolve(index); out.values[index] = self.compute(id, winner, entry, from_parent); } } fn compute( self: *Resolver, id: property.Id, winner: ?Slot, entry: property.Metadata, from_parent: Value, ) Value { const fallback = if (entry.inherited) from_parent else entry.initial; const slot = winner orelse return fallback; const resolved = if (slot.value.flags & flag_pending != 0) self.substituted(id, slot.value) orelse return fallback else slot.value; return switch (resolved.asKeyword()) { .inherit => from_parent, .initial, .revert => entry.initial, .unset => fallback, else => resolved, }; } fn resolve(self: *Resolver, index: usize) ?Slot { var excluded: u8 = 0; var round: u32 = 0; while (round <= 3) : (round += 1) { const found = self.pick(index, excluded) orelse return null; if (found.slot.value.asKeyword() != .revert) return found.slot; const origin = originOf(found.level); excluded |= originBit(origin); } return null; } const Winner = struct { slot: Slot, level: Level, }; fn pick(self: *Resolver, index: usize, excluded: u8) ?Winner { var level: usize = level_count; while (level > 0) { level -= 1; const candidate: Level = @fromBackingInt(@intCast(level)); if (excluded & originBit(originOf(candidate)) != 0) continue; const slot = self.slots[index][level]; if (slot.present) return .{ .slot = slot, .level = candidate }; } return null; } fn substituted(self: *Resolver, id: property.Id, pending: Value) ?Value { const begin_offset = self.used; var visiting: u64 = 0; if (!self.expandSpan(pending.a, pending.a + pending.b, 0, &visiting)) { self.used = begin_offset; self.substitution_rejected += 1; return null; } const text = self.buffer[begin_offset..self.used]; const grammar = property.metadata(id).grammar; const parsed = value.parse(grammar, text, 0, @intCast(text.len)); if (!parsed.present()) return null; return parsed; } fn expandSpan(self: *Resolver, start: u32, end: u32, depth: u32, visiting: *u64) bool { if (depth > max_substitution_depth) return false; std.debug.assert(end <= self.source.len); var tokenizer = token.Tokenizer{ .source = self.source[0..end], .index = start }; var copied = start; var guard: usize = 0; while (guard <= self.source.len + 1) : (guard += 1) { const next = tokenizer.next(); if (next.kind == .eof) break; if (next.kind != .function) continue; if (!std.ascii.eqlIgnoreCase(next.value(self.source), "var")) continue; const close = closeParen(&tokenizer, self.source.len) orelse return false; if (!self.write(self.source[copied..next.start])) return false; if (!self.expandVariable(next.end, close, depth, visiting)) return false; copied = close + 1; tokenizer.index = close + 1; } return self.write(self.source[copied..end]); } fn expandVariable(self: *Resolver, start: u32, end: u32, depth: u32, visiting: *u64) bool { const body = self.source[start..end]; const comma = topLevelComma(body); const name = std.mem.trim(u8, body[0 .. comma orelse body.len], " \t\r\n\x0C"); if (!property.isCustom(name)) return false; if (self.lookupCustom(name)) |index| { const mask = @as(u64, 1) << @intCast(index); if (visiting.* & mask != 0) return false; visiting.* |= mask; const slot = self.customs.slots[index]; const expanded = self.expandSpan(slot.start, slot.end, depth + 1, visiting); visiting.* &= ~mask; if (expanded) return true; } const offset = comma orelse return false; const fallback_start = start + @as(u32, @intCast(offset)) + 1; return self.expandSpan(fallback_start, end, depth + 1, visiting); } fn lookupCustom(self: *const Resolver, name: []const u8) ?usize { const table = self.names orelse return null; const id = table.lookup(name); if (id == atom.none) return null; if (self.customs.count > max_custom_properties) return null; return self.customs.find(id); } fn write(self: *Resolver, text: []const u8) bool { if (self.used + text.len > self.buffer.len) return false; @memcpy(self.buffer[self.used..][0..text.len], text); self.used += @intCast(text.len); return true; }};Source: lib/css/src/cascade/resolve.zig:56
/// The winning declaration at one cascade level.pub const Slot = struct { value: Value = .{}, specificity: Specificity = .{}, order: u32 = 0, present: bool = false,};Source: lib/css/src/cascade/resolve.zig:44
/// Bit one of a value's flag word marks a declaration whose text still holds a/// `var()` reference. Its `a` and `b` words are the source span, not a value.pub const flag_pending: u16 = 2;Source: lib/css/src/cascade/resolve.zig:359
/// The cascade level a declaration lands in.pub fn levelOf(origin: Origin, important: bool) Level { return switch (origin) { .user_agent => if (important) .user_agent_important else .user_agent_normal, .user => if (important) .user_important else .user_normal, .author => if (important) .author_important else .author_normal, };}Source: lib/css/src/cascade/resolve.zig:31
/// The number of cascade levels.pub const level_count: usize = 6;Source: lib/css/src/cascade/resolve.zig:34
/// The most custom properties one element may carry.pub const max_custom_properties: usize = 64;Source: lib/css/src/cascade/resolve.zig:37
/// The substitution buffer one element's `var()` expansions share.pub const max_substitution_bytes: usize = 4096;Source: lib/css/src/cascade/resolve.zig:40
/// The deepest `var()` nesting the resolver expands before failing closed.pub const max_substitution_depth: u32 = 8;Source: lib/css/src/cascade/resolve.zig:372
/// The origin behind a cascade level.pub fn originOf(level: Level) Origin { return switch (level) { .user_agent_normal, .user_agent_important => .user_agent, .user_normal, .user_important => .user, .author_normal, .author_important => .author, };}Source: lib/css/src/cascade/root.zig
//! The cascade: origin, importance, specificity, source order, inheritance,//! the four CSS wide keywords, custom properties, and `var()` substitution.//!//! `Resolver` holds one winning declaration per property per cascade level, so//! `revert` is answered by re-picking with the reverting origin removed rather//! than by a second pass over the matched rules. The caller owns the resolver//! and reuses it across elements, so the cascade allocates nothing.//!//! A computed style is a dense array indexed by property identifier, which is//! what makes inheritance a copy and comparison a memcmp.const resolve = @import("resolve.zig");pub const Computed = resolve.Computed;pub const Custom = resolve.Custom;pub const CustomSlot = resolve.CustomSlot;pub const CustomTable = resolve.CustomTable;pub const Level = resolve.Level;pub const Origin = resolve.Origin;pub const Resolver = resolve.Resolver;pub const Slot = resolve.Slot;pub const flag_pending = resolve.flag_pending;pub const level_count = resolve.level_count;pub const levelOf = resolve.levelOf;pub const max_custom_properties = resolve.max_custom_properties;pub const max_substitution_bytes = resolve.max_substitution_bytes;pub const max_substitution_depth = resolve.max_substitution_depth;pub const originOf = resolve.originOf;Source: lib/css/src/root.zig:37
pub const cascade = @import("cascade/root.zig");Audit
| Definitions | 24 |
|---|---|
| Public names | 24 |
| Members | 35 |
| Version | 26.7.0 |
| Revision | daab053ee433 |