///|
/// Wrap the colour of each comma-separated shadow layer in
/// `var(, )` so `shadow-` utilities can recolour
/// it. Handles multi-layer shadows and colours with internal spaces such as
/// `rgb(0 0 0 / 0.1)`.
/// Wraps a shadow's color in the alpha the modifier asked for. A literal from
/// the theme is rewritten in place with relative color syntax; `currentcolor`
/// has to go through `color-mix`, which the fallback pass then polyfills.
fn shadow_color_with_alpha(color : String, alpha : String?) -> String {
match alpha {
None => color
Some(alpha) =>
if color == "currentcolor" {
"color-mix(in oklab, currentcolor \{alpha}, transparent)"
} else {
"oklab(from \{color} l a b / \{alpha})"
}
}
}
///|
fn shadow_value_with_color(
value : String,
custom_property : String,
alpha : String?,
) -> String {
let layers = split_function_arguments(value)
if layers.length() == 0 {
return value
}
let wrapped = layers.map(fn(layer) {
let nodes = parse_value(layer)
// The colour is the last non-separator token in the layer.
let mut index = -1
for i, node in nodes {
if !(node is ValueSeparator(_)) {
index = i
}
}
if index < 0 {
return layer
}
let is_color = match nodes[index] {
// A trailing `var()` is taken as the color, as upstream does.
ValueFunction(name, _) =>
["rgb", "rgba", "hsl", "hsla", "oklch", "oklab", "hwb", "color", "var"].contains(
name,
)
ValueWord(word) => word.has_prefix("#")
_ => false
}
if is_color {
let rendered = shadow_color_with_alpha(
render_value([nodes[index]]),
alpha,
)
nodes[index] = ValueWord("var(\{custom_property}, \{rendered})")
render_value(nodes)
} else {
// A shadow written without a color still gets the slot, defaulting to
// `currentcolor`, so `shadow-` can fill it in later.
let default_color = shadow_color_with_alpha("currentcolor", alpha)
"\{layer} var(\{custom_property}, \{default_color})"
}
})
wrapped.join(", ")
}
///|
/// A color for one of the shadow or ring utilities, resolved against its own
/// theme namespace before `--color`, with the three universal keywords.
fn shadow_color(
theme : Map[String, String],
theme_namespace : String,
key : String,
) -> String? {
match arbitrary_value(key) {
Some(value) => return Some(value)
None => ()
}
match keyword_color(key) {
Some(value) => return Some(value)
None => ()
}
for candidate_namespace in [theme_namespace, "--color"] {
if theme_value(theme, candidate_namespace, key) is Some(_) {
return theme_css_value(theme, "\{candidate_namespace}-\{key}")
}
}
None
}
///|
/// The literal and the `var()` form of a shadow color, which the shadow
/// utilities need together: the literal becomes the `color-mix` fallback.
fn shadow_color_pair(
theme : Map[String, String],
theme_namespace : String,
key : String,
) -> (String, String)? {
// The universal keywords resolve to themselves, and are their own fallback.
match keyword_color(key) {
Some(value) => return Some((value, value))
None => ()
}
for candidate_namespace in [theme_namespace, "--color"] {
if theme.get("\{candidate_namespace}-\{key}") is Some(literal) &&
theme_css_value(theme, "\{candidate_namespace}-\{key}") is Some(value) {
return Some((literal, value))
}
}
None
}
///|
fn shadow_ring_utility(
theme : Map[String, String],
name : String,
modifier : String?,
) -> Array[Declaration]? {
let shadow_stack = "var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)"
if name == "inset-ring" || name.has_prefix("inset-ring-") {
let key = if name == "inset-ring" { "" } else { name[11:].to_owned() }
let width = if key == "" {
Some("1px")
} else {
match arbitrary_typed_value(key) {
Some((value, data_type)) =>
if is_measure_value(value, data_type) {
Some(value)
} else {
None
}
None =>
if is_nonnegative_integer(key) {
Some("\{key}px")
} else {
theme_css_value(theme, "--inset-ring-width-\{key}")
}
}
}
match width {
Some(value) =>
return Some([
decl(
"--tw-inset-ring-shadow",
"inset 0 0 0 \{value} var(--tw-inset-ring-color, currentcolor)",
),
decl("box-shadow", shadow_stack),
])
None =>
match shadow_color(theme, "--ring-color", key) {
Some(value) => return Some([decl("--tw-inset-ring-color", value)])
None => return None
}
}
}
if name == "ring" || name.has_prefix("ring-") {
let key = if name == "ring" { "" } else { name[5:].to_owned() }
if key.has_prefix("offset-") {
let offset_key = key[7:].to_owned()
let width = match arbitrary_typed_value(offset_key) {
Some((value, data_type)) =>
if is_measure_value(value, data_type) {
Some(value)
} else {
None
}
None =>
if is_nonnegative_integer(offset_key) {
Some("\{offset_key}px")
} else {
theme_css_value(theme, "--ring-offset-width-\{offset_key}")
}
}
match width {
Some(value) =>
return Some([
decl("--tw-ring-offset-width", value),
decl(
"--tw-ring-offset-shadow", "var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)",
),
])
None =>
match shadow_color(theme, "--ring-offset-color", offset_key) {
Some(value) => return Some([decl("--tw-ring-offset-color", value)])
None => return None
}
}
}
let width = if key == "" {
Some(theme.get("--default-ring-width").unwrap_or("1px"))
} else {
match arbitrary_typed_value(key) {
Some((value, data_type)) =>
if is_measure_value(value, data_type) {
Some(value)
} else {
None
}
None =>
if is_nonnegative_integer(key) {
Some("\{key}px")
} else {
theme_css_value(theme, "--ring-width-\{key}")
}
}
}
match width {
Some(value) =>
return Some([
decl(
"--tw-ring-shadow",
"var(--tw-ring-inset,) 0 0 0 calc(\{value} + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor)",
),
decl("box-shadow", shadow_stack),
])
None =>
match shadow_color(theme, "--ring-color", key) {
Some(value) => return Some([decl("--tw-ring-color", value)])
None => return None
}
}
}
let (root, theme_namespace, property, color_property) = if name.has_prefix(
"inset-shadow-",
) {
(
"inset-shadow", "--inset-shadow", "--tw-inset-shadow", "--tw-inset-shadow-color",
)
} else if name.has_prefix("shadow-") || name == "shadow" {
("shadow", "--shadow", "--tw-shadow", "--tw-shadow-color")
} else {
return None
}
let key = if name == root {
""
} else {
guard prefixed_value(name, root) is Some(key) else { return None }
key
}
if key == "none" {
return Some([decl(property, "0 0 #0000"), decl("box-shadow", shadow_stack)])
}
match shadow_color_pair(theme, "--box-shadow-color", key) {
Some((literal, value)) => {
let alpha_property = "\{color_property[:color_property.length() - 6]}-alpha"
return Some([
{
property: color_property,
value: "color-mix(in oklab, \{value} var(\{alpha_property}), transparent)",
fallback: Some(literal),
},
])
}
_ => ()
}
let value = if key == "" {
theme.get(theme_namespace)
} else {
match arbitrary_value(key) {
Some(value) => Some(value)
None => theme.get("\{theme_namespace}-\{key}")
}
}
// A modifier is the shadow's alpha, recorded in its own custom property and
// folded into the color slot's default.
let alpha = match modifier {
Some(source) =>
match alpha_modifier_value(theme, source) {
Some(alpha) => Some(alpha)
None => return None
}
None => None
}
value.map(fn(value) {
let declarations : Array[Declaration] = []
match alpha {
Some(alpha) =>
declarations.push(
decl("\{color_property[:color_property.length() - 6]}-alpha", alpha),
)
None => ()
}
declarations.push(
decl(property, shadow_value_with_color(value, color_property, alpha)),
)
declarations.push(decl("box-shadow", shadow_stack))
declarations
})
}
///|
fn text_shadow_utility(
theme : Map[String, String],
name : String,
modifier : String?,
) -> Array[Declaration]? {
guard prefixed_value(name, "text-shadow") is Some(key) else { return None }
match shadow_color_pair(theme, "--text-shadow-color", key) {
Some((literal, value)) =>
return Some([
{
property: "--tw-text-shadow-color",
value: "color-mix(in oklab, \{value} var(--tw-text-shadow-alpha), transparent)",
fallback: Some(literal),
},
])
_ => ()
}
let value = match arbitrary_value(key) {
Some(value) => Some(value)
None => theme.get("--text-shadow-\{key}")
}
let alpha = match modifier {
Some(source) =>
match alpha_modifier_value(theme, source) {
Some(alpha) => Some(alpha)
None => return None
}
None => None
}
value.map(fn(value) {
let declarations : Array[Declaration] = []
match alpha {
Some(alpha) => declarations.push(decl("--tw-text-shadow-alpha", alpha))
None => ()
}
declarations.push(
decl(
"text-shadow",
shadow_value_with_color(value, "--tw-text-shadow-color", alpha),
),
)
declarations
})
}
///|
fn gradient_color_or_position(
theme : Map[String, String],
key : String,
) -> (String, Bool)? {
// A stop position is a percentage written as a plain number, so `from-25.0%`
// and `from--5%` are not candidates.
if key.has_suffix("%") {
let number = key[:key.length() - 1].to_owned()
return if is_canonical_number(number) { Some((key, true)) } else { None }
}
match arbitrary_typed_value(key) {
// A typehint says which of the two a stop is; without one, a length-looking
// value is a position and anything else is a color.
Some((value, Some("length"))) | Some((value, Some("percentage"))) =>
Some((value, true))
Some((value, Some("color"))) => Some((value, false))
Some((value, _)) =>
Some(
(
value,
value.has_suffix("%") ||
value.has_suffix("px") ||
value.has_suffix("rem"),
),
)
None =>
match keyword_color(key) {
Some(value) => Some((value, false))
None =>
match theme_css_value(theme, "--color-\{key}") {
Some(value) => Some((value, false))
None => None
}
}
}
}
///|
fn gradient_utility(
theme : Map[String, String],
name : String,
) -> Array[Declaration]? {
let directions : Array[(String, String)] = [
("bg-linear-to-t", "to top"),
("bg-linear-to-tr", "to top right"),
("bg-linear-to-r", "to right"),
("bg-linear-to-br", "to bottom right"),
("bg-linear-to-b", "to bottom"),
("bg-linear-to-bl", "to bottom left"),
("bg-linear-to-l", "to left"),
("bg-linear-to-tl", "to top left"),
]
for entry in directions {
let (candidate, direction) = entry
if name == candidate {
return Some([
decl("--tw-gradient-position", direction),
decl("background-image", "linear-gradient(var(--tw-gradient-stops))"),
])
}
}
if name.has_prefix("bg-linear-") {
let key = name[10:].to_owned()
match arbitrary_value(key) {
Some(value) =>
return Some([
decl("--tw-gradient-position", value),
decl(
"background-image",
"linear-gradient(var(--tw-gradient-stops,\{value}))",
),
])
None => ()
}
}
let (root, color_property, position_property) = if name.has_prefix("from-") {
("from", "--tw-gradient-from", "--tw-gradient-from-position")
} else if name.has_prefix("via-") {
("via", "--tw-gradient-via", "--tw-gradient-via-position")
} else if name.has_prefix("to-") {
("to", "--tw-gradient-to", "--tw-gradient-to-position")
} else {
return None
}
guard prefixed_value(name, root) is Some(key) else { return None }
guard gradient_color_or_position(theme, key) is Some((value, position)) else {
return None
}
if position {
return Some([decl(position_property, value)])
}
let standard_stops = "var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))"
match root {
"via" =>
Some([
decl(color_property, value),
decl(
"--tw-gradient-via-stops", "var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position)",
),
decl("--tw-gradient-stops", "var(--tw-gradient-via-stops)"),
])
_ =>
Some([
decl(color_property, value),
decl("--tw-gradient-stops", standard_stops),
])
}
}
///|
/// The second field reports whether the utility consumed the candidate's
/// modifier — the shadows take one as an alpha on their color slot.
fn stateful_effect_utility(
theme : Map[String, String],
name : String,
modifier : String?,
) -> (Array[Declaration], Bool)? {
// The alpha property is written exactly when a modifier was taken as a
// shadow's alpha, so its presence is the answer.
fn consumed(declarations : Array[Declaration]) -> Bool {
declarations
.iter()
.any(fn(declaration) { declaration.property.has_suffix("-alpha") })
}
match shadow_ring_utility(theme, name, modifier) {
Some(value) => return Some((value, consumed(value)))
None => ()
}
match text_shadow_utility(theme, name, modifier) {
Some(value) => return Some((value, consumed(value)))
None => gradient_utility(theme, name).map(fn(value) { (value, false) })
}
}