Skip to main content

oxirast_parser/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{
4    ext::IdentExt, 
5    parse::{Parse, ParseStream},
6    parse_macro_input, Expr, Ident, LitStr, Result, Token,
7};
8
9enum AttrValue {
10    Literal(LitStr),
11    Expression(Expr),
12}
13
14struct HtmlAttribute {
15    original_ident: Ident, 
16    full_key: String,      
17    value: AttrValue,
18}
19
20impl Parse for HtmlAttribute {
21    fn parse(input: ParseStream) -> Result<Self> {
22        let original_ident = Ident::parse_any(input)?;
23        let mut full_key = original_ident.to_string();
24
25        while input.peek(Token![-]) || input.peek(Token![:]) {
26            if input.peek(Token![-]) {
27                input.parse::<Token![-]>()?;
28                full_key.push('-');
29            } else if input.peek(Token![:]) {
30                input.parse::<Token![:]>()?;
31                full_key.push(':');
32            }
33            let next_ident = Ident::parse_any(input)?;
34            full_key.push_str(&next_ident.to_string());
35        }
36
37        let value = if input.peek(Token![=]) {
38            input.parse::<Token![=]>()?;
39            if input.peek(syn::token::Brace) {
40                let content;
41                syn::braced!(content in input);
42                AttrValue::Expression(content.parse()?)
43            } else {
44                AttrValue::Literal(input.parse::<LitStr>()?)
45            }
46        } else {
47            AttrValue::Literal(syn::LitStr::new("true", original_ident.span()))
48        };
49
50        Ok(Self { original_ident, full_key, value })
51    }
52}
53
54struct HtmlElement {
55    tag: syn::Path, 
56    attributes: Vec<HtmlAttribute>,
57    children: Vec<HtmlNode>,
58}
59
60enum HtmlNode {
61    Element(HtmlElement),
62    Text(LitStr),
63    Expression(Expr), 
64}
65
66impl Parse for HtmlNode {
67    fn parse(input: ParseStream) -> Result<Self> {
68        if input.peek(Token![<]) {
69            input.parse::<Token![<]>()?;
70            let tag = input.parse::<syn::Path>()?; 
71
72            let mut attributes = Vec::new();
73            while !input.peek(Token![>]) && !input.peek(Token![/]) {
74                attributes.push(input.parse()?);
75            }
76
77            if input.peek(Token![/]) {
78                input.parse::<Token![/]>()?;
79                input.parse::<Token![>]>()?;
80                return Ok(HtmlNode::Element(HtmlElement { tag, attributes, children: Vec::new() }));
81            }
82
83            input.parse::<Token![>]>()?;
84
85            let mut children = Vec::new();
86            while !(input.peek(Token![<]) && input.peek2(Token![/])) {
87                children.push(input.parse()?);
88            }
89
90            input.parse::<Token![<]>()?;
91            input.parse::<Token![/]>()?;
92            let close_tag = input.parse::<syn::Path>()?;
93            input.parse::<Token![>]>()?;
94
95            let tag_str = quote!(#tag).to_string();
96            let close_tag_str = quote!(#close_tag).to_string();
97
98            if tag_str != close_tag_str {
99                return Err(syn::Error::new_spanned(close_tag, format!("Mismatched tag. Expected `{}`, found `{}`", tag_str, close_tag_str)));
100            }
101
102            Ok(HtmlNode::Element(HtmlElement { tag, attributes, children }))
103            
104        } else if input.peek(syn::token::Brace) {
105            let content;
106            syn::braced!(content in input);
107            Ok(HtmlNode::Expression(content.parse()?))
108        } else {
109            let text: LitStr = input.parse()?;
110            Ok(HtmlNode::Text(text))
111        }
112    }
113}
114
115fn generate_node(node: &HtmlNode) -> proc_macro2::TokenStream {
116    match node {
117        HtmlNode::Text(text) => quote! { oxirast_core::VNode::text(#text) },
118        HtmlNode::Expression(expr) => quote! { oxirast_core::VNode::text(&(#expr).to_string()) },
119        HtmlNode::Element(el) => {
120            let tag_path = &el.tag;
121            let last_segment = tag_path.segments.last().unwrap().ident.to_string();
122            let is_custom_component = last_segment.chars().next().unwrap().is_ascii_uppercase();
123
124            if is_custom_component {
125                let mut props_path = tag_path.clone();
126                let last = props_path.segments.last_mut().unwrap();
127                last.ident = syn::Ident::new(&format!("{}Props", last.ident), last.ident.span());
128
129                if el.attributes.is_empty() { return quote! { #tag_path() }; }
130
131                let props_fields: Vec<_> = el.attributes.iter().map(|attr| {
132                    let key = &attr.original_ident; 
133                    match &attr.value {
134                        AttrValue::Literal(lit) => quote! { #key: String::from(#lit) },
135                        AttrValue::Expression(expr) => quote! { #key: #expr },
136                    }
137                }).collect();
138
139                return quote! { #tag_path(#props_path { #(#props_fields),* }) };
140            }
141
142            let tag_str = last_segment;
143            let mut attr_calls = Vec::new();
144
145            for attr in &el.attributes {
146                let key = &attr.full_key; 
147                
148                match &attr.value {
149                    AttrValue::Literal(lit) => attr_calls.push(quote! { .attr(#key, #lit) }),
150                    AttrValue::Expression(expr) => {
151                        if key == "bind_text" {
152                            attr_calls.push(quote! { .bind_text(#expr) });
153                        } else if key.starts_with("bind_attr:") {
154                            let attr_name = key.replace("bind_attr:", "");
155                            attr_calls.push(quote! { .bind_attr(#attr_name, #expr) });
156                            
157                        // --- NEW: LIFECYCLE HOOKS ---
158                        } else if key == "on_mount" {
159                            attr_calls.push(quote! { 
160                                .on_mount(std::rc::Rc::new(std::cell::RefCell::new(Box::new(#expr)))) 
161                            });
162                        } else if key == "on_cleanup" {
163                            attr_calls.push(quote! { 
164                                .on_cleanup(std::rc::Rc::new(std::cell::RefCell::new(Box::new(#expr)))) 
165                            });
166                            
167                        } else if key.starts_with("on_") {
168                            let event_name = key.replace("on_", "");
169                            attr_calls.push(quote! { 
170                                .on(#event_name, std::rc::Rc::new(std::cell::RefCell::new(Box::new(#expr)))) 
171                            });
172                        } else {
173                            attr_calls.push(quote! { .attr(#key, &(#expr).to_string()) });
174                        }
175                    }, 
176                }
177            }
178
179            let children: Vec<_> = el.children.iter().map(|child| {
180                let child_code = generate_node(child);
181                quote! { .child(#child_code) }
182            }).collect();
183
184            quote! {
185                oxirast_core::VNode::element(#tag_str)
186                #(#attr_calls)*
187                #(#children)*
188                .build()
189            }
190        }
191    }
192}
193
194#[proc_macro]
195pub fn rsx(input: TokenStream) -> TokenStream {
196    let root_node = parse_macro_input!(input as HtmlNode);
197    let expanded = generate_node(&root_node);
198    TokenStream::from(expanded)
199}