Menu
Public repository / master
AkurAI-Framework / crates/css/src/utilities/border.rs
Latest change 3aae9f4e85241bcb775c31f5c03094becd3da56a - Pure-Rust UI layer: akurai-ui, akurai-crypto, Tailwind-grade akurai-css by Ólafur Búi Ólafsson
//! Borders: widths, styles, `divide-*` and radius.
//!
//! There is no preflight reset, so width utilities also set the side's
//! `border-style` from `var(--tw-border-style,solid)`; `border-dashed` and
//! friends only set that variable, so `border-t border-dashed` dashes the top
//! edge without turning on the other three.
//!
//! **Flat corners (house rule):** radius is at most 6px and there are no pills.
//! `rounded-sm` = 2px, `rounded` = 4px, `rounded-md` = 6px, and `rounded-lg`,
//! `-xl`, `-2xl`, `-3xl` and `-full` all clamp to 6px. Arbitrary radii clamp
//! too: `rounded-[20px]` ⇒ 6px, `rounded-[50%]` ⇒ `min(50%,6px)`.
use super::value::{arbitrary_hinted, integer, looks_like_length, trim_float};
use super::{register, Utility, BETWEEN_CHILDREN};
/// Largest radius any utility may emit, in px.
pub const MAX_RADIUS_PX: f64 = 6.0;
const BORDER_STYLE_PROPS: &[&str] = &[register!("--tw-border-style")];
/// `(suffix, sides, tier)` for the per-side border utilities.
const SIDES: &[(&str, &[&str], u8)] = &[
("x", &["left", "right"], 1),
("y", &["top", "bottom"], 1),
("t", &["top"], 2),
("r", &["right"], 2),
("b", &["bottom"], 2),
("l", &["left"], 2),
];
const RADII: &[(&str, &str)] = &[
("none", "0px"),
("sm", "2px"),
("md", "6px"),
("lg", "6px"),
("xl", "6px"),
("2xl", "6px"),
("3xl", "6px"),
("full", "6px"),
];
/// Radius when no size is given (`rounded`, `rounded-t`).
const DEFAULT_RADIUS: &str = "4px";
/// `(suffix, corners, tier)` for per-side/corner radius utilities.
const CORNERS: &[(&str, &[&str], u8)] = &[
("t", &["top-left", "top-right"], 1),
("r", &["top-right", "bottom-right"], 1),
("b", &["bottom-right", "bottom-left"], 1),
("l", &["top-left", "bottom-left"], 1),
("tl", &["top-left"], 2),
("tr", &["top-right"], 2),
("br", &["bottom-right"], 2),
("bl", &["bottom-left"], 2),
];
pub fn resolve(u: &str) -> Option<Utility> {
border_style(u)
.or_else(|| border_width(u))
.or_else(|| divide(u))
.or_else(|| rounded(u))
}
fn border_style(u: &str) -> Option<Utility> {
match u.strip_prefix("border-")? {
s @ ("solid" | "dashed" | "dotted" | "double") => {
Some(Utility::decl("--tw-border-style", s).extras(BORDER_STYLE_PROPS))
}
s @ ("none" | "hidden") => Some(
Utility::body(&format!("--tw-border-style:{s};border-style:{s}"))
.extras(BORDER_STYLE_PROPS),
),
_ => None,
}
}
/// `border`, `border-2`, `border-[3px]`, `border-t`, `border-x-4`.
fn border_width(u: &str) -> Option<Utility> {
let rest = u.strip_prefix("border")?;
if let Some(width) = width_suffix(rest) {
return Some(width_decls(None, &width).tier(0));
}
let rest = rest.strip_prefix('-')?;
SIDES.iter().find_map(|&(side, edges, tier)| {
let width = width_suffix(rest.strip_prefix(side)?)?;
let mut decls = Vec::new();
for edge in edges {
decls.extend(width_decls(Some(edge), &width).decls);
}
Some(Utility::from_decls(decls).tier(tier))
})
}
/// `""` ⇒ 1px, `-N` ⇒ Npx, `-[len]` ⇒ len.
fn width_suffix(rest: &str) -> Option<String> {
if rest.is_empty() {
return Some("1px".into());
}
let v = rest.strip_prefix('-')?;
if let Some(n) = integer(v) {
return Some(format!("{n}px"));
}
let (hint, value) = arbitrary_hinted(v)?;
let is_width = match hint.as_deref() {
Some("length") | Some("width") => true,
None => looks_like_length(&value),
Some(_) => false,
};
is_width.then_some(value)
}
/// Width plus the edge's style; a zero width needs no style.
fn width_decls(edge: Option<&str>, width: &str) -> Utility {
let prop = |what: &str| match edge {
Some(e) => format!("border-{e}-{what}"),
None => format!("border-{what}"),
};
let mut decls = Vec::new();
if width != "0px" {
decls.push(format!("{}:var(--tw-border-style,solid)", prop("style")));
}
decls.push(format!("{}:{width}", prop("width")));
Utility::from_decls(decls)
}
/// `divide-x`, `divide-y-2`: borders between children.
fn divide(u: &str) -> Option<Utility> {
let rest = u.strip_prefix("divide-")?;
let (edge, rest) = match rest.split_at_checked(1)? {
("x", rest) => ("left", rest),
("y", rest) => ("top", rest),
_ => return None,
};
let width = width_suffix(rest)?;
Some(
Utility::body(&format!(
"border-{edge}-style:solid;border-{edge}-width:{width}"
))
.child(BETWEEN_CHILDREN),
)
}
/// `rounded`, `rounded-md`, `rounded-t`, `rounded-tl-sm`, `rounded-[3px]`.
fn rounded(u: &str) -> Option<Utility> {
let rest = u.strip_prefix("rounded")?;
if rest.is_empty() {
return Some(Utility::decl("border-radius", DEFAULT_RADIUS));
}
let rest = rest.strip_prefix('-')?;
if let Some(r) = radius(rest) {
return Some(Utility::decl("border-radius", &r));
}
CORNERS.iter().find_map(|&(side, corners, tier)| {
let r = match rest.strip_prefix(side)? {
"" => DEFAULT_RADIUS.to_string(),
size => radius(size.strip_prefix('-')?)?,
};
let props: Vec<String> = corners
.iter()
.map(|c| format!("border-{c}-radius:{r}"))
.collect();
Some(Utility::from_decls(props).tier(tier))
})
}
/// A named or arbitrary radius, clamped to [`MAX_RADIUS_PX`].
fn radius(v: &str) -> Option<String> {
if let Some((_, r)) = RADII.iter().find(|(k, _)| *k == v) {
return Some((*r).to_string());
}
let (hint, value) = arbitrary_hinted(v)?;
if hint.is_some() {
return None;
}
Some(clamp_radius(&value))
}
/// Clamp an arbitrary radius to 6px: px/rem/em are compared numerically
/// (1rem = 16px); anything else is wrapped in `min(…,6px)`.
fn clamp_radius(value: &str) -> String {
let px = [("px", 1.0), ("rem", 16.0), ("em", 16.0)]
.iter()
.find_map(|(unit, scale)| {
let n: f64 = value.strip_suffix(unit)?.parse().ok()?;
Some(n * scale)
})
.or_else(|| (value == "0").then_some(0.0));
match px {
Some(px) if px > MAX_RADIUS_PX => format!("{}px", trim_float(MAX_RADIUS_PX)),
Some(_) => value.to_string(),
None => format!("min({value},{}px)", trim_float(MAX_RADIUS_PX)),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn decl(u: &str) -> Option<String> {
resolve(u).map(|x| x.declaration())
}
#[test]
fn clamp_rule() {
assert_eq!(clamp_radius("6px"), "6px");
assert_eq!(clamp_radius("6.5px"), "6px");
assert_eq!(clamp_radius("0.25rem"), "0.25rem");
assert_eq!(clamp_radius("1rem"), "6px");
assert_eq!(clamp_radius("var(--r)"), "min(var(--r),6px)");
for named in RADII {
let px: f64 = named.1.trim_end_matches("px").parse().unwrap();
assert!(
px <= MAX_RADIUS_PX,
"{} exceeds the flat-corner rule",
named.0
);
}
}
#[test]
fn corners_default_and_sized() {
assert_eq!(
decl("rounded-l").as_deref(),
Some("border-top-left-radius:4px;border-bottom-left-radius:4px")
);
assert_eq!(
decl("rounded-tl-[1px]").as_deref(),
Some("border-top-left-radius:1px")
);
assert_eq!(decl("rounded-q"), None);
assert_eq!(decl("rounded-t-q"), None);
}
#[test]
fn widths_and_sides() {
assert_eq!(
decl("border-b-0").as_deref(),
Some("border-bottom-width:0px")
);
assert_eq!(decl("border-accent"), None);
assert_eq!(decl("border-t-accent"), None);
assert_eq!(decl("border-[#fff]"), None);
assert_eq!(
decl("border-[length:var(--w)]").as_deref(),
Some("border-style:var(--tw-border-style,solid);border-width:var(--w)")
);
assert_eq!(decl("divide-z"), None);
}
}