AkurAI Build
Menu

Public repository / master

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

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

//! Class-token scanners.
//!
//! - [`classes`] extracts the class names used in `class="…"` / `class='…'`
//!   attributes from HTML/template source. Matching is byte-oriented and
//!   quote-agnostic; it requires a word boundary before `class` so identifiers
//!   like `className` or `myclass` are not matched.
//! - [`candidates`] extracts every *possible* class token from any source text
//!   (Rust string literals, JS, HTML): it splits on whitespace and code
//!   punctuation, but keeps a `[...]` arbitrary value together, so
//!   `w-[calc(100%-2rem)]` and `data-[state=open]:bg-panel` survive intact.
//!
//! Both return tokens in first-seen order, de-duplicated.

/// Extract the de-duplicated, first-seen-ordered set of class tokens used in
/// `class` attributes within `source`.
pub fn classes(source: &str) -> Vec<String> {
    let bytes = source.as_bytes();
    let mut out: Vec<String> = Vec::new();
    let mut i = 0;

    while i < bytes.len() {
        match find_class_attr(bytes, i) {
            Some((value_start, value_end, next)) => {
                let value = &source[value_start..value_end];
                for token in value.split_whitespace() {
                    if !out.iter().any(|c| c == token) {
                        out.push(token.to_string());
                    }
                }
                i = next;
            }
            None => break,
        }
    }

    out
}

/// Bytes that end a candidate token outside a `[...]` arbitrary value.
const DELIMITERS: &[u8] = b"\"'`,;(){}<>=";

/// Bytes that end a candidate token even inside `[...]`: whitespace and quotes
/// (arbitrary values spell spaces as `_` and never contain raw quotes, so a
/// quote means the bracket was code such as `vec!["p-2"]`, not a value).
fn ends_bracket(b: u8) -> bool {
    b.is_ascii_whitespace() || b == b'"' || b == b'\'' || b == b'`'
}

/// Extract every candidate utility token from arbitrary source text, in
/// first-seen order, de-duplicated.
///
/// Tokens are split on whitespace and on `"` `'` `` ` `` `,` `;` `(` `)` `{`
/// `}` `<` `>` `=`. Inside square brackets only whitespace and quotes split, so
/// arbitrary values keep their commas, parentheses and `=`. Most tokens will
/// not be utilities; the generator ignores those.
pub fn candidates(source: &str) -> Vec<String> {
    let bytes = source.as_bytes();
    let mut out = Unique::default();
    let mut start: Option<usize> = None;
    let mut depth = 0usize;

    for (i, &b) in bytes.iter().enumerate() {
        let splits = if depth > 0 {
            ends_bracket(b)
        } else {
            b.is_ascii_whitespace() || DELIMITERS.contains(&b)
        };
        if splits {
            if let Some(s) = start.take() {
                out.push(&source[s..i]);
            }
            depth = 0;
            continue;
        }
        start.get_or_insert(i);
        match b {
            b'[' => depth += 1,
            b']' => depth = depth.saturating_sub(1),
            _ => {}
        }
    }
    if let Some(s) = start {
        out.push(&source[s..]);
    }
    out.tokens
}

/// First-seen-ordered set of tokens (linear time for large sources).
#[derive(Default)]
struct Unique<'a> {
    seen: std::collections::HashSet<&'a str>,
    tokens: Vec<String>,
}

impl<'a> Unique<'a> {
    fn push(&mut self, token: &'a str) {
        if !token.is_empty() && self.seen.insert(token) {
            self.tokens.push(token.to_string());
        }
    }
}

/// Locate the next `class=…"value"` attribute at or after `from`.
///
/// Returns `(value_start, value_end, next_index)` where the value byte range is
/// the text between the quotes and `next_index` is just past the closing quote.
fn find_class_attr(bytes: &[u8], from: usize) -> Option<(usize, usize, usize)> {
    let mut i = from;
    while i + 5 <= bytes.len() {
        if matches_keyword(bytes, i) {
            // Word boundary before `class`: start-of-input or a non-identifier byte.
            let boundary = i == 0 || !is_ident_byte(bytes[i - 1]);
            if boundary {
                let mut j = i + 5;
                j = skip_ws(bytes, j);
                if j < bytes.len() && bytes[j] == b'=' {
                    j = skip_ws(bytes, j + 1);
                    if j < bytes.len() && (bytes[j] == b'"' || bytes[j] == b'\'') {
                        let quote = bytes[j];
                        let value_start = j + 1;
                        if let Some(close) = find_byte(bytes, value_start, quote) {
                            return Some((value_start, close, close + 1));
                        }
                    }
                }
            }
        }
        i += 1;
    }
    None
}

