AkurAI Build
Menu

Public repository / master

AkurAI-Framework / crates/css/src/utilities/flex_grid.rs

Latest change 3aae9f4e85241bcb775c31f5c03094becd3da56a - Pure-Rust UI layer: akurai-ui, akurai-crypto, Tailwind-grade akurai-css by Ólafur Búi Ólafsson

//! Flexbox and grid: direction/wrap, flex/grow/shrink/basis, order, alignment
//! (`items-*`, `justify-*`, `content-*`, `self-*`, `place-*`), grid templates,
//! spans/starts/ends, auto flow and implicit tracks.

use super::value::{arbitrary, integer, length_or_fraction, positive};
use super::{keyword, Utility};

const KEYWORDS: &[(&str, &str)] = &[
    ("flex-row", "flex-direction:row"),
    ("flex-row-reverse", "flex-direction:row-reverse"),
    ("flex-col", "flex-direction:column"),
    ("flex-col-reverse", "flex-direction:column-reverse"),
    ("flex-wrap", "flex-wrap:wrap"),
    ("flex-wrap-reverse", "flex-wrap:wrap-reverse"),
    ("flex-nowrap", "flex-wrap:nowrap"),
    ("flex-1", "flex:1 1 0%"),
    ("flex-auto", "flex:1 1 auto"),
    ("flex-initial", "flex:0 1 auto"),
    ("flex-none", "flex:none"),
    ("order-first", "order:-9999"),
    ("order-last", "order:9999"),
    ("order-none", "order:0"),
    ("grid-cols-none", "grid-template-columns:none"),
    ("grid-cols-subgrid", "grid-template-columns:subgrid"),
    ("grid-rows-none", "grid-template-rows:none"),
    ("grid-rows-subgrid", "grid-template-rows:subgrid"),
    ("col-auto", "grid-column:auto"),
    ("col-span-full", "grid-column:1 / -1"),
    ("row-auto", "grid-row:auto"),
    ("row-span-full", "grid-row:1 / -1"),
    ("grid-flow-row", "grid-auto-flow:row"),
    ("grid-flow-col", "grid-auto-flow:column"),
    ("grid-flow-dense", "grid-auto-flow:dense"),
    ("grid-flow-row-dense", "grid-auto-flow:row dense"),
    ("grid-flow-col-dense", "grid-auto-flow:column dense"),
];

/// Tiered keywords: these override the `flex` shorthand, so they rank later.
const LONGHANDS: &[(&str, &str)] = &[
    ("grow", "flex-grow:1"),
    ("grow-0", "flex-grow:0"),
    ("shrink", "flex-shrink:1"),
    ("shrink-0", "flex-shrink:0"),
];

/// `(prefix, property, values as (suffix, css))` alignment tables.
type Align = (
    &'static str,
    &'static str,
    &'static [(&'static str, &'static str)],
);

const CONTENT_VALUES: &[(&str, &str)] = &[
    ("start", "flex-start"),
    ("end", "flex-end"),
    ("center", "center"),
    ("between", "space-between"),
    ("around", "space-around"),
    ("evenly", "space-evenly"),
    ("stretch", "stretch"),
    ("baseline", "baseline"),
    ("normal", "normal"),
];

const ITEM_VALUES: &[(&str, &str)] = &[
    ("start", "flex-start"),
    ("end", "flex-end"),
    ("center", "center"),
    ("baseline", "baseline"),
    ("stretch", "stretch"),
];

const SELF_VALUES: &[(&str, &str)] = &[
    ("auto", "auto"),
    ("start", "flex-start"),
    ("end", "flex-end"),
    ("center", "center"),
    ("stretch", "stretch"),
    ("baseline", "baseline"),
];

const PLACE_VALUES: &[(&str, &str)] = &[
    ("start", "start"),
    ("end", "end"),
    ("center", "center"),
    ("stretch", "stretch"),
    ("between", "space-between"),
    ("around", "space-around"),
    ("evenly", "space-evenly"),
    ("baseline", "baseline"),
    ("auto", "auto"),
];

/// Longest prefixes first so `justify-items-*` is not read as `justify-*`.
const ALIGN: &[Align] = &[
    ("justify-items", "justify-items", PLACE_VALUES),
    ("justify-self", "justify-self", PLACE_VALUES),
    ("justify", "justify-content", CONTENT_VALUES),
    ("place-content", "place-content", PLACE_VALUES),
    ("place-items", "place-items", PLACE_VALUES),
    ("place-self", "place-self", PLACE_VALUES),
    ("items", "align-items", ITEM_VALUES),
    ("content", "align-content", CONTENT_VALUES),
    ("self", "align-self", SELF_VALUES),
];

const AUTO_TRACKS: &[(&str, &str)] = &[
    ("auto", "auto"),
    ("min", "min-content"),
    ("max", "max-content"),
    ("fr", "minmax(0,1fr)"),
];

