Menu
Public repository / master
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"\-");
}
}