Menu
Public repository / master
AkurAI-Framework / crates/css/src/generate.rs
Latest change 3aae9f4e85241bcb775c31f5c03094becd3da56a - Pure-Rust UI layer: akurai-ui, akurai-crypto, Tailwind-grade akurai-css by Ólafur Búi Ólafsson
//! Stylesheet generation: used class tokens → compact CSS.
//!
//! Each recognised class becomes one compact rule `selector{declarations}`;
//! unknown classes are skipped. Output has no whitespace between rules and is
//! fully deterministic. Rule order:
//!
//! 1. base utilities (no variants), then
//! 2. state/attribute variant utilities in Tailwind's precedence order
//! (`hover` < `focus` < `active` < `disabled` < `group-*` < `aria-*` <
//! `data-*`), then
//! 3. media blocks — `dark`/`motion-*`/`print` first, then the breakpoints in
//! ascending width — with consecutive rules sharing one `@media` block, and
//! 4. the `@property` registrations and `@keyframes` the used utilities need,
//! each once.
//!
//! Within a group rules keep first-seen order ([`stylesheet`]) or, in
//! canonical order ([`stylesheet_canonical`]), shorthands come before axis and
//! single-side longhands so `p-4 px-2` behaves like Tailwind regardless of
//! where each class was first seen.
use crate::escape::class_selector;
use crate::utilities;
use crate::variants::{split, Context};
/// How rules inside one group are ordered.
#[derive(Clone, Copy, PartialEq, Eq)]
enum Order {
/// Strictly first-seen.
FirstSeen,
/// Shorthand → axis → side tiers, then first-seen.
Canonical,
}
/// One rendered rule plus its sort key.
struct Rule {
/// `(breakpoint, media bits, variant rank, tier, first-seen index)`.
key: (u8, u8, u8, u8, usize),
media: Option<String>,
css: String,
extras: Vec<&'static str>,
}
/// Build a compact stylesheet for `classes`, keeping only recognised utilities.
/// Base rules keep the input (first-seen) order.
pub fn stylesheet(classes: &[String]) -> String {
render(classes, Order::FirstSeen)
}
/// Like [`stylesheet`], but orders shorthands before longhands within each
/// group (Tailwind's cascade), so the later, more specific utility wins.
pub fn stylesheet_canonical(classes: &[String]) -> String {
render(classes, Order::Canonical)
}
/// Render one class to its standalone CSS (including any `@media` wrapper),
/// or `None` if it is not a recognised utility. Extras are not included.
pub fn rule(class: &str) -> Option<String> {
let r = build_rule(class, 0, Order::FirstSeen)?;
Some(match r.media {
Some(q) => format!("@media {q}{{{}}}", r.css),
None => r.css,
})
}
fn render(classes: &[String], order: Order) -> String {
let mut rules: Vec<Rule> = classes
.iter()
.enumerate()
.filter_map(|(i, class)| build_rule(class, i, order))
.collect();
rules.sort_by_key(|r| r.key);
let mut out = String::new();
let mut open: Option<&str> = None;
for rule in &rules {
let media = rule.media.as_deref();
if media != open {
if open.is_some() {
out.push('}');
}
if let Some(q) = media {
out.push_str("@media ");
out.push_str(q);
out.push('{');
}
open = media;
}
out.push_str(&rule.css);
}
if open.is_some() {
out.push('}');
}
let mut extras: Vec<&str> = Vec::new();
for extra in rules.iter().flat_map(|r| r.extras.iter().copied()) {
if !extras.contains(&extra) {
extras.push(extra);
}
}
extras.sort_by_key(|e| !e.starts_with("@property"));
for extra in extras {
out.push_str(extra);
}
out
}
fn build_rule(class: &str, seen: usize, order: Order) -> Option<Rule> {
let parsed = split(class)?;
let utility = utilities::resolve(parsed.utility)?;
let mut ctx = Context::default();
for variant in &parsed.variants {
ctx.apply(variant)?;
}
let suffix = if parsed.important { "!important" } else { "" };
let body = utility
.decls
.iter()
.map(|d| format!("{d}{suffix}"))
.collect::<Vec<_>>()
.join(";");
let selector = ctx.selector(&class_selector(class), utility.child);
let tier = if order == Order::Canonical {
utility.tier
} else {
0
};
Some(Rule {
key: (ctx.breakpoint, ctx.media, ctx.rank, tier, seen),
media: ctx.media_query(),
css: format!("{selector}{{{body}}}"),
extras: utility.extras,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn owned(items: &[&str]) -> Vec<String> {
items.iter().map(|s| s.to_string()).collect()
}
#[test]
fn emits_only_known_utilities_in_order() {
let css = stylesheet(&owned(&["flex", "btn", "p-2"]));
assert_eq!(css, ".flex{display:flex}.p-2{padding:8px}");
}
#[test]
fn empty_input_yields_empty_string() {
assert_eq!(stylesheet(&[]), "");
}
#[test]
fn all_unknown_yields_empty_string() {
assert_eq!(stylesheet(&owned(&["card", "btn-primary"])), "");
}
#[test]
fn first_seen_keeps_longhand_position() {
let classes = owned(&["px-2", "p-4"]);
assert_eq!(
stylesheet(&classes),
".px-2{padding-left:8px;padding-right:8px}.p-4{padding:16px}"
);
assert_eq!(
stylesheet_canonical(&classes),
".p-4{padding:16px}.px-2{padding-left:8px;padding-right:8px}"
);
}
#[test]
fn single_rule_rendering() {
assert_eq!(rule("p-2").as_deref(), Some(".p-2{padding:8px}"));
assert_eq!(
rule("sm:p-2").as_deref(),
Some(r"@media (min-width:640px){.sm\:p-2{padding:8px}}")
);
assert_eq!(rule("nope"), None);
}
#[test]
fn media_groups_close_between_queries() {
let css = stylesheet(&owned(&["dark:flex", "sm:flex", "sm:block"]));
assert_eq!(
css,
concat!(
r"@media (prefers-color-scheme:dark){.dark\:flex{display:flex}}",
r"@media (min-width:640px){.sm\:flex{display:flex}.sm\:block{display:block}}",
)
);
}
#[test]
fn registrations_precede_keyframes() {
let css = stylesheet(&owned(&["animate-spin", "shadow"]));
let prop = css.find("@property").unwrap();
let kf = css.find("@keyframes").unwrap();
assert!(prop < kf);
}
}