pub fn resolve(u: &str) -> Option<Utility> {
    keyword(KEYWORDS, u)
        .or_else(|| keyword(LONGHANDS, u).map(|x| x.tier(1)))
        .or_else(|| align(u))
        .or_else(|| templates(u))
        .or_else(|| placement(u))
        .or_else(|| auto_tracks(u))
        .or_else(|| basis(u))
        .or_else(|| order(u, false))
        .or_else(|| arbitrary_flex(u))
}

/// `-order-1`.
pub fn negative(u: &str) -> Option<Utility> {
    order(u, true)
}

fn align(u: &str) -> Option<Utility> {
    ALIGN.iter().find_map(|&(prefix, prop, values)| {
        let v = u.strip_prefix(prefix)?.strip_prefix('-')?;
        let (_, css) = values.iter().find(|(k, _)| *k == v)?;
        Some(Utility::decl(prop, css))
    })
}

/// `grid-cols-3` ⇒ `repeat(3,minmax(0,1fr))`, `grid-cols-[1fr_2fr]`.
fn templates(u: &str) -> Option<Utility> {
    let (prop, v) = if let Some(v) = u.strip_prefix("grid-cols-") {
        ("grid-template-columns", v)
    } else {
        ("grid-template-rows", u.strip_prefix("grid-rows-")?)
    };
    let value = match positive(v) {
        Some(n) => format!("repeat({n},minmax(0,1fr))"),
        None => arbitrary(v)?,
    };
    Some(Utility::decl(prop, &value))
}

/// `col-span-2`, `col-start-1`, `col-end-3`, and the `row-*` equivalents.
fn placement(u: &str) -> Option<Utility> {
    let (axis, rest) = if let Some(rest) = u.strip_prefix("col-") {
        ("grid-column", rest)
    } else {
        ("grid-row", u.strip_prefix("row-")?)
    };
    let (kind, v) = rest.split_once('-')?;
    let value = match kind {
        "span" => {
            let n = positive(v)?;
            return Some(Utility::decl(axis, &format!("span {n} / span {n}")));
        }
        "start" | "end" if v == "auto" => "auto".to_string(),
        "start" | "end" => positive(v)
            .map(|n| n.to_string())
            .or_else(|| arbitrary(v))?,
        _ => return None,
    };
    Some(Utility::decl(&format!("{axis}-{kind}"), &value).tier(1))
}

fn auto_tracks(u: &str) -> Option<Utility> {
    let (prop, v) = if let Some(v) = u.strip_prefix("auto-cols-") {
        ("grid-auto-columns", v)
    } else {
        ("grid-auto-rows", u.strip_prefix("auto-rows-")?)
    };
    let value = AUTO_TRACKS
        .iter()
        .find(|(k, _)| *k == v)
        .map(|(_, css)| css.to_string())
        .or_else(|| arbitrary(v))?;
    Some(Utility::decl(prop, &value))
}

fn basis(u: &str) -> Option<Utility> {
    let v = u.strip_prefix("basis-")?;
    let value = if v == "auto" {
        "auto".to_string()
    } else {
        length_or_fraction(v)?
    };
    Some(Utility::decl("flex-basis", &value).tier(1))
}

fn order(u: &str, neg: bool) -> Option<Utility> {
    let n = integer(u.strip_prefix("order-")?)?;
    let sign = if neg && n != 0 { "-" } else { "" };
    Some(Utility::decl("order", &format!("{sign}{n}")))
}

/// `flex-[2_2_0%]`.
fn arbitrary_flex(u: &str) -> Option<Utility> {
    Some(Utility::decl("flex", &arbitrary(u.strip_prefix("flex-")?)?))
}

#[cfg(test)]
mod tests {
    use super::*;

    fn decl(u: &str) -> Option<String> {
        resolve(u).map(|x| x.declaration())
    }

    #[test]
    fn alignment_prefixes_do_not_shadow_each_other() {
        assert_eq!(
            decl("justify-items-start").as_deref(),
            Some("justify-items:start")
        );
        assert_eq!(
            decl("justify-start").as_deref(),
            Some("justify-content:flex-start")
        );
        assert_eq!(
            decl("justify-self-auto").as_deref(),
            Some("justify-self:auto")
        );
        assert_eq!(decl("self-auto").as_deref(), Some("align-self:auto"));
        assert_eq!(decl("items-between"), None);
    }

    #[test]
    fn placement_values() {
        assert_eq!(decl("row-start-2").as_deref(), Some("grid-row-start:2"));
        assert_eq!(
            decl("col-end-auto").as_deref(),
            Some("grid-column-end:auto")
        );
        assert_eq!(decl("col-span-0"), None);
        assert_eq!(decl("col-wat-2"), None);
    }

    #[test]
    fn flex_longhands_rank_after_shorthand() {
        assert_eq!(resolve("flex-1").unwrap().tier, 0);
        assert_eq!(resolve("grow").unwrap().tier, 1);
        assert_eq!(resolve("basis-4").unwrap().tier, 1);
    }

    #[test]
    fn negative_order() {
        assert_eq!(
            negative("order-2").map(|x| x.declaration()).as_deref(),
            Some("order:-2")
        );
        assert_eq!(negative("order-last"), None);
    }
}