Skip to main content

rue_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::{quote, ToTokens};
3use syn::parse::{Parse, ParseStream};
4use syn::{braced, Expr, Ident, LitStr, Token};
5
6/// Parsed HTML template
7struct HtmlTemplate {
8    nodes: Vec<HtmlNode>,
9}
10
11enum HtmlNode {
12    Element {
13        tag: String,
14        attrs: Vec<HtmlAttr>,
15        children: Vec<HtmlNode>,
16        self_closing: bool,
17    },
18    Text(String),
19    Fragment(Vec<HtmlNode>),
20    Expr(Expr),
21    /// A VNode expression — `{vnode expr}` evaluates to a VNode directly
22    VNodeExpr(Expr),
23}
24
25struct HtmlAttr {
26    name: String,
27    value: HtmlAttrValue,
28}
29
30enum HtmlAttrValue {
31    Static(String),
32    Dynamic(Expr),
33    Event(Expr),
34    Bool,
35}
36
37impl Parse for HtmlTemplate {
38    fn parse(input: ParseStream) -> syn::Result<Self> {
39        let nodes = parse_nodes(input, false)?;
40        Ok(HtmlTemplate { nodes })
41    }
42}
43
44/// Parse a sequence of HTML nodes until a closing tag or end of input.
45fn parse_nodes(input: ParseStream, inside_tag: bool) -> syn::Result<Vec<HtmlNode>> {
46    let mut nodes = Vec::new();
47
48    while !input.is_empty() {
49        if inside_tag {
50            if input.peek(Token![<]) && input.peek2(Token![/]) {
51                break;
52            }
53        }
54
55        if input.peek(Token![<]) {
56            let fork = input.fork();
57            let _ = fork.parse::<Token![<]>();
58            if fork.peek(Token![>]) {
59                // Fragment <> ... </>
60                let _ = input.parse::<Token![<]>();
61                let _ = input.parse::<Token![>]>();
62                let children = parse_nodes(input, false)?;
63                let _ = input.parse::<Token![<]>();
64                let _ = input.parse::<Token![/]>();
65                let _ = input.parse::<Token![>]>();
66                nodes.push(HtmlNode::Fragment(children));
67                continue;
68            }
69
70            nodes.push(parse_element(input)?);
71        } else if input.peek(syn::token::Brace) {
72            let content;
73            let _ = braced!(content in input);
74
75            // Check for `vnode` prefix: {vnode expr} or {vnode: expr}
76            // This tells the macro the expression returns a VNode directly
77            if content.peek(Ident) && content.fork().parse::<Ident>().ok().map_or(false, |id| id == "vnode") {
78                // Consume the "vnode" keyword
79                let _: Ident = content.parse()?;
80                // Optionally consume `:`
81                if content.peek(Token![:]) {
82                    let _: Token![:] = content.parse()?;
83                }
84                let expr: Expr = content.parse()?;
85                nodes.push(HtmlNode::VNodeExpr(expr));
86            } else {
87                // Regular expression — treat as text
88                let expr: Expr = content.parse()?;
89                nodes.push(HtmlNode::Expr(expr));
90            }
91        } else {
92            // Text content
93            let mut text = String::new();
94            while !input.is_empty() && !input.peek(Token![<]) && !input.peek(syn::token::Brace) {
95                if let Ok(lit) = input.parse::<LitStr>() {
96                    text.push_str(&lit.value());
97                } else {
98                    let fork = input.fork();
99                    if let Ok(ident) = fork.parse::<Ident>() {
100                        text.push_str(&ident.to_string());
101                        text.push(' ');
102                        input.parse::<Ident>().ok();
103                    } else if let Ok(remaining) = input.fork().parse::<proc_macro2::TokenStream>()
104                    {
105                        let s = remaining.to_string();
106                        if !s.is_empty() {
107                            text.push_str(&s);
108                            input.parse::<proc_macro2::TokenStream>().ok();
109                        } else {
110                            break;
111                        }
112                    } else {
113                        break;
114                    }
115                }
116            }
117            if !text.is_empty() {
118                let text = text.trim_end().to_string();
119                nodes.push(HtmlNode::Text(text));
120            } else {
121                break;
122            }
123        }
124    }
125
126    Ok(nodes)
127}
128
129fn parse_element(input: ParseStream) -> syn::Result<HtmlNode> {
130    let _ = input.parse::<Token![<]>();
131    let tag: Ident = input.parse()?;
132    let tag_str = tag.to_string();
133
134    let mut attrs = Vec::new();
135    let mut self_closing = false;
136
137    // Parse attributes
138    while !input.peek(Token![>]) && !input.peek(Token![/]) {
139        if input.peek(syn::token::Brace) {
140            let content;
141            let _ = braced!(content in input);
142            let _: Expr = content.parse()?;
143            continue;
144        }
145
146        // Parse attribute name — may contain hyphens (e.g. "stroke-linecap")
147        let first_ident: Ident = input.parse()?;
148        let mut name_str = first_ident.to_string();
149        // Consume any `-ident` suffixes (for hyphenated HTML attributes)
150        while input.peek(Token![-]) {
151            let _ = input.parse::<Token![-]>();
152            let part: Ident = input.parse()?;
153            name_str.push('-');
154            name_str.push_str(&part.to_string());
155        }
156
157        // Event handler: on:click={...}
158        if name_str == "on" && input.peek(Token![:]) {
159            let _ = input.parse::<Token![:]>();
160            let event_ident: Ident = input.parse()?;
161            let full_name = format!("on:{}", event_ident);
162
163            if input.peek(Token![=]) {
164                let _ = input.parse::<Token![=]>();
165            }
166            if input.peek(syn::token::Brace) {
167                let content;
168                let _ = braced!(content in input);
169                let handler: Expr = content.parse()?;
170                attrs.push(HtmlAttr {
171                    name: full_name,
172                    value: HtmlAttrValue::Event(handler),
173                });
174            }
175            continue;
176        }
177
178        if input.peek(Token![=]) {
179            let _ = input.parse::<Token![=]>();
180
181            if input.peek(LitStr) {
182                let lit: LitStr = input.parse()?;
183                attrs.push(HtmlAttr {
184                    name: name_str,
185                    value: HtmlAttrValue::Static(lit.value()),
186                });
187            } else if input.peek(syn::token::Brace) {
188                let content;
189                let _ = braced!(content in input);
190                let expr: Expr = content.parse()?;
191                attrs.push(HtmlAttr {
192                    name: name_str,
193                    value: HtmlAttrValue::Dynamic(expr),
194                });
195            }
196        } else {
197            attrs.push(HtmlAttr {
198                name: name_str,
199                value: HtmlAttrValue::Bool,
200            });
201        }
202    }
203
204    if input.peek(Token![/]) {
205        let _ = input.parse::<Token![/]>();
206        self_closing = true;
207    }
208
209    let _ = input.parse::<Token![>]>();
210
211    let children = if self_closing {
212        vec![]
213    } else {
214        let children = parse_nodes(input, true)?;
215        let _ = input.parse::<Token![<]>();
216        let _ = input.parse::<Token![/]>();
217        let _: Ident = input.parse()?;
218        let _ = input.parse::<Token![>]>();
219        children
220    };
221
222    Ok(HtmlNode::Element {
223        tag: tag_str,
224        attrs,
225        children,
226        self_closing,
227    })
228}
229
230impl ToTokens for HtmlTemplate {
231    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
232        let node_tokens = self.nodes.iter().map(|node| node.as_statement());
233        let expanded = quote! {
234            {
235                let mut __rue_children: ::std::vec::Vec<rue_core::node::VNode> = ::std::vec::Vec::new();
236                #(#node_tokens)*
237                if __rue_children.len() == 1 {
238                    __rue_children.into_iter().next().unwrap()
239                } else {
240                    rue_core::node::VNode::fragment(__rue_children)
241                }
242            }
243        };
244        tokens.extend(expanded);
245    }
246}
247
248impl HtmlNode {
249    /// Generate a statement that pushes this node to __rue_children.
250    fn as_statement(&self) -> proc_macro2::TokenStream {
251        match self {
252            HtmlNode::Text(text) => {
253                let text = text.clone();
254                quote! {
255                    __rue_children.push(rue_core::node::VNode::text(#text));
256                }
257            }
258            HtmlNode::Expr(expr) => {
259                quote! {
260                    __rue_children.push({
261                        let __val = (#expr);
262                        rue_core::node::VNode::text(&__val.to_string())
263                    });
264                }
265            }
266            HtmlNode::VNodeExpr(expr) => {
267                // Push the VNode expression directly — caller must return VNode
268                quote! {
269                    __rue_children.push({
270                        let __vnode: rue_core::node::VNode = (#expr);
271                        __vnode
272                    });
273                }
274            }
275            HtmlNode::Fragment(children) => {
276                let child_stmts: Vec<_> = children.iter().map(|c| c.as_statement()).collect();
277                quote! {
278                    {
279                        let mut __frag: ::std::vec::Vec<rue_core::node::VNode> = ::std::vec::Vec::new();
280                        #(#child_stmts)*
281                        __rue_children.push(rue_core::node::VNode::fragment(__frag));
282                    }
283                }
284            }
285            HtmlNode::Element { .. } => {
286                let expr = self.as_expression();
287                quote! {
288                    __rue_children.push(#expr);
289                }
290            }
291        }
292    }
293
294    /// Generate an expression that evaluates to a VNode.
295    fn as_expression(&self) -> proc_macro2::TokenStream {
296        match self {
297            HtmlNode::Element { tag, attrs, children, self_closing } => {
298                let tag_str = tag.as_str();
299                let mut builder = quote! {
300                    rue_core::node::VNode::element(#tag_str)
301                };
302
303                for attr in attrs {
304                    match &attr.value {
305                        HtmlAttrValue::Static(val) => {
306                            let name = attr.name.as_str();
307                            let val = val.clone();
308                            builder = quote! {
309                                #builder.attr(#name, #val)
310                            };
311                        }
312                        HtmlAttrValue::Dynamic(expr) => {
313                            let name = attr.name.as_str();
314                            builder = quote! {
315                                #builder.attr(#name, &(#expr).to_string())
316                            };
317                        }
318                        HtmlAttrValue::Event(expr) => {
319                            let event_type = &attr.name;
320                            let event_type = event_type.strip_prefix("on:").unwrap_or(event_type);
321                            builder = quote! {
322                                #builder.on(#event_type, #expr)
323                            };
324                        }
325                        HtmlAttrValue::Bool => {
326                            let name = attr.name.as_str();
327                            builder = quote! {
328                                #builder.attr(#name, "")
329                            };
330                        }
331                    }
332                }
333
334                if !children.is_empty() && !*self_closing {
335                    let child_exprs: Vec<_> = children.iter().map(|c| c.as_expression()).collect();
336                    builder = quote! {
337                        #builder.children(::std::vec![#(#child_exprs),*])
338                    };
339                }
340
341                quote! { #builder.build() }
342            }
343            HtmlNode::Text(text) => {
344                let text = text.clone();
345                quote! { rue_core::node::VNode::text(#text) }
346            }
347            HtmlNode::Expr(expr) => {
348                quote! {{
349                    let __val = (#expr);
350                    rue_core::node::VNode::text(&__val.to_string())
351                }}
352            }
353            HtmlNode::VNodeExpr(expr) => {
354                quote! {{
355                    let __vnode: rue_core::node::VNode = (#expr);
356                    __vnode
357                }}
358            }
359            HtmlNode::Fragment(children) => {
360                let child_exprs: Vec<_> = children.iter().map(|c| c.as_expression()).collect();
361                quote! { rue_core::node::VNode::fragment(::std::vec![#(#child_exprs),*]) }
362            }
363        }
364    }
365}
366
367/// Entry point: html! { ... } macro
368#[proc_macro]
369pub fn html(input: TokenStream) -> TokenStream {
370    let template = syn::parse_macro_input!(input as HtmlTemplate);
371    let expanded = template.to_token_stream();
372    TokenStream::from(expanded)
373}
374
375/// Component attribute macro (for future use)
376#[proc_macro_attribute]
377pub fn component(_attr: TokenStream, item: TokenStream) -> TokenStream {
378    item
379}