workers-rsx-impl 0.1.0

Proc macros for workers-rsx
Documentation
use proc_macro2::TokenTree;

/// Reconstruct a CSS string from a proc-macro token stream.
///
/// Handles the quirks of Rust tokenization:
/// - `.class` tokenized as Punct('.') Ident(class) → joined without space
/// - `#id` tokenized as Punct('#') Ident(id) → joined without space
/// - `margin-left` tokenized as Ident(margin) Punct('-') Ident(left) → joined without space
/// - `10px` tokenized as Literal(10) Ident(px) → joined without space
/// - `0.5em` tokenized as Literal(0.5) Ident(em) → joined without space
/// - `@media` tokenized as Punct('@') Ident(media) → joined without space
/// - `:hover` / `::before` → joined without space
pub fn tokens_to_css(input: proc_macro2::TokenStream) -> String {
    let tokens: Vec<TokenTree> = input.into_iter().collect();
    let mut result = String::new();
    build_css(&tokens, &mut result);
    result
}

fn build_css(tokens: &[TokenTree], out: &mut String) {
    let len = tokens.len();
    let mut i = 0;

    while i < len {
        match &tokens[i] {
            TokenTree::Group(group) => {
                let delim = group.delimiter();
                match delim {
                    proc_macro2::Delimiter::Brace => {
                        out.push('{');
                        let inner: Vec<TokenTree> = group.stream().into_iter().collect();
                        build_css(&inner, out);
                        out.push('}');
                    }
                    proc_macro2::Delimiter::Parenthesis => {
                        out.push('(');
                        let inner: Vec<TokenTree> = group.stream().into_iter().collect();
                        build_css(&inner, out);
                        out.push(')');
                    }
                    proc_macro2::Delimiter::Bracket => {
                        out.push('[');
                        let inner: Vec<TokenTree> = group.stream().into_iter().collect();
                        build_css(&inner, out);
                        out.push(']');
                    }
                    proc_macro2::Delimiter::None => {
                        let inner: Vec<TokenTree> = group.stream().into_iter().collect();
                        build_css(&inner, out);
                    }
                }
                i += 1;
            }
            TokenTree::Punct(p) => {
                let ch = p.as_char();
                match ch {
                    // `.class` or `.class1.class2` — no space before ident
                    '.' => {
                        // Check if previous char needs no space
                        if needs_space_before_punct(out) {
                            out.push(' ');
                        }
                        out.push('.');
                        // If next token is an ident or literal, join directly
                        if i + 1 < len && is_ident_or_literal(&tokens[i + 1]) {
                            i += 1;
                            push_token(&tokens[i], out);
                        }
                        i += 1;
                    }
                    // `#id` or `#ff0000`
                    '#' => {
                        if needs_space_before_punct(out) {
                            out.push(' ');
                        }
                        out.push('#');
                        if i + 1 < len && is_ident_or_literal(&tokens[i + 1]) {
                            i += 1;
                            push_token(&tokens[i], out);
                        }
                        i += 1;
                    }
                    // `@media`, `@keyframes`, etc.
                    '@' => {
                        if !out.is_empty() && !out.ends_with('{') && !out.ends_with('\n') {
                            out.push(' ');
                        }
                        out.push('@');
                        if i + 1 < len && is_ident(&tokens[i + 1]) {
                            i += 1;
                            push_token(&tokens[i], out);
                        }
                        i += 1;
                    }
                    // `-` could be part of a CSS property name like `margin-left`
                    // or a negative value like `-10px`
                    '-' => {
                        let prev_is_ident = i > 0 && is_ident(&tokens[i - 1]);
                        let next_is_ident_or_lit =
                            i + 1 < len && is_ident_or_literal(&tokens[i + 1]);

                        if prev_is_ident && next_is_ident_or_lit {
                            // `margin-left` pattern — join without spaces
                            out.push('-');
                        } else if next_is_ident_or_lit {
                            // Negative value like `-10px`
                            if needs_space_before_value(out) {
                                out.push(' ');
                            }
                            out.push('-');
                        } else {
                            out.push('-');
                        }
                        i += 1;
                    }
                    // `:` for property values or pseudo-selectors like `:hover`
                    ':' => {
                        out.push(':');
                        // `::before` — double colon
                        if i + 1 < len && is_punct_char(&tokens[i + 1], ':') {
                            out.push(':');
                            i += 1;
                        }
                        i += 1;
                    }
                    // `;` ends a declaration
                    ';' => {
                        out.push(';');
                        i += 1;
                    }
                    // `,` in selectors or values
                    ',' => {
                        out.push(',');
                        i += 1;
                    }
                    // `>`, `+`, `~` combinators
                    '>' | '+' | '~' => {
                        out.push(' ');
                        out.push(ch);
                        out.push(' ');
                        i += 1;
                    }
                    // `*` universal selector or in calc
                    '*' => {
                        if needs_space_before_value(out) {
                            out.push(' ');
                        }
                        out.push('*');
                        i += 1;
                    }
                    // `=` in attribute selectors
                    '=' => {
                        out.push('=');
                        i += 1;
                    }
                    // `!` for `!important`
                    '!' => {
                        out.push(' ');
                        out.push('!');
                        if i + 1 < len && is_ident(&tokens[i + 1]) {
                            i += 1;
                            push_token(&tokens[i], out);
                        }
                        i += 1;
                    }
                    // `%` for percentages
                    '%' => {
                        out.push('%');
                        i += 1;
                    }
                    _ => {
                        out.push(ch);
                        i += 1;
                    }
                }
            }
            TokenTree::Ident(ident) => {
                let s = ident.to_string();
                if needs_space_before_value(out) {
                    out.push(' ');
                }
                out.push_str(&s);

                // Check if next is a literal directly after ident (shouldn't normally happen)
                // or handle `10px` where literal comes before ident
                i += 1;
            }
            TokenTree::Literal(lit) => {
                let s = lit.to_string();
                if needs_space_before_value(out) {
                    out.push(' ');
                }
                // String literals: strip quotes and use contents directly
                if s.starts_with('"') && s.ends_with('"') {
                    out.push_str(&s[1..s.len() - 1]);
                } else {
                    out.push_str(&s);
                    // Check if next token is an ident (unit suffix like `px`, `em`, `rem`, `%`)
                    if i + 1 < len && is_ident(&tokens[i + 1]) {
                        // Could be a unit: join directly
                        if is_css_unit(&tokens[i + 1]) {
                            i += 1;
                            push_token(&tokens[i], out);
                        }
                    }
                }
                i += 1;
            }
        }
    }
}