fn matches_keyword(bytes: &[u8], i: usize) -> bool {
    bytes[i..].starts_with(b"class")
}

fn skip_ws(bytes: &[u8], mut i: usize) -> usize {
    while i < bytes.len() && bytes[i].is_ascii_whitespace() {
        i += 1;
    }
    i
}

fn find_byte(bytes: &[u8], from: usize, target: u8) -> Option<usize> {
    (from..bytes.len()).find(|&i| bytes[i] == target)
}

fn is_ident_byte(b: u8) -> bool {
    b.is_ascii_alphanumeric() || b == b'_' || b == b'-'
}

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

    #[test]
    fn single_attribute_multiple_classes() {
        assert_eq!(classes(r#"<div class="a b c">"#), vec!["a", "b", "c"]);
    }

    #[test]
    fn deduplicates_across_attributes() {
        let src = r#"<p class="a b"></p><span class="b c"></span>"#;
        assert_eq!(classes(src), vec!["a", "b", "c"]);
    }

    #[test]
    fn handles_single_quotes() {
        assert_eq!(classes("<div class='x y'>"), vec!["x", "y"]);
    }

    #[test]
    fn ignores_non_class_identifiers() {
        // `className` (JS) and a stray `myclass=` must not match.
        let src = r#"<div className="nope" myclass="no"><b class="yes">"#;
        assert_eq!(classes(src), vec!["yes"]);
    }

    #[test]
    fn tolerates_whitespace_around_equals() {
        assert_eq!(classes(r#"<div class = "a">"#), vec!["a"]);
    }

    #[test]
    fn collapses_extra_internal_whitespace() {
        assert_eq!(classes("<div class=\"  a   b \">"), vec!["a", "b"]);
    }

    #[test]
    fn candidates_split_on_code_punctuation() {
        let src = r#"el("div").class("flex p-2"); x = {a: 'm-1', b: `gap-2`}"#;
        let got = candidates(src);
        for want in ["flex", "p-2", "m-1", "gap-2"] {
            assert!(got.iter().any(|c| c == want), "missing {want}: {got:?}");
        }
        assert!(got.contains(&"el".to_string()));
        assert!(!got.iter().any(|c| c.contains('"') || c.contains('(')));
    }

    #[test]
    fn candidates_keep_arbitrary_values_together() {
        let src = r#"class("w-[220px] grid-cols-[1fr_2fr] data-[state=open]:bg-panel w-[calc(100%-2rem)] max-w-[min(1px,2px)]")"#;
        assert_eq!(
            candidates(src),
            vec![
                "class",
                "w-[220px]",
                "grid-cols-[1fr_2fr]",
                "data-[state=open]:bg-panel",
                "w-[calc(100%-2rem)]",
                "max-w-[min(1px,2px)]",
            ]
        );
    }

    #[test]
    fn candidates_do_not_swallow_code_brackets() {
        let src = r#"let xs = vec!["p-2", "m-1"]; arr[i] = "flex";"#;
        let got = candidates(src);
        for want in ["p-2", "m-1", "flex"] {
            assert!(got.iter().any(|c| c == want), "missing {want}: {got:?}");
        }
    }

    #[test]
    fn candidates_dedupe_in_first_seen_order() {
        assert_eq!(candidates("b a\nb 'a' c"), vec!["b", "a", "c"]);
        assert!(candidates("  \n\t ").is_empty());
    }

    #[test]
    fn empty_and_no_class() {
        assert!(classes("<div>no classes</div>").is_empty());
        assert!(classes(r#"<div class="">"#).is_empty());
    }
}