///|
/// The sides a `mask--*` utility drives, in the order upstream emits them.
let mask_edges : Array[(String, Array[String])] = [
("mask-t", ["top"]),
("mask-r", ["right"]),
("mask-b", ["bottom"]),
("mask-l", ["left"]),
("mask-x", ["right", "left"]),
("mask-y", ["top", "bottom"]),
]
///|
/// The plumbing every mask utility re-declares, so that several of them
/// combine: the image is composed from the three gradient slots, and the linear
/// slot from the four sides.
fn mask_composition_declarations() -> Array[Declaration] {
[
decl(
"mask-image", "var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic)",
),
decl("mask-composite", "intersect"),
decl(
"--tw-mask-linear", "var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top)",
),
]
}
///|
/// A mask stop is a position or a color, the same split the gradient stops
/// make — but a bare number here is a spacing multiplier, and an untyped
/// arbitrary value is a color unless it reads as a measurement.
fn mask_stop_value(
theme : Map[String, String],
key : String,
) -> (String, Bool)? {
// A percentage stop is a whole number here, unlike the gradient stops.
if key.has_suffix("%") {
let number = key[:key.length() - 1].to_owned()
return if is_nonnegative_integer(number) { Some((key, true)) } else { None }
}
match arbitrary_typed_value(key) {
Some((value, Some("color"))) => return Some((value, false))
Some((value, Some(_))) => return Some((value, true))
// Without a typehint a mask stop is a position: upstream lists the length
// types before the color one, and a bare `var()` infers nothing. A negative
// measurement is not a position at all.
Some((value, None)) =>
return if value.has_prefix("-") && is_number_with_unit(value) {
None
} else {
Some((value, true))
}
None => ()
}
match keyword_color(key) {
Some(value) => return Some((value, false))
None => ()
}
match theme_css_value(theme, "--color-\{key}") {
Some(value) => return Some((value, false))
None => ()
}
numeric_spacing(theme, key).map(fn(value) { (value, true) })
}
///|
/// `mask-t-from-50%` and friends: a linear gradient masking one edge, or two
/// for the `x` and `y` forms. The value is a stop, read the same way a gradient
/// stop is — a position or a color.
fn mask_edge_utility(
theme : Map[String, String],
name : String,
) -> Array[Declaration]? {
guard name.has_prefix("mask-") else { return None }
for entry in mask_edges {
let (root, sides) = entry
for stop in ["from", "to"] {
let prefix = "\{root}-\{stop}"
guard prefixed_value(name, prefix) is Some(key) else { continue }
guard mask_stop_value(theme, key) is Some((value, position)) else {
return None
}
let declarations = mask_composition_declarations()
for side in sides {
declarations.push(
decl(
"--tw-mask-\{side}",
"linear-gradient(to \{side}, var(--tw-mask-\{side}-from-color) var(--tw-mask-\{side}-from-position), var(--tw-mask-\{side}-to-color) var(--tw-mask-\{side}-to-position))",
),
)
let slot = if position { "position" } else { "color" }
declarations.push(decl("--tw-mask-\{side}-\{stop}-\{slot}", value))
}
return Some(declarations)
}
}
None
}
///|
/// The three gradient mask slots. Each composes a gradient of its own kind from
/// a `*-stops` list, and takes `from`/`to` stops the way the edges do.
let mask_gradients : Array[(String, String, String)] = [
(
"mask-linear", "linear", "var(--tw-mask-linear-position), var(--tw-mask-linear-from-color) var(--tw-mask-linear-from-position), var(--tw-mask-linear-to-color) var(--tw-mask-linear-to-position)",
),
(
"mask-conic", "conic", "from var(--tw-mask-conic-position), var(--tw-mask-conic-from-color) var(--tw-mask-conic-from-position), var(--tw-mask-conic-to-color) var(--tw-mask-conic-to-position)",
),
(
"mask-radial", "radial", "var(--tw-mask-radial-shape) var(--tw-mask-radial-size) at var(--tw-mask-radial-position), var(--tw-mask-radial-from-color) var(--tw-mask-radial-from-position), var(--tw-mask-radial-to-color) var(--tw-mask-radial-to-position)",
),
]
///|
/// `mask-linear-45`, `mask-conic-from-20%`, `mask-radial-[circle]` and the rest
/// of the gradient masks.
fn mask_gradient_utility(
theme : Map[String, String],
name : String,
) -> Array[Declaration]? {
guard name.has_prefix("mask-") else { return None }
for entry in mask_gradients {
let (root, kind, stops) = entry
let slot = "--tw-mask-\{kind}"
for stop in ["from", "to"] {
guard prefixed_value(name, "\{root}-\{stop}") is Some(key) else {
continue
}
guard mask_stop_value(theme, key) is Some((value, position)) else {
return None
}
let declarations = mask_composition_declarations()
declarations.remove(2) |> ignore
declarations.push(decl("\{slot}-stops", stops))
declarations.push(decl(slot, "\{kind}-gradient(var(\{slot}-stops))"))
let field = if position { "position" } else { "color" }
declarations.push(decl("\{slot}-\{stop}-\{field}", value))
return Some(declarations)
}
// `mask-radial-at-*` moves the radial center; the keyword forms are statics.
if kind == "radial" && prefixed_value(name, "\{root}-at") is Some(key) {
// The keyword positions are statics; only an arbitrary one lands here.
match arbitrary_value(key) {
Some(value) => return Some([decl("\{slot}-position", value)])
None => ()
}
}
guard prefixed_value(name, root) is Some(key) else { continue }
// The bare form sets the gradient's own parameter: an angle for the linear
// and conic masks, a size for the radial one.
let property = if kind == "radial" {
"\{slot}-size"
} else {
"\{slot}-position"
}
let resolved = match arbitrary_value(key) {
Some(value) => Some(value)
None =>
if kind == "radial" {
None
} else if key == "0" {
// As with the spacing scale, a zero multiplier folds away.
Some("0deg")
} else if key == "1" {
Some("1deg")
} else if is_nonnegative_integer(key) {
Some("calc(1deg * \{key})")
} else {
None
}
}
// A keyword form — `mask-radial-at-bottom`, `mask-radial-circle` — is a
// static utility, so leave it to that rather than claiming the name.
guard resolved is Some(value) else { continue }
let declarations = mask_composition_declarations()
declarations.remove(2) |> ignore
declarations.push(
decl(slot, "\{kind}-gradient(var(\{slot}-stops, var(\{property})))"),
)
declarations.push(decl(property, value))
return Some(declarations)
}
None
}
///|
/// `mask-position-*` and `mask-size-*` write their property outright.
fn mask_placement_utility(
theme : Map[String, String],
name : String,
) -> Array[Declaration]? {
for entry in [("mask-position", "mask-position"), ("mask-size", "mask-size")] {
let (root, property) = entry
guard prefixed_value(name, root) is Some(key) else { continue }
let value = match arbitrary_value(key) {
Some(value) => Some(value)
None => theme_css_value(theme, "--\{property}-\{key}")
}
guard value is Some(value) else { return None }
return Some([decl(property, value)])
}
None
}
///|
/// `mask-radial-at-*` moves the radial center. The keyword forms name a corner
/// or an edge; the two-word ones join with a space.
fn mask_radial_position_utility(name : String) -> Array[Declaration]? {
guard prefixed_value(name, "mask-radial-at") is Some(key) else { return None }
for
position in [
"center", "top", "right", "bottom", "left", "top-left", "top-right", "bottom-left",
"bottom-right",
] {
if key == position {
return Some([
decl("--tw-mask-radial-position", replace_all(position, "-", " ")),
])
}
}
None
}