hers-macro 0.1.1

Procedural macros for HERS templating
Documentation
use proc_macro::TokenStream;
use proc_macro2::{Literal, TokenStream as TokenStream2};
use quote::quote;

mod parser;
use parser::{HtmlElement, HtmlNode, parse_html};

/// The main hers! macro for HTML templating
#[proc_macro]
pub fn hers(input: TokenStream) -> TokenStream {
    let input = TokenStream2::from(input);

    match parse_html(input) {
        Ok(nodes) => {
            let code = generate_code(&nodes);
            TokenStream::from(quote! {
                {
                    use ::hers::{HtmlString, ToHtml};
                    #code
                }
            })
        }
        Err(err) => TokenStream::from(err.to_compile_error()),
    }
}

/// Generate Rust code from parsed HTML nodes
fn generate_code(nodes: &[HtmlNode]) -> TokenStream2 {
    let mut parts = Vec::new();

    for node in nodes {
        match node {
            HtmlNode::Element(element) => {
                let element_code = generate_element_code(element);
                parts.push(element_code);
            }
            HtmlNode::Text(text) => {
                let text_literal = Literal::string(text);
                parts.push(quote! { #text_literal.to_string() });
            }
            HtmlNode::Variable(expr) => {
                parts.push(quote! { (#expr).to_html().into_string() });
            }
            HtmlNode::If(if_node) => {
                let condition = &if_node.condition;
                let then_code = generate_code(&if_node.then_branch);

                let else_code = if let Some(else_branch) = &if_node.else_branch {
                    let else_gen = generate_code(else_branch);
                    quote! { #else_gen.into_string() }
                } else {
                    quote! { String::new() }
                };

                parts.push(quote! {
                    if #condition {
                        #then_code.into_string()
                    } else {
                        #else_code
                    }
                });
            }
            HtmlNode::For(for_node) => {
                let pattern = &for_node.pattern;
                let iterable = &for_node.iterable;
                let body_code = generate_code(&for_node.body);

                parts.push(quote! {
                    {
                        let mut result = String::new();
                        for #pattern in #iterable {
                            result.push_str(&#body_code.into_string());
                        }
                        result
                    }
                });
            }
        }
    }

    if parts.is_empty() {
        quote! { HtmlString::new(String::new()) }
    } else if parts.len() == 1 {
        let part = &parts[0];
        quote! { HtmlString::new(#part) }
    } else {
        quote! {
            HtmlString::new(format!("{}", [#(#parts),*].join("")))
        }
    }
}

/// Generate code for an HTML element
fn generate_element_code(element: &HtmlElement) -> TokenStream2 {
    let tag = &element.tag;
    let children_code = if element.children.is_empty() {
        quote! { String::new() }
    } else {
        let child_parts: Vec<_> = element
            .children
            .iter()
            .map(|child| match child {
                HtmlNode::Element(child_element) => generate_element_code(child_element),
                HtmlNode::Text(text) => {
                    let text_literal = Literal::string(text);
                    quote! { #text_literal.to_string() }
                }
                HtmlNode::Variable(expr) => {
                    quote! { (#expr).to_html().into_string() }
                }
                HtmlNode::If(if_node) => {
                    let condition = &if_node.condition;
                    let then_code = generate_code(&if_node.then_branch);

                    let else_code = if let Some(else_branch) = &if_node.else_branch {
                        let else_gen = generate_code(else_branch);
                        quote! { #else_gen.into_string() }
                    } else {
                        quote! { String::new() }
                    };

                    quote! {
                        if #condition {
                            #then_code.into_string()
                        } else {
                            #else_code
                        }
                    }
                }
                HtmlNode::For(for_node) => {
                    let pattern = &for_node.pattern;
                    let iterable = &for_node.iterable;
                    let body_code = generate_code(&for_node.body);

                    quote! {
                        {
                            let mut result = String::new();
                            for #pattern in #iterable {
                                result.push_str(&#body_code.into_string());
                            }
                            result
                        }
                    }
                }
            })
            .collect();

        if child_parts.len() == 1 {
            child_parts[0].clone()
        } else {
            quote! { [#(#child_parts),*].join("") }
        }
    };

    // Handle attributes
    let attrs_code =
        if element.attributes.is_empty() {
            quote! { String::new() }
        } else {
            let attr_parts: Vec<_> = element.attributes.iter().map(|(name, value)| {
            let name_str = name.to_string();
            match value {
                Some(val_expr) => {
                    quote! { format!(" {}=\"{}\"", #name_str, (#val_expr).to_html().as_str()) }
                }
                None => {
                    quote! { format!(" {}", #name_str) }
                }
            }
        }).collect();

            if attr_parts.len() == 1 {
                attr_parts[0].clone()
            } else {
                quote! { [#(#attr_parts),*].join("") }
            }
        };

    // Check if this is a self-closing tag
    let self_closing_tags = ["img", "br", "hr", "input", "meta", "link"];
    let is_self_closing = self_closing_tags.contains(&tag.as_str());

    if is_self_closing {
        quote! {
            format!("<{}{} />", #tag, #attrs_code)
        }
    } else {
        quote! {
            format!("<{}{}>{}</{}>", #tag, #attrs_code, #children_code, #tag)
        }
    }
}