simple_rsx_macros/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::{Span, TokenStream as TokenStream2};
3use quote::quote;
4use syn::{
5    Block, Expr, ExprLit, Ident, Lit, LitStr, Macro, Result, Token,
6    parse::{Parse, ParseStream},
7    parse_macro_input, parse_quote,
8    token::{Brace, Not},
9};
10
11/// A procedural macro that provides JSX-like syntax for creating HTML elements in Rust.
12///
13/// # Examples
14///
15/// ```rust
16/// // Fragment
17/// rsx!(<>Hello World</>);
18///
19/// // Self-closing tag
20/// rsx!(<div class="container" id="app" />);
21///
22/// // Tag with children
23/// rsx!(<div class="container">
24///     <h1>Title</h1>
25///     <p>Paragraph text</p>
26/// </div>);
27///
28/// // Expression
29/// let name = "World";
30/// rsx!(<div>Hello {name}</div>);
31/// ```
32#[proc_macro]
33pub fn rsx(input: TokenStream) -> TokenStream {
34    let input = parse_macro_input!(input as JsxNode);
35    let expanded = input.to_tokens();
36    expanded.into()
37}
38
39/// Represents the different types of JSX nodes
40enum JsxNode {
41    Fragment(Vec<JsxNode>),
42    Element {
43        tag: Ident,
44        attributes: Vec<(Ident, Block)>,
45        children: Vec<JsxNode>,
46        close_tag: Option<Ident>, // Optional closing tag for elements
47    },
48    Text(Expr),
49    Block(Block),
50    Empty,
51}
52
53struct NodeBlock {
54    value: Block,
55}
56
57impl Parse for NodeBlock {
58    fn parse(input: ParseStream) -> Result<Self> {
59        if input.peek(LitStr) {
60            let parsed: LitStr = input.parse()?;
61            let value = Block {
62                brace_token: Brace::default(),
63                stmts: vec![syn::Stmt::Expr(
64                    syn::Expr::Macro(syn::ExprMacro {
65                        attrs: Vec::new(),
66                        mac: Macro {
67                            path: parse_quote!(format),
68                            bang_token: Not::default(),
69                            delimiter: syn::MacroDelimiter::Paren(syn::token::Paren::default()),
70                            tokens: {
71                                let string_lit = syn::Lit::Str(parsed);
72                                quote::quote!(#string_lit)
73                            },
74                        },
75                    }),
76                    None,
77                )],
78            };
79            return Ok(NodeBlock { value });
80        }
81        let is_block = input.to_string().trim().starts_with('{');
82
83        if is_block {
84            let value = input.parse()?;
85            Ok(NodeBlock { value })
86        } else {
87            let mut str = String::new();
88            let mut in_string = false;
89            let mut last_end = 0;
90
91            loop {
92                if input.lookahead1().peek(Token![<]) && !in_string {
93                    // Found a non-literal '<', stop here without consuming it
94                    break;
95                }
96
97                match input.parse::<proc_macro2::TokenTree>() {
98                    Ok(token) => {
99                        match &token {
100                            proc_macro2::TokenTree::Literal(lit) => {
101                                let lit_str = lit.to_string();
102                                in_string = lit_str.starts_with('"') || lit_str.starts_with('\'');
103                            }
104                            _ => in_string = false,
105                        }
106
107                        let span_info = format!("{:?}", token.span());
108                        let (start, end) = parse_range(&span_info).unwrap_or((0, 0));
109
110                        let mut value = token.to_string();
111
112                        if value.starts_with('{') && value.ends_with('}') {
113                            value = value.replace("{ ", "{");
114                            value = value.replace(" }", "}");
115                        }
116
117                        if start > last_end {
118                            str.push(' ');
119                            last_end = end;
120                        }
121                        str.push_str(&value);
122                    }
123                    Err(_) => break, // End of input
124                }
125            }
126
127            let lit = LitStr::new(&str, Span::call_site());
128            let value = Block {
129                brace_token: Brace::default(),
130                stmts: vec![syn::Stmt::Expr(
131                    syn::Expr::Macro(syn::ExprMacro {
132                        attrs: Vec::new(),
133                        mac: Macro {
134                            path: parse_quote!(format),
135                            bang_token: Not::default(),
136                            delimiter: syn::MacroDelimiter::Paren(syn::token::Paren::default()),
137                            tokens: {
138                                let string_lit = syn::Lit::Str(lit);
139                                quote::quote!(#string_lit)
140                            },
141                        },
142                    }),
143                    None,
144                )],
145            };
146            Ok(NodeBlock { value })
147        }
148    }
149}
150
151/// Represents an attribute name-value pair
152struct NodeValue {
153    name: Ident,
154    value: Block,
155}
156
157impl Parse for NodeValue {
158    fn parse(input: ParseStream) -> Result<Self> {
159        let name = input.parse()?;
160        input.parse::<Token![=]>()?;
161        let NodeBlock { value } = input.parse()?;
162        Ok(NodeValue { name, value })
163    }
164}
165
166impl Parse for JsxNode {
167    fn parse(input: ParseStream) -> Result<Self> {
168        // Empty
169        if input.is_empty() {
170            return Ok(JsxNode::Empty);
171        }
172
173        // Look ahead to see if we start with a '<'
174        if input.peek(Token![<]) {
175            input.parse::<Token![<]>()?;
176
177            // Fragment: <>...</>
178            if input.peek(Token![>]) {
179                input.parse::<Token![>]>()?;
180
181                let mut children = Vec::new();
182                while !input.is_empty()
183                    && !(input.peek(Token![<]) && input.peek2(Token![/]) && input.peek3(Token![>]))
184                {
185                    if let Ok(child) = input.parse::<JsxNode>() {
186                        children.push(child);
187                    } else {
188                        let _ = input.parse::<proc_macro2::TokenTree>();
189                    }
190                }
191
192                input.parse::<Token![<]>()?;
193                input.parse::<Token![/]>()?;
194                input.parse::<Token![>]>()?;
195
196                return Ok(JsxNode::Fragment(children));
197            }
198
199            // Element: <tag ...>...</tag> or <tag ... />
200            let tag = input.parse::<Ident>()?;
201
202            // Parse attributes
203            let mut attributes = Vec::new();
204            while !input.peek(Token![>]) && !input.peek(Token![/]) {
205                let attr: NodeValue = input.parse()?;
206                attributes.push((attr.name, attr.value));
207            }
208
209            // Self-closing tag: <tag ... />
210            if input.peek(Token![/]) {
211                input.parse::<Token![/]>()?;
212                input.parse::<Token![>]>()?;
213
214                return Ok(JsxNode::Element {
215                    tag,
216                    attributes,
217                    children: Vec::new(),
218                    close_tag: None,
219                });
220            }
221
222            // Opening tag ends: <tag ...>
223            input.parse::<Token![>]>()?;
224
225            // Parse children
226            let mut children = Vec::new();
227            while !input.is_empty() && !(input.peek(Token![<]) && input.peek2(Token![/])) {
228                let child = input.parse::<JsxNode>()?;
229                children.push(child);
230            }
231
232            // Closing tag: </tag>
233            input.parse::<Token![<]>()?;
234            input.parse::<Token![/]>()?;
235            let close_tag = input.parse::<Ident>()?;
236
237            // Validate matching tags
238            if tag != close_tag {
239                return Err(syn::Error::new(
240                    close_tag.span(),
241                    format!(
242                        "Closing tag </{}> doesn't match opening tag <{}>",
243                        close_tag, tag
244                    ),
245                ));
246            }
247
248            input.parse::<Token![>]>()?;
249
250            return Ok(JsxNode::Element {
251                tag,
252                attributes,
253                children,
254                close_tag: Some(close_tag),
255            });
256        }
257
258        // Text content or expression
259        if input.peek(Lit) {
260            let lit: Lit = input.parse()?;
261            let expr = Expr::Lit(ExprLit {
262                attrs: Vec::new(),
263                lit,
264            });
265            return Ok(JsxNode::Text(expr));
266        }
267        match input.parse::<Block>() {
268            Ok(block) => Ok(JsxNode::Block(block)),
269            Err(_) => match input.parse::<NodeBlock>() {
270                Ok(block) => Ok(JsxNode::Block(block.value)),
271                Err(_) => match input.parse::<Expr>() {
272                    Ok(expr) => Ok(JsxNode::Text(expr)),
273                    Err(_) => Err(syn::Error::new(
274                        Span::call_site(),
275                        "Invalid JSX node, expected text or expression",
276                    )),
277                },
278            },
279        }
280    }
281}
282
283impl JsxNode {
284    fn to_tokens(&self) -> TokenStream2 {
285        match self {
286            JsxNode::Fragment(children) => {
287                let children_tokens = children.iter().map(|child| child.to_tokens());
288
289                quote! {
290                    {
291                        let mut nodes = Vec::new();
292                        #(
293                            let result = #children_tokens;
294                            match result {
295                                simple_rsx::NodeList::Fragment(mut child_nodes) => nodes.append(&mut child_nodes),
296                                simple_rsx::NodeList::Single(node) => nodes.push(node),
297                            }
298                        )*
299                        simple_rsx::NodeList::Fragment(nodes)
300                    }
301                }
302            }
303            JsxNode::Element {
304                tag,
305                attributes,
306                children,
307                close_tag,
308            } => {
309                let tag_str = tag.to_string();
310                let attr_setters = attributes.iter().map(|(name, value)| {
311                    let name_str = name.to_string().replace("r#", "");
312                    quote! {
313                        if let Some(e) = #tag.as_element_mut() {
314                            let #name = #value;
315                            e.set_attribute(#name_str, #name);
316                        }
317                    }
318                });
319
320                let children_handlers = if children.is_empty() {
321                    quote! {}
322                } else {
323                    let children_tokens = children.iter().map(|child| child.to_tokens());
324
325                    quote! {
326                        #(
327                            let child_result = #children_tokens;
328                            match child_result {
329                                simple_rsx::NodeList::Fragment(nodes) => {
330                                    for child in nodes {
331                                        #tag.append_child(child);
332                                    }
333                                },
334                                simple_rsx::NodeList::Single(node) => {
335                                    #tag.append_child(node);
336                                }
337                            }
338                        )*
339                    }
340                };
341
342                let close_tag = if let Some(close_tag) = close_tag {
343                    quote! {
344                        #close_tag = #tag;
345                    }
346                } else {
347                    quote! {}
348                };
349
350                quote! {
351                    {
352                        #[allow(unused_mut)]
353                        let mut #tag = simple_rsx::Element::new(#tag_str);
354                        #(#attr_setters)*
355                        #children_handlers
356                        #close_tag
357                        simple_rsx::NodeList::Single(#tag)
358                    }
359                }
360            }
361            JsxNode::Text(expr) => {
362                quote! {
363                    simple_rsx::NodeList::Single(simple_rsx::TextNode::new(&(#expr).to_string()))
364                }
365            }
366            JsxNode::Empty => {
367                quote! {
368                    simple_rsx::NodeList::Fragment(Vec::new())
369                }
370            }
371            JsxNode::Block(block) => {
372                quote! {
373                    simple_rsx::NodeList::from(#block)
374                }
375            }
376        }
377    }
378}
379
380fn parse_range(input: &str) -> Option<(usize, usize)> {
381    use regex::Regex;
382    let re = Regex::new(r"(\d+)\.\.(\d+)").ok()?;
383    let captures = re.captures(input)?;
384    let start = captures.get(1)?.as_str().parse::<usize>().ok()?;
385    let end = captures.get(2)?.as_str().parse::<usize>().ok()?;
386
387    Some((start, end))
388}