hers_macro/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::{Literal, TokenStream as TokenStream2};
3use quote::quote;
4
5mod parser;
6use parser::{HtmlElement, HtmlNode, parse_html};
7
8/// The main hers! macro for HTML templating
9#[proc_macro]
10pub fn hers(input: TokenStream) -> TokenStream {
11    let input = TokenStream2::from(input);
12
13    match parse_html(input) {
14        Ok(nodes) => {
15            let code = generate_code(&nodes);
16            TokenStream::from(quote! {
17                {
18                    use ::hers::{HtmlString, ToHtml};
19                    #code
20                }
21            })
22        }
23        Err(err) => TokenStream::from(err.to_compile_error()),
24    }
25}
26
27/// Generate Rust code from parsed HTML nodes
28fn generate_code(nodes: &[HtmlNode]) -> TokenStream2 {
29    let mut parts = Vec::new();
30
31    for node in nodes {
32        match node {
33            HtmlNode::Element(element) => {
34                let element_code = generate_element_code(element);
35                parts.push(element_code);
36            }
37            HtmlNode::Text(text) => {
38                let text_literal = Literal::string(text);
39                parts.push(quote! { #text_literal.to_string() });
40            }
41            HtmlNode::Variable(expr) => {
42                parts.push(quote! { (#expr).to_html().into_string() });
43            }
44            HtmlNode::If(if_node) => {
45                let condition = &if_node.condition;
46                let then_code = generate_code(&if_node.then_branch);
47
48                let else_code = if let Some(else_branch) = &if_node.else_branch {
49                    let else_gen = generate_code(else_branch);
50                    quote! { #else_gen.into_string() }
51                } else {
52                    quote! { String::new() }
53                };
54
55                parts.push(quote! {
56                    if #condition {
57                        #then_code.into_string()
58                    } else {
59                        #else_code
60                    }
61                });
62            }
63            HtmlNode::For(for_node) => {
64                let pattern = &for_node.pattern;
65                let iterable = &for_node.iterable;
66                let body_code = generate_code(&for_node.body);
67
68                parts.push(quote! {
69                    {
70                        let mut result = String::new();
71                        for #pattern in #iterable {
72                            result.push_str(&#body_code.into_string());
73                        }
74                        result
75                    }
76                });
77            }
78        }
79    }
80
81    if parts.is_empty() {
82        quote! { HtmlString::new(String::new()) }
83    } else if parts.len() == 1 {
84        let part = &parts[0];
85        quote! { HtmlString::new(#part) }
86    } else {
87        quote! {
88            HtmlString::new(format!("{}", [#(#parts),*].join("")))
89        }
90    }
91}
92
93/// Generate code for an HTML element
94fn generate_element_code(element: &HtmlElement) -> TokenStream2 {
95    let tag = &element.tag;
96    let children_code = if element.children.is_empty() {
97        quote! { String::new() }
98    } else {
99        let child_parts: Vec<_> = element
100            .children
101            .iter()
102            .map(|child| match child {
103                HtmlNode::Element(child_element) => generate_element_code(child_element),
104                HtmlNode::Text(text) => {
105                    let text_literal = Literal::string(text);
106                    quote! { #text_literal.to_string() }
107                }
108                HtmlNode::Variable(expr) => {
109                    quote! { (#expr).to_html().into_string() }
110                }
111                HtmlNode::If(if_node) => {
112                    let condition = &if_node.condition;
113                    let then_code = generate_code(&if_node.then_branch);
114
115                    let else_code = if let Some(else_branch) = &if_node.else_branch {
116                        let else_gen = generate_code(else_branch);
117                        quote! { #else_gen.into_string() }
118                    } else {
119                        quote! { String::new() }
120                    };
121
122                    quote! {
123                        if #condition {
124                            #then_code.into_string()
125                        } else {
126                            #else_code
127                        }
128                    }
129                }
130                HtmlNode::For(for_node) => {
131                    let pattern = &for_node.pattern;
132                    let iterable = &for_node.iterable;
133                    let body_code = generate_code(&for_node.body);
134
135                    quote! {
136                        {
137                            let mut result = String::new();
138                            for #pattern in #iterable {
139                                result.push_str(&#body_code.into_string());
140                            }
141                            result
142                        }
143                    }
144                }
145            })
146            .collect();
147
148        if child_parts.len() == 1 {
149            child_parts[0].clone()
150        } else {
151            quote! { [#(#child_parts),*].join("") }
152        }
153    };
154
155    // Handle attributes
156    let attrs_code =
157        if element.attributes.is_empty() {
158            quote! { String::new() }
159        } else {
160            let attr_parts: Vec<_> = element.attributes.iter().map(|(name, value)| {
161            let name_str = name.to_string();
162            match value {
163                Some(val_expr) => {
164                    quote! { format!(" {}=\"{}\"", #name_str, (#val_expr).to_html().as_str()) }
165                }
166                None => {
167                    quote! { format!(" {}", #name_str) }
168                }
169            }
170        }).collect();
171
172            if attr_parts.len() == 1 {
173                attr_parts[0].clone()
174            } else {
175                quote! { [#(#attr_parts),*].join("") }
176            }
177        };
178
179    // Check if this is a self-closing tag
180    let self_closing_tags = ["img", "br", "hr", "input", "meta", "link"];
181    let is_self_closing = self_closing_tags.contains(&tag.as_str());
182
183    if is_self_closing {
184        quote! {
185            format!("<{}{} />", #tag, #attrs_code)
186        }
187    } else {
188        quote! {
189            format!("<{}{}>{}</{}>", #tag, #attrs_code, #children_code, #tag)
190        }
191    }
192}