rullst-macros 0.2.0

JSX-like compilation macros for Rullst framework
Documentation
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::parse::{Parse, ParseStream};
use syn::ext::IdentExt;
use syn::{token, Expr, Ident, LitStr, Result, Token};

pub enum HtmlNode {
    Element(HtmlElement),
    Text(LitStr),
    Block(Expr),
}

pub struct HtmlElement {
    pub tag_name: Ident,
    pub attributes: Vec<HtmlAttribute>,
    pub children: Vec<HtmlNode>,
}

pub struct HtmlAttribute {
    pub name: Ident,
    pub value: HtmlAttrValue,
}

pub enum HtmlAttrValue {
    Static(LitStr),
    Dynamic(Expr),
}

impl Parse for HtmlNode {
    fn parse(input: ParseStream) -> Result<Self> {
        if input.peek(Token![<]) {
            let element = input.parse::<HtmlElement>()?;
            Ok(HtmlNode::Element(element))
        } else if input.peek(token::Brace) {
            let content;
            syn::braced!(content in input);
            let expr = content.parse::<Expr>()?;
            Ok(HtmlNode::Block(expr))
        } else {
            let lit = input.parse::<LitStr>()?;
            Ok(HtmlNode::Text(lit))
        }
    }
}

impl Parse for HtmlElement {
    fn parse(input: ParseStream) -> Result<Self> {
        input.parse::<Token![<]>()?;
        let tag_name = input.parse::<Ident>()?;
        
        let mut attributes = Vec::new();
        while !input.peek(Token![>]) && !(input.peek(Token![/]) && input.peek2(Token![>])) {
            attributes.push(input.parse::<HtmlAttribute>()?);
        }
        
        if input.peek(Token![/]) {
            input.parse::<Token![/]>()?;
            input.parse::<Token![>]>()?;
            return Ok(HtmlElement {
                tag_name,
                attributes,
                children: Vec::new(),
            });
        }
        
        input.parse::<Token![>]>()?;
        
        let mut children = Vec::new();
        while !input.is_empty() {
            if input.peek(Token![<]) && input.peek2(Token![/]) {
                break;
            }
            children.push(input.parse::<HtmlNode>()?);
        }
        
        input.parse::<Token![<]>()?;
        input.parse::<Token![/]>()?;
        let closing_tag = input.parse::<Ident>()?;
        if closing_tag != tag_name {
            return Err(syn::Error::new(
                closing_tag.span(),
                format!("Mismatched closing tag: expected </{}>, found </{}>", tag_name, closing_tag),
            ));
        }
        input.parse::<Token![>]>()?;
        
        Ok(HtmlElement {
            tag_name,
            attributes,
            children,
        })
    }
}

impl Parse for HtmlAttribute {
    fn parse(input: ParseStream) -> Result<Self> {
        let name = Ident::parse_any(input)?;
        input.parse::<Token![=]>()?;
        let value = if input.peek(token::Brace) {
            let content;
            syn::braced!(content in input);
            let expr = content.parse::<Expr>()?;
            HtmlAttrValue::Dynamic(expr)
        } else {
            let lit = input.parse::<LitStr>()?;
            HtmlAttrValue::Static(lit)
        };
        Ok(HtmlAttribute { name, value })
    }
}

impl HtmlNode {
    pub fn to_tokens(&self) -> TokenStream {
        match self {
            HtmlNode::Element(el) => el.to_tokens(),
            HtmlNode::Text(txt) => {
                let val = txt.value();
                quote! { #val.to_string() }
            }
            HtmlNode::Block(expr) => {
                quote! { rullst::html::escape(&(#expr)) }
            }
        }
    }
}

impl HtmlElement {
    pub fn to_tokens(&self) -> TokenStream {
        let tag = self.tag_name.to_string();
        
        let mut attr_tokens = Vec::new();
        for attr in &self.attributes {
            let attr_name = attr.name.to_string();
            match &attr.value {
                HtmlAttrValue::Static(lit) => {
                    let val = lit.value();
                    attr_tokens.push(quote! {
                        format!(" {}=\"{}\"", #attr_name, #val)
                    });
                }
                HtmlAttrValue::Dynamic(expr) => {
                    attr_tokens.push(quote! {
                        format!(" {}=\"{}\"", #attr_name, rullst::html::escape_attr(&(#expr)))
                    });
                }
            }
        }
        
        let child_tokens = self.children.iter().map(|child| child.to_tokens());
        
        if self.children.is_empty() {
            quote! {
                {
                    let mut s = String::new();
                    s.push_str("<");
                    s.push_str(#tag);
                    #( s.push_str(&#attr_tokens); )*
                    s.push_str(" />");
                    s
                }
            }
        } else {
            quote! {
                {
                    let mut s = String::new();
                    s.push_str("<");
                    s.push_str(#tag);
                    #( s.push_str(&#attr_tokens); )*
                    s.push_str(">");
                    #( s.push_str(&#child_tokens); )*
                    s.push_str("</");
                    s.push_str(#tag);
                    s.push_str(">");
                    s
                }
            }
        }
    }
}