///|
/// Expansion of a shorthand declaration into its longhand form.
///
/// This mirrors upstream `expand-declaration.ts`. Tailwind uses it to compare
/// candidates that mean the same thing while spelling it differently, such as
/// `p-4` versus `pt-4 pr-4 pb-4 pl-4`, so it operates on declarations rather
/// than on rendered CSS.
///|
/// Longhand properties of a four-sided shorthand, in `top right bottom left`
/// order.
fn side_expansion(
prefix : String,
top : String,
right : String,
bottom : String,
left : String,
) -> Array[String] {
[
"\{prefix}-\{top}",
"\{prefix}-\{right}",
"\{prefix}-\{bottom}",
"\{prefix}-\{left}",
]
}
///|
fn box_expansion(prefix : String) -> Array[String] {
side_expansion(prefix, "top", "right", "bottom", "left")
}
///|
/// Map the parts of a shorthand value onto four longhand properties.
fn expand_sides(
properties : ArrayView[String],
parts : ArrayView[String],
) -> Array[Declaration]? {
let picks : Array[Int] = match parts.length() {
1 => [0, 0, 0, 0]
2 => [0, 1, 0, 1]
3 => [0, 1, 2, 1]
4 => [0, 1, 2, 3]
_ => return None
}
let expanded : Array[Declaration] = []
for index, property in properties {
expanded.push(decl(property, parts[picks[index]]))
}
Some(expanded)
}
///|
/// Map the parts of a shorthand value onto two longhand properties.
fn expand_pair(
properties : ArrayView[String],
parts : ArrayView[String],
) -> Array[Declaration]? {
let picks : Array[Int] = match parts.length() {
1 => [0, 0]
2 => [0, 1]
_ => return None
}
let expanded : Array[Declaration] = []
for index, property in properties {
expanded.push(decl(property, parts[picks[index]]))
}
Some(expanded)
}
///|
/// Shorthands that always expand, independent of writing direction.
fn physical_shorthand(property : String) -> (Array[String], Bool)? {
match property {
"inset" => Some((["top", "right", "bottom", "left"], true))
"margin" => Some((box_expansion("margin"), true))
"padding" => Some((box_expansion("padding"), true))
"scroll-margin" => Some((box_expansion("scroll-margin"), true))
"scroll-padding" => Some((box_expansion("scroll-padding"), true))
"border-width" =>
Some(
(
side_expansion(
"border", "top-width", "right-width", "bottom-width", "left-width",
),
true,
),
)
"border-style" =>
Some(
(
side_expansion(
"border", "top-style", "right-style", "bottom-style", "left-style",
),
true,
),
)
"border-color" =>
Some(
(
side_expansion(
"border", "top-color", "right-color", "bottom-color", "left-color",
),
true,
),
)
"gap" => Some((["row-gap", "column-gap"], false))
"overflow" => Some((["overflow-x", "overflow-y"], false))
"overscroll-behavior" =>
Some((["overscroll-behavior-x", "overscroll-behavior-y"], false))
_ => None
}
}
///|
/// Logical shorthands that expand only when physical properties are requested.
fn logical_shorthand(property : String) -> Array[String]? {
match property {
"inset-block" => Some(["top", "bottom"])
"inset-inline" => Some(["left", "right"])
"margin-block" => Some(["margin-top", "margin-bottom"])
"margin-inline" => Some(["margin-left", "margin-right"])
"padding-block" => Some(["padding-top", "padding-bottom"])
"padding-inline" => Some(["padding-left", "padding-right"])
"scroll-margin-block" => Some(["scroll-margin-top", "scroll-margin-bottom"])
"scroll-margin-inline" =>
Some(["scroll-margin-left", "scroll-margin-right"])
"scroll-padding-block" =>
Some(["scroll-padding-top", "scroll-padding-bottom"])
"scroll-padding-inline" =>
Some(["scroll-padding-left", "scroll-padding-right"])
_ => None
}
}
///|
/// Logical border shorthands whose value is copied to each physical property
/// instead of being split.
fn logical_border_shorthand(property : String) -> Array[String]? {
match property {
"border-block" => Some(["border-bottom", "border-top"])
"border-block-color" => Some(["border-bottom-color", "border-top-color"])
"border-block-style" => Some(["border-bottom-style", "border-top-style"])
"border-block-width" => Some(["border-bottom-width", "border-top-width"])
"border-inline" => Some(["border-left", "border-right"])
"border-inline-color" => Some(["border-left-color", "border-right-color"])
"border-inline-style" => Some(["border-left-style", "border-right-style"])
"border-inline-width" => Some(["border-left-width", "border-right-width"])
_ => None
}
}
///|
/// Expand a shorthand declaration, or return `None` when it has no expansion.
///
/// `logical_to_physical` enables the direction-dependent expansions, which are
/// only correct when both declarations are known to be laid out left to right.
///
/// Upstream calls this from candidate canonicalization rather than from
/// `compile`, so it is exercised by white-box tests instead of by the public
/// build path.
#warnings("-unused_value")
fn expand_declaration(
declaration : Declaration,
logical_to_physical? : Bool = false,
) -> Array[Declaration]? {
let parts = split_top_level(declaration.value, ' ')
if logical_to_physical {
match logical_shorthand(declaration.property) {
Some(properties) => return expand_pair(properties, parts)
None => ()
}
match logical_border_shorthand(declaration.property) {
Some(properties) =>
return Some(
properties.map(fn(property) { decl(property, declaration.value) }),
)
None => ()
}
}
match physical_shorthand(declaration.property) {
Some((properties, four_sided)) =>
if four_sided {
expand_sides(properties, parts)
} else {
expand_pair(properties, parts)
}
None => None
}
}