///|
priv struct GeneratedPropertySpec {
name : String
syntax : String
initial : String?
initial_before_inherits : Bool
}
///|
/// A block of `@property` registrations that upstream emits as one `at-root`
/// node.
///
/// `triggers` are the declaration fragments that show the owning utility ran.
/// Membership in a block is all or nothing: registering one member registers
/// every member, in the declared order.
priv struct GeneratedPropertyBlock {
triggers : Array[String]
members : Array[GeneratedPropertySpec]
}
///|
fn property_spec(
name : String,
syntax : String,
initial? : String,
initial_before_inherits? : Bool = false,
) -> GeneratedPropertySpec {
{ name, syntax, initial, initial_before_inherits }
}
///|
fn declaration_triggers(names : ArrayView[String]) -> Array[String] {
names.map(fn(name) { "\{name}:" })
}
///|
fn generated_property_blocks() -> Array[GeneratedPropertyBlock] {
[
{
triggers: ["--tw-border-spacing-x:", "--tw-border-spacing-y:"],
members: [
property_spec("--tw-border-spacing-x", "", initial="0"),
property_spec("--tw-border-spacing-y", "", initial="0"),
],
},
{
triggers: ["var(--tw-border-style"],
members: [property_spec("--tw-border-style", "*", initial="solid")],
},
{
triggers: ["--tw-space-x-reverse:"],
members: [property_spec("--tw-space-x-reverse", "*", initial="0")],
},
{
triggers: ["--tw-space-y-reverse:"],
members: [property_spec("--tw-space-y-reverse", "*", initial="0")],
},
{
triggers: ["--tw-divide-x-reverse:"],
members: [property_spec("--tw-divide-x-reverse", "*", initial="0")],
},
{
triggers: ["--tw-divide-y-reverse:"],
members: [property_spec("--tw-divide-y-reverse", "*", initial="0")],
},
{
triggers: ["var(--tw-outline-style"],
members: [property_spec("--tw-outline-style", "*", initial="solid")],
},
{
triggers: ["--tw-leading:"],
members: [property_spec("--tw-leading", "*")],
},
{
triggers: ["--tw-font-weight:"],
members: [property_spec("--tw-font-weight", "*")],
},
{
triggers: [
"--tw-translate-x:", "--tw-translate-y:", "--tw-translate-z:", "var(--tw-translate-",
],
members: [
property_spec("--tw-translate-x", "*", initial="0"),
property_spec("--tw-translate-y", "*", initial="0"),
property_spec("--tw-translate-z", "*", initial="0"),
],
},
{
triggers: [
"--tw-scale-x:", "--tw-scale-y:", "--tw-scale-z:", "var(--tw-scale-",
],
members: [
property_spec("--tw-scale-x", "*", initial="1"),
property_spec("--tw-scale-y", "*", initial="1"),
property_spec("--tw-scale-z", "*", initial="1"),
],
},
{
triggers: [
"--tw-rotate-x:", "--tw-rotate-y:", "--tw-rotate-z:", "--tw-skew-x:", "--tw-skew-y:",
"var(--tw-rotate-", "var(--tw-skew-",
],
members: [
property_spec("--tw-rotate-x", "*"),
property_spec("--tw-rotate-y", "*"),
property_spec("--tw-rotate-z", "*"),
property_spec("--tw-skew-x", "*"),
property_spec("--tw-skew-y", "*"),
],
},
{
triggers: [
"--tw-pan-x:", "--tw-pan-y:", "--tw-pinch-zoom:", "var(--tw-pan-", "var(--tw-pinch-zoom",
],
members: [
property_spec("--tw-pan-x", "*"),
property_spec("--tw-pan-y", "*"),
property_spec("--tw-pinch-zoom", "*"),
],
},
{
triggers: [
"--tw-scroll-snap-strictness:", "var(--tw-scroll-snap-strictness",
],
members: [
property_spec("--tw-scroll-snap-strictness", "*", initial="proximity"),
],
},
{
triggers: declaration_triggers([
"--tw-gradient-from", "--tw-gradient-via", "--tw-gradient-to", "--tw-gradient-stops",
"--tw-gradient-via-stops", "--tw-gradient-from-position", "--tw-gradient-via-position",
"--tw-gradient-to-position",
]),
members: [
property_spec("--tw-gradient-position", "*"),
property_spec("--tw-gradient-from", "", initial="#0000"),
property_spec("--tw-gradient-via", "", initial="#0000"),
property_spec("--tw-gradient-to", "", initial="#0000"),
property_spec("--tw-gradient-stops", "*"),
property_spec("--tw-gradient-via-stops", "*"),
property_spec(
"--tw-gradient-from-position",
"",
initial="0%",
),
property_spec(
"--tw-gradient-via-position",
"",
initial="50%",
),
property_spec(
"--tw-gradient-to-position",
"",
initial="100%",
),
],
},
{
triggers: declaration_triggers([
"--tw-shadow", "--tw-shadow-color", "--tw-inset-shadow", "--tw-inset-shadow-color",
"--tw-ring-shadow", "--tw-inset-ring-shadow", "--tw-ring-inset",
]),
members: [
property_spec("--tw-shadow", "*", initial="0 0 #0000"),
property_spec("--tw-shadow-color", "*"),
property_spec("--tw-shadow-alpha", "", initial="100%"),
property_spec("--tw-inset-shadow", "*", initial="0 0 #0000"),
property_spec("--tw-inset-shadow-color", "*"),
property_spec("--tw-inset-shadow-alpha", "", initial="100%"),
property_spec("--tw-ring-color", "*"),
property_spec("--tw-ring-shadow", "*", initial="0 0 #0000"),
property_spec("--tw-inset-ring-color", "*"),
property_spec("--tw-inset-ring-shadow", "*", initial="0 0 #0000"),
property_spec("--tw-ring-inset", "*"),
property_spec("--tw-ring-offset-width", "", initial="0px"),
property_spec("--tw-ring-offset-color", "*", initial="#fff"),
property_spec("--tw-ring-offset-shadow", "*", initial="0 0 #0000"),
],
},
{
triggers: ["filter: var(--tw-blur"],
members: [
property_spec("--tw-blur", "*"),
property_spec("--tw-brightness", "*"),
property_spec("--tw-contrast", "*"),
property_spec("--tw-grayscale", "*"),
property_spec("--tw-hue-rotate", "*"),
property_spec("--tw-invert", "*"),
property_spec("--tw-opacity", "*"),
property_spec("--tw-saturate", "*"),
property_spec("--tw-sepia", "*"),
property_spec("--tw-drop-shadow", "*"),
property_spec("--tw-drop-shadow-color", "*"),
property_spec("--tw-drop-shadow-alpha", "", initial="100%"),
property_spec("--tw-drop-shadow-size", "*"),
],
},
{
triggers: ["backdrop-filter: var(--tw-backdrop-blur"],
members: [
property_spec("--tw-backdrop-blur", "*"),
property_spec("--tw-backdrop-brightness", "*"),
property_spec("--tw-backdrop-contrast", "*"),
property_spec("--tw-backdrop-grayscale", "*"),
property_spec("--tw-backdrop-hue-rotate", "*"),
property_spec("--tw-backdrop-invert", "*"),
property_spec("--tw-backdrop-opacity", "*"),
property_spec("--tw-backdrop-saturate", "*"),
property_spec("--tw-backdrop-sepia", "*"),
],
},
{
triggers: ["--tw-duration:"],
members: [property_spec("--tw-duration", "*")],
},
{ triggers: ["--tw-ease:"], members: [property_spec("--tw-ease", "*")] },
{
// Trigger on the utility's own custom property rather than a plain
// `text-shadow:` declaration, so author `text-shadow` rules don't register
// these (upstream registers them only for the `text-shadow-*` utility).
triggers: ["--tw-text-shadow-color"],
members: [
property_spec("--tw-text-shadow-color", "*"),
property_spec("--tw-text-shadow-alpha", "", initial="100%"),
],
},
{
triggers: ["--tw-tracking:"],
members: [property_spec("--tw-tracking", "*")],
},
{
triggers: declaration_triggers([
"--tw-ordinal", "--tw-slashed-zero", "--tw-numeric-figure", "--tw-numeric-spacing",
"--tw-numeric-fraction",
]),
members: [
property_spec("--tw-ordinal", "*"),
property_spec("--tw-slashed-zero", "*"),
property_spec("--tw-numeric-figure", "*"),
property_spec("--tw-numeric-spacing", "*"),
property_spec("--tw-numeric-fraction", "*"),
],
},
{
// `content-none` sets the property without reading it back and registers
// nothing; the functional `content-*` utility and the `before`/`after`
// variants both read it.
triggers: ["content: var(--tw-content)"],
members: [
property_spec(
"--tw-content",
"*",
initial="\"\"",
initial_before_inherits=true,
),
],
},
]
}
///|
/// The registration blocks, built once. They are read-only after construction,
/// and both the trigger index below and every `build` walk them.
let generated_property_block_table : Array[GeneratedPropertyBlock] = generated_property_blocks()
///|
/// One trigger, prepared for a single scan of the compiled CSS.
///
/// `offset` is where `--tw-` starts inside the trigger and `key` is the code unit
/// straight after it, so a `--tw-` found in the CSS can rewind to where this
/// trigger would have to begin and reject most candidates on one comparison.
priv struct GeneratedPropertyTrigger {
text : String
offset : Int
key : UInt16
block : Int
}
///|
fn build_generated_property_triggers() -> Array[GeneratedPropertyTrigger] {
let entries : Array[GeneratedPropertyTrigger] = []
for index, block in generated_property_block_table {
for trigger in block.triggers {
// Every trigger names a `--tw-…` custom property, so `--tw-` is present
// exactly once and something always follows it.
let offset = index_of(trigger, "--tw-").unwrap_or(0)
entries.push({
text: trigger,
offset,
key: trigger[offset + 5],
block: index,
})
}
}
entries
}
///|
let generated_property_triggers : Array[GeneratedPropertyTrigger] = build_generated_property_triggers()
///|
/// Position of the first declaration that registers each block, or `-1` for the
/// blocks nothing registers.
///
/// Each trigger used to be searched for over the whole stylesheet on its own —
/// ~66 full scans per `build`. Since every trigger contains `--tw-`, one pass
/// that stops at each `--tw-` and tests only the triggers whose next code unit
/// matches finds all of them.
fn generated_property_block_positions(css : String) -> Array[Int] {
let positions = Array::make(generated_property_block_table.length(), -1)
let length = css.length()
let mut pos = 0
while pos < length {
guard css[pos:].find("--tw-") is Some(found) else { break }
let at = pos + found
pos = at + 1
let key_index = at + 5
if key_index >= length {
break
}
let key = css[key_index]
for entry in generated_property_triggers {
if entry.key != key {
continue
}
let start = at - entry.offset
if start < 0 || start + entry.text.length() > length {
continue
}
if css[start:start + entry.text.length()] == entry.text {
let current = positions[entry.block]
if current < 0 || start < current {
positions[entry.block] = start
}
}
}
}
positions
}
///|
/// Whether `--tw-content` is registered by the `before`/`after` variants rather
/// than by a `content-*` utility.
///
/// The two upstream registration sites disagree about declaration order, and
/// the one that appears first in the output wins.
fn content_registered_by_variant(css : String) -> Bool {
// The functional `content-*` utility declares `--tw-content` and reads it
// back in the same rule; the variants only read it.
let mut rest = css
for ;; {
guard rest.split_once("--tw-content:") is Some((_, after)) else { break }
let tail = after.to_owned()
let boundary = index_of(tail, "}").unwrap_or(tail.length())
if index_of(tail[:boundary].to_owned(), "content: var(--tw-content)")
is Some(_) {
return false
}
rest = tail
}
true
}
///|
/// Collect the author `@property` registrations the compiled CSS exposes at its
/// top level.
///
/// These used to be recovered by running `parse_css` over the whole compiled
/// stylesheet, which is the single most expensive thing a `build` does on a graph
/// that actually registers one — opt 17's `@property` literal guard skips the
/// parse only for stylesheets that register nothing. But that text is rendered
/// from an AST this compiler is already holding, so the registrations can be read
/// off the AST and the round trip through the parser disappears.
///
/// Only the top level counts, which is what iterating `parse_css`'s result did.
/// `Context` wrappers render their children inline and `at-root` re-indents its
/// children to column zero, so children of both stay at the top level of the
/// text; a `@property` nested in a rule or another at-rule does not, and is
/// skipped exactly as it was before.
fn collect_author_property_at_rules(
nodes : ArrayView[CssNode],
output : Array[CssNode],
) -> Unit {
for node in nodes {
match node {
AtRule(name="@property", nodes=Some(_), ..) => output.push(node)
Context(nodes=children, ..) =>
collect_author_property_at_rules(children, output)
AtRoot(nodes=children, ..) =>
collect_author_property_at_rules(children, output)
_ => ()
}
}
}
///|
/// Turn author `@property` registrations into the `@layer properties` fallback
/// declarations. Returns the declarations for inheriting properties (emitted on
/// `:root, :host`) and the rest (appended to the universal selector).
fn author_property_defaults(
at_rules : ArrayView[CssNode],
span : SourceSpan,
) -> (Array[CssNode], Array[CssNode]) {
let root : Array[CssNode] = []
let universal : Array[CssNode] = []
for node in at_rules {
guard node is AtRule(params~, nodes=Some(declarations), ..) else { continue }
let name = trim(params)
let mut inherits_true = false
let mut initial : String? = None
for declaration in declarations {
match declaration {
Declaration(name="inherits", value~, ..) =>
inherits_true = trim(value) == "true"
Declaration(name="initial-value", value~, ..) => initial = Some(value)
_ => ()
}
}
match initial {
Some(value) => {
let entry = Declaration(name~, value~, important=false, span~)
if inherits_true {
root.push(entry)
} else {
universal.push(entry)
}
}
None => ()
}
}
(root, universal)
}
///|
fn render_generated_properties(
css : String,
author_properties : ArrayView[CssNode],
at_property_fallback? : Bool = true,
) -> (String, String) {
let positions = generated_property_block_positions(css)
let blocks : Array[(GeneratedPropertyBlock, Int, Int)] = []
for index, block in generated_property_block_table {
if positions[index] >= 0 {
blocks.push((block, positions[index], index))
}
}
if blocks.is_empty() {
return ("", "")
}
// Upstream hoists `at-root` nodes in the order the optimized AST reaches
// them, which is the order the registering declarations are rendered in.
blocks.sort_by(fn(a, b) {
let (_, a_position, a_index) = a
let (_, b_position, b_index) = b
let by_position = a_position.compare(b_position)
if by_position != 0 {
by_position
} else {
a_index.compare(b_index)
}
})
let span : SourceSpan = { start: 0, end: 0 }
let registrations : Array[CssNode] = []
let defaults : Array[CssNode] = []
let content_first = content_registered_by_variant(css)
for entry in blocks {
let (block, _, _) = entry
for spec in block.members {
let initial_before_inherits = if spec.name == "--tw-content" {
content_first
} else {
spec.initial_before_inherits
}
let declarations : Array[CssNode] = [
Declaration(
name="syntax",
value="\"\{spec.syntax}\"",
important=false,
span~,
),
]
if initial_before_inherits {
match spec.initial {
Some(initial) =>
declarations.push(
Declaration(
name="initial-value",
value=initial,
important=false,
span~,
),
)
None => ()
}
}
declarations.push(
Declaration(name="inherits", value="false", important=false, span~),
)
if !initial_before_inherits {
match spec.initial {
Some(initial) =>
declarations.push(
Declaration(
name="initial-value",
value=initial,
important=false,
span~,
),
)
None => ()
}
}
registrations.push(
AtRule(
name="@property",
params=spec.name,
nodes=Some(declarations),
span~,
),
)
defaults.push(
Declaration(
name=spec.name,
value=spec.initial.unwrap_or("initial"),
important=false,
span~,
),
)
}
}
if !at_property_fallback {
return ("", render_css_nodes(registrations))
}
// Author `@property` registrations receive the same `@layer properties`
// fallback as generated ones: inheriting properties on `:root, :host`, the
// rest appended to the universal selector.
let (author_root, author_universal) = author_property_defaults(
author_properties,
span,
)
for declaration in author_universal {
defaults.push(declaration)
}
let supports_children : Array[CssNode] = []
if !author_root.is_empty() {
supports_children.push(
Rule(selector=":root, :host", nodes=author_root, span~),
)
}
supports_children.push(
Rule(selector="*, ::before, ::after, ::backdrop", nodes=defaults, span~),
)
registrations.push(
AtRule(
name="@layer",
params="properties",
nodes=Some([
AtRule(
name="@supports",
params="((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b))))",
nodes=Some(supports_children),
span~,
),
]),
span~,
),
)
(
render_css_nodes([
AtRule(name="@layer", params="properties", nodes=None, span~),
]),
render_css_nodes(registrations),
)
}