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 } 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}