tent_codegen 0.2.1

Procedural macros for tent.
Documentation
use proc_macro2::TokenStream;
use quote::quote;

use crate::Language;
use crate::indent::IndentNode;
use crate::source::{Source, Token};
use crate::text::Text;

pub(super) enum Item {
    Node { name: String, children: Vec<Self> },
    Declaration { property: String, value: String },
}

impl Item {
    fn join_name(namespace: &str, name: &str) -> String {
        if name.contains(',') {
            name.split(',')
                .map(|part| {
                    let part = part.trim();
                    match part.strip_prefix('&') {
                        Some(rest) => format!("{namespace}{rest}"),
                        None => format!("{namespace} {part}"),
                    }
                })
                .collect::<Vec<_>>()
                .join(", ")
        } else {
            match name.strip_prefix('&') {
                Some(rest) => format!("{namespace}{rest}"),
                None => format!("{namespace} {name}"),
            }
        }
    }

    fn flatten(self, namespace: Option<String>) -> (Option<Self>, Vec<Self>) {
        let Self::Node { name, children } = self else {
            return (Some(self), Vec::new());
        };

        if let Some(parent) = namespace.as_ref().filter(|_| name.starts_with("@media")) {
            let mut declarations = Vec::new();
            let mut nodes = Vec::new();
            for child in children {
                let (declaration, mut child_nodes) = child.flatten(Some(parent.clone()));
                if let Some(declaration) = declaration {
                    declarations.push(declaration);
                }
                nodes.append(&mut child_nodes);
            }
            if !declarations.is_empty() {
                nodes.push(Self::Node {
                    name: parent.clone(),
                    children: declarations,
                });
            }
            return (
                None,
                vec![Self::Node {
                    name,
                    children: nodes,
                }],
            );
        }

        let scoped = match &namespace {
            Some(namespace) => Self::join_name(namespace, &name),
            None => name,
        };
        let mut declarations = Vec::new();
        let mut nodes = Vec::new();
        for child in children {
            let (declaration, mut child_nodes) = child.flatten(Some(scoped.clone()));
            if let Some(declaration) = declaration {
                declarations.push(declaration);
            }
            nodes.append(&mut child_nodes);
        }
        nodes.push(Self::Node {
            name: scoped,
            children: declarations,
        });
        (None, nodes)
    }

    fn render(&self, out: &mut String) {
        match self {
            Self::Node { name, children } => {
                if children.is_empty() {
                    return;
                }
                out.push_str(name);
                out.push_str(" {");
                for child in children {
                    child.render(out);
                }
                out.push('}');
            }
            Self::Declaration { property, value } => {
                out.push_str(property);
                out.push_str(": ");
                out.push_str(&Text::unquote(value));
                out.push(';');
            }
        }
    }
}

pub(super) struct Node {
    level: usize,
    item: Item,
}

impl IndentNode for Node {
    type Output = Item;

    fn level(&self) -> usize {
        self.level
    }

    fn into_output(self) -> Item {
        self.item
    }

    fn adopt(&mut self, children: Vec<Item>) {
        match &mut self.item {
            Item::Node { children: slot, .. } => *slot = children,
            Item::Declaration { property, .. } => {
                panic!("CSS: declaration `{property}` cannot have children")
            }
        }
    }
}

enum State {
    StandBy,
    Prefix(char),
    Selector(String),
    Accumulated(String),
    Punctuated(String),
    DeclarationValue(String),
    Done(Item),
}