fn push_token(token: &TokenTree, out: &mut String) {
    match token {
        TokenTree::Ident(ident) => out.push_str(&ident.to_string()),
        TokenTree::Literal(lit) => {
            let s = lit.to_string();
            if s.starts_with('"') && s.ends_with('"') {
                out.push_str(&s[1..s.len() - 1]);
            } else {
                out.push_str(&s);
            }
        }
        _ => out.push_str(&token.to_string()),
    }
}

fn is_ident(token: &TokenTree) -> bool {
    matches!(token, TokenTree::Ident(_))
}

fn is_ident_or_literal(token: &TokenTree) -> bool {
    matches!(token, TokenTree::Ident(_) | TokenTree::Literal(_))
}

fn is_punct_char(token: &TokenTree, ch: char) -> bool {
    matches!(token, TokenTree::Punct(p) if p.as_char() == ch)
}

fn is_css_unit(token: &TokenTree) -> bool {
    if let TokenTree::Ident(ident) = token {
        let s = ident.to_string();
        matches!(
            s.as_str(),
            "px" | "em" | "rem" | "vh" | "vw" | "vmin" | "vmax" | "ch" | "ex" | "cm" | "mm"
                | "in" | "pt" | "pc" | "fr" | "s" | "ms" | "deg" | "rad" | "grad" | "turn"
                | "dpi" | "dpcm" | "dppx" | "n" | "x"
        )
    } else {
        false
    }
}

fn needs_space_before_punct(out: &str) -> bool {
    if out.is_empty() {
        return false;
    }
    let last = out.chars().last().unwrap();
    // No space after these
    !matches!(last, '{' | '(' | '[' | ' ' | '\n' | '.' | '#' | ':' | ',' | ';')
}

fn needs_space_before_value(out: &str) -> bool {
    if out.is_empty() {
        return false;
    }
    let last = out.chars().last().unwrap();
    !matches!(
        last,
        '{' | '(' | '[' | ' ' | '\n' | '.' | '#' | '@' | ':' | '-' | ',' | ';' | '>' | '+'
            | '~' | '*' | '=' | '!'
    )
}