AkurAI Build
Menu

Public repository / fee5b6c2cde89fa6bf8dc51068af7336b56a88b8

AkurAI-Framework / crates/css/src/escape.rs

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

//! CSS identifier escaping for class selectors.
//!
//! Utility classes contain characters that are special in a selector (`:` from
//! variants, `/` from fractions and opacity, `[`/`]` from arbitrary values, `.`
//! from `0.5`, …). [`class_selector`] turns a raw class into a valid `.class`
//! selector by backslash-escaping every byte outside `[A-Za-z0-9_-]` and
//! hex-escaping a leading digit (`2xl:` ⇒ `\32 xl\:`), following the CSSOM
//! `CSS.escape` rules.

/// Build the `.class` selector for `class`, escaped so it is valid CSS.
pub fn class_selector(class: &str) -> String {
    let mut out = String::with_capacity(class.len() + 8);
    out.push('.');
    out.push_str(&escape_ident(class));
    out
}

/// Escape `ident` per CSSOM `CSS.escape`.
pub fn escape_ident(ident: &str) -> String {
    let mut out = String::with_capacity(ident.len() + 4);
    let lone_hyphen = ident == "-";
    for (i, ch) in ident.chars().enumerate() {
        let leading_digit = ch.is_ascii_digit() && (i == 0 || (i == 1 && ident.starts_with('-')));
        if leading_digit {
            out.push_str(&format!("\\{:x} ", ch as u32));
        } else if is_plain(ch) && !lone_hyphen {
            out.push(ch);
        } else if ch.is_control() {
            out.push_str(&format!("\\{:x} ", ch as u32));
        } else {
            out.push('\\');
            out.push(ch);
        }
    }
    out
}

/// Characters that never need escaping inside an identifier.
fn is_plain(ch: char) -> bool {
    ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' || !ch.is_ascii()
}

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

    #[test]
    fn plain_classes_are_untouched() {
        assert_eq!(class_selector("p-2"), ".p-2");
        assert_eq!(class_selector("-mt-2"), ".-mt-2");
        assert_eq!(class_selector("snake_case"), ".snake_case");
    }

    #[test]
    fn variant_and_modifier_punctuation() {
        assert_eq!(
            class_selector("hover:bg-accent/90"),
            r".hover\:bg-accent\/90"
        );
        assert_eq!(class_selector("p-0.5"), r".p-0\.5");
        assert_eq!(class_selector("!p-0"), r".\!p-0");
    }

    #[test]
    fn arbitrary_value_punctuation() {
        assert_eq!(
            class_selector("data-[state=open]:x"),
            r".data-\[state\=open\]\:x"
        );
        assert_eq!(class_selector("bg-[#fff]"), r".bg-\[\#fff\]");
        assert_eq!(class_selector("w-[50%]"), r".w-\[50\%\]");
        assert_eq!(
            class_selector("w-[calc(1px,2px)]"),
            r".w-\[calc\(1px\,2px\)\]"
        );
    }

    #[test]
    fn leading_digits_are_hex_escaped() {
        assert_eq!(class_selector("2xl:flex"), r".\32 xl\:flex");
        assert_eq!(escape_ident("-1x"), r"-\31 x");
        assert_eq!(escape_ident("-"), r"\-");
    }
}