impl Node {
    fn parse(level: usize, tokens: Vec<Token>) -> Result<Self, String> {
        let mut state = State::StandBy;
        for token in tokens {
            state = match (state, token) {
                (State::StandBy, Token::Punct('@')) => State::Prefix('@'),
                (State::StandBy, Token::Ident(ident)) => State::Selector(ident),
                (State::StandBy, Token::Punct(p @ ('.' | '#' | '&'))) => {
                    State::Punctuated(p.to_string())
                }
                (State::Prefix(prefix), Token::Ident(ident)) => {
                    State::Selector(format!("{prefix}{}", Text::camel_to_dashed(&ident)))
                }
                (State::Selector(sel), Token::Punct('.')) => State::Punctuated(format!("{sel} .")),
                (State::Selector(sel), Token::Punct(',')) => State::Punctuated(format!("{sel}, ")),
                (State::Selector(sel), Token::Punct(':')) => State::DeclarationValue(sel),
                (State::DeclarationValue(property), Token::Literal(value)) => {
                    State::Done(Item::Declaration {
                        property: Text::camel_to_dashed(&property),
                        value,
                    })
                }
                (State::Selector(prev), Token::Literal(lit)) => {
                    State::Accumulated(format!("{prev} {}", Text::unquote(&lit)))
                }
                (State::Selector(prev), Token::Ident(ident)) => {
                    State::Accumulated(format!("{prev} {ident}"))
                }
                (State::Accumulated(prev), Token::Ident(ident)) => {
                    State::Accumulated(format!("{prev} {ident}"))
                }
                (State::Punctuated(prev), Token::Ident(ident)) => {
                    State::Accumulated(format!("{prev}{ident}"))
                }
                (State::Accumulated(prev), Token::Punct('-')) => {
                    State::Punctuated(format!("{prev}-"))
                }
                (State::Accumulated(prev), Token::Punct(',')) => {
                    State::Punctuated(format!("{prev}, "))
                }
                (State::Accumulated(prev), Token::Punct(p)) => {
                    State::Punctuated(format!("{prev} {p}"))
                }
                (State::Punctuated(prev), Token::Punct(p)) => {
                    State::Punctuated(format!("{prev}{p}"))
                }
                (State::Selector(prev), Token::Group { delimited, .. })
                | (State::Accumulated(prev), Token::Group { delimited, .. })
                | (State::Punctuated(prev), Token::Group { delimited, .. }) => {
                    State::Accumulated(format!("{prev}{delimited}"))
                }
                (_, token) => {
                    return Err(format!("CSS: unexpected `{}`", token.describe()));
                }
            };
        }

        let item = match state {
            State::Done(item) => item,
            State::Selector(name) | State::Accumulated(name) => Item::Node {
                name,
                children: Vec::new(),
            },
            _ => return Err("CSS: incomplete line".to_string()),
        };
        Ok(Self { level, item })
    }
}

pub(super) struct Css;

impl Css {
    fn render(roots: Vec<Item>) -> String {
        let mut flat = Vec::new();
        for item in roots {
            let (declaration, mut nodes) = item.flatten(None);
            debug_assert!(declaration.is_none(), "CSS: top-level declaration");
            flat.append(&mut nodes);
        }
        let mut css = String::new();
        for item in &flat {
            item.render(&mut css);
        }
        css
    }
}

impl Language for Css {
    type Line = Node;
    type Tree = Item;

    fn parse(input: &str) -> Result<Vec<Node>, String> {
        let mut vars: Vec<(String, String)> = Vec::new();
        let mut content: Vec<(usize, usize, String)> = Vec::new();
        for line in Source::lines(input) {
            let trimmed = line.text.trim();
            if let Some(rest) = trimmed.strip_prefix('$')
                && let Some(colon) = rest.find(':')
            {
                vars.push((
                    rest[..colon].trim().to_string(),
                    rest[colon + 1..].trim().to_string(),
                ));
            } else {
                content.push((line.num, line.level, line.text.to_string()));
            }
        }
        vars.sort_by_key(|v| std::cmp::Reverse(v.0.len()));

        let mut nodes = Vec::new();
        for (num, level, mut text) in content {
            for (name, value) in &vars {
                text = text.replace(&format!("${name}"), value);
            }
            let tokens = Source::tokenize(&text).map_err(|e| format!("CSS line {num}: {e}"))?;
            if tokens.is_empty() {
                continue;
            }
            nodes.push(Node::parse(level, tokens).map_err(|e| format!("CSS line {num}: {e}"))?);
        }
        Ok(nodes)
    }

    fn emit_markup(roots: Vec<Item>) -> Result<TokenStream, String> {
        let css = Self::render(roots);
        Ok(quote! { #css })
    }

    fn emit_body(roots: Vec<Item>) -> Result<TokenStream, String> {
        let css = Self::render(roots);
        Ok(quote! { ::tent::Body::from_static(#css) })
    }
}