simple_rsx_macros/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::{Span, TokenStream as TokenStream2};
3use quote::quote;
4use syn::spanned::Spanned;
5use syn::token::Colon;
6use syn::{
7    Block, Expr, ExprLit, Ident, ItemFn, Lit, LitStr, Macro, Result, Token,
8    parse::{Parse, ParseStream},
9    parse_macro_input, parse_quote,
10    token::{Brace, Not},
11};
12use syn::{FnArg, PatType, Signature};
13
14/// A procedural macro that provides JSX-like syntax for creating HTML elements in Rust.
15///
16/// # Examples
17///
18/// ```rust
19/// use simple_rsx::*;
20/// // Fragment
21/// rsx!(<>Hello World</>);
22///
23/// // Self-closing tag
24/// rsx!(<div class="container" id="app" />);
25///
26/// // Tag with children
27/// rsx!(<div class="container">
28///     <h1>Title</h1>
29///     <p>Paragraph text</p>
30/// </div>);
31///
32/// // Expression
33/// let name = "World";
34/// rsx!(<div>Hello {name}</div>);
35/// ```
36#[proc_macro]
37pub fn rsx(input: TokenStream) -> TokenStream {
38    let input = parse_macro_input!(input as RsxNode);
39    let expanded = input.to_tokens();
40    expanded.into()
41}
42/// A procedural macro that transforms a conditional expression into a JSX-like syntax.
43///
44/// # Examples
45/// ```rust
46/// use simple_rsx::*;
47/// // Fragment
48/// either!(show => <p>"Show me"</p>);
49/// ```
50#[proc_macro]
51pub fn either(input: TokenStream) -> TokenStream {
52    let input = parse_macro_input!(input as Either);
53    let expanded = input.to_tokens();
54    expanded.into()
55}
56
57struct Either {
58    condition: Expr,
59    true_value: RsxNode,
60    false_value: Option<RsxNode>,
61}
62
63impl Parse for Either {
64    fn parse(input: ParseStream) -> Result<Self> {
65        let condition = input.parse()?;
66        input.parse::<Token![=>]>()?;
67        let true_value = input.parse()?;
68        let false_value = if input.peek(Token![else]) {
69            input.parse::<Token![else]>()?;
70            Some(input.parse()?)
71        } else {
72            None
73        };
74        Ok(Either {
75            condition,
76            true_value,
77            false_value,
78        })
79    }
80}
81
82impl Either {
83    fn to_tokens(&self) -> TokenStream2 {
84        let condition = &self.condition;
85        let false_value = &self
86            .false_value
87            .as_ref()
88            .and_then(|v| Some(v.to_tokens()))
89            .or_else(|| Some(quote! {simple_rsx::Node::Fragment(vec![])}));
90        let true_value = self.true_value.to_tokens();
91
92        quote! {
93            if #condition {
94                #true_value.into()
95            } else {
96                #false_value
97            }
98        }
99    }
100}
101
102/// A procedural macro that transforms a rust function into a component.
103///
104/// # Examples
105///
106/// ```rust
107/// use simple_rsx::*;
108///
109/// #[component]
110/// fn HelloWorld() -> Node {
111///     rsx!(<div>Hello World</div>)
112/// }
113/// ```
114#[proc_macro_attribute]
115pub fn component(_attr: TokenStream, input: TokenStream) -> TokenStream {
116    let ItemFn {
117        vis,
118        attrs,
119        sig,
120        block,
121    } = parse_macro_input!(input as ItemFn);
122    let Signature {
123        ident,
124        asyncness,
125        constness,
126        unsafety,
127        mut inputs,
128        output,
129        fn_token,
130        ..
131    } = sig;
132
133    if asyncness.is_some() || constness.is_some() || unsafety.is_some() {
134        panic!("async, const, and unsafe functions are not supported");
135    }
136
137    if inputs.len() > 1 {
138        panic!("Components can only take a single prop as input");
139    }
140
141    let prop_type = inputs
142        .iter()
143        .find_map(|input| match input {
144            FnArg::Typed(PatType { ty, .. }) => Some(quote! {type Props = #ty;}),
145            _ => panic!("Only typed inputs are supported"),
146        })
147        .unwrap_or_else(|| quote! {type Props = ();});
148
149    if inputs.is_empty() {
150        inputs.push(FnArg::Typed(PatType {
151            attrs: Vec::new(),
152            pat: parse_quote!(_),
153            colon_token: Colon::default(),
154            ty: parse_quote!(Self::Props),
155        }));
156    }
157
158    let expanded = quote! {
159        #vis #(#attrs)* struct #ident;
160
161        impl simple_rsx::Component for #ident {
162            #prop_type
163            #fn_token render(#inputs) #output #block
164        }
165    };
166
167    expanded.into()
168}
169
170/// Represents the different types of JSX nodes
171enum RsxNode {
172    Fragment(Vec<RsxNode>),
173    Component {
174        name: Ident,
175        props: Vec<(Ident, Option<Block>)>,
176        children: Vec<RsxNode>,
177        close_tag: Option<Ident>,
178    },
179    Text(Expr),
180    Block(Block),
181    Empty,
182    Comment(String), // HTML comments
183}
184
185struct NodeBlock {
186    expr: Option<Expr>,
187    value: Option<Block>,
188}
189
190impl Parse for NodeBlock {
191    fn parse(input: ParseStream) -> Result<Self> {
192        if input.peek(LitStr) {
193            let parsed: LitStr = input.parse()?;
194            return Ok(NodeBlock {
195                value: None,
196                expr: Some(syn::Expr::Macro(syn::ExprMacro {
197                    attrs: Vec::new(),
198                    mac: Macro {
199                        path: parse_quote!(format),
200                        bang_token: Not::default(),
201                        delimiter: syn::MacroDelimiter::Paren(syn::token::Paren::default()),
202                        tokens: quote::quote!(#parsed),
203                    },
204                })),
205            });
206        }
207
208        let is_block = input.to_string().trim().starts_with('{');
209
210        if is_block {
211            let value: Block = input.parse()?;
212            return Ok(NodeBlock {
213                value: Some(value),
214                expr: None,
215            });
216        }
217
218        let mut str = String::new();
219        let mut in_string = false;
220        let mut last_end = 0;
221
222        while !input.is_empty() {
223            if input.lookahead1().peek(Token![<]) && !in_string {
224                // Found a non-literal '<', stop here without consuming it
225                break;
226            }
227
228            match input.parse::<proc_macro2::TokenTree>() {
229                Ok(token) => {
230                    match &token {
231                        proc_macro2::TokenTree::Literal(lit) => {
232                            let lit_str = lit.to_string();
233                            in_string = lit_str.starts_with('"') || lit_str.starts_with('\'');
234                        }
235                        _ => in_string = false,
236                    }
237
238                    let span_info = format!("{:?}", token.span());
239                    let (start, end) = parse_range(&span_info).unwrap_or((0, 0));
240
241                    let mut value = token.to_string();
242
243                    if value.starts_with('{') && value.ends_with('}') {
244                        value = value.replace("{ ", "{");
245                        value = value.replace(" }", "}");
246                    }
247
248                    if start > last_end {
249                        str.push(' ');
250                        last_end = end;
251                    }
252                    str.push_str(&value);
253                }
254                Err(_) => break, // End of input
255            }
256        }
257
258        let lit = LitStr::new(&str.trim(), Span::call_site());
259
260        Ok(NodeBlock {
261            value: None,
262            expr: Some(syn::Expr::Macro(syn::ExprMacro {
263                attrs: Vec::new(),
264                mac: Macro {
265                    path: parse_quote!(format),
266                    bang_token: Not::default(),
267                    delimiter: syn::MacroDelimiter::Paren(syn::token::Paren::default()),
268                    tokens: quote::quote!(#lit),
269                },
270            })),
271        })
272    }
273}
274
275/// Represents an attribute name-value pair
276struct NodeValue {
277    name: Ident,
278    value: Option<Block>,
279}
280
281impl Parse for NodeValue {
282    fn parse(input: ParseStream) -> Result<Self> {
283        let name = input.parse()?;
284        if !input.peek(Token![=]) {
285            return Ok(NodeValue { name, value: None });
286        }
287        input.parse::<Token![=]>()?;
288        let NodeBlock { value, expr } = input.parse()?;
289        Ok(NodeValue {
290            name,
291            value: value.or_else(|| match expr {
292                Some(expr) => Some(Block {
293                    brace_token: Brace::default(),
294                    stmts: vec![syn::Stmt::Expr(expr, None)],
295                }),
296                None => None,
297            }),
298        })
299    }
300}
301
302impl Parse for RsxNode {
303    fn parse(input: ParseStream) -> Result<Self> {
304        if input.is_empty() {
305            return Ok(RsxNode::Empty);
306        }
307
308        // Look ahead to see if we start with a '<'
309        if input.peek(Token![<]) {
310            input.parse::<Token![<]>()?;
311
312            // Comments: <!-- ... -->
313            if input.peek(Token![!]) && input.peek2(Token![-]) && input.peek3(Token![-]) {
314                input.parse::<Token![!]>()?;
315                input.parse::<Token![-]>()?;
316                input.parse::<Token![-]>()?;
317
318                let mut comment = String::new();
319                let mut last_end = 0;
320                while !input.is_empty()
321                    && !(input.peek(Token![-]) && input.peek2(Token![-]) && input.peek3(Token![>]))
322                {
323                    let token = input.parse::<proc_macro2::TokenTree>()?;
324                    let span_info = format!("{:?}", token.span());
325                    let (start, end) = parse_range(&span_info).unwrap_or((0, 0));
326                    if start > last_end {
327                        comment.push(' ');
328                        last_end = end;
329                    }
330                    comment.push_str(&token.to_string());
331                }
332
333                let token = input.parse::<Token![-]>()?;
334                let span_info = format!("{:?}", token.span());
335                let (start, _) = parse_range(&span_info).unwrap_or((0, 0));
336                if start > last_end {
337                    comment.push(' ');
338                }
339                input.parse::<Token![-]>()?;
340                input.parse::<Token![>]>()?;
341
342                return Ok(RsxNode::Comment(comment.to_string()));
343            }
344
345            // Fragment: <>...</>
346            if input.peek(Token![>]) {
347                input.parse::<Token![>]>()?;
348
349                let mut children = Vec::with_capacity(4); // Pre-allocate with reasonable capacity
350                while !input.is_empty()
351                    && !(input.peek(Token![<]) && input.peek2(Token![/]) && input.peek3(Token![>]))
352                {
353                    match input.parse::<RsxNode>() {
354                        Ok(child) => children.push(child),
355                        Err(_) => {
356                            input.parse::<proc_macro2::TokenTree>()?;
357                        }
358                    }
359                }
360
361                input.parse::<Token![<]>()?;
362                input.parse::<Token![/]>()?;
363                input.parse::<Token![>]>()?;
364
365                return Ok(RsxNode::Fragment(children));
366            }
367
368            // Element: <tag ...>...</tag> or <tag ... />
369            let tag = input.parse::<Ident>()?;
370
371            let mut attributes = Vec::with_capacity(4);
372            while !input.peek(Token![>]) && !input.peek(Token![/]) {
373                match input.parse::<NodeValue>() {
374                    Ok(attr) => attributes.push((attr.name, attr.value)),
375                    Err(e) => return Err(e),
376                }
377            }
378
379            // Self-closing tag: <tag ... /> or <Component... />
380            if input.peek(Token![/]) {
381                input.parse::<Token![/]>()?;
382                input.parse::<Token![>]>()?;
383
384                return Ok(RsxNode::Component {
385                    name: tag.clone(),
386                    props: attributes,
387                    children: Vec::new(),
388                    close_tag: None,
389                });
390            }
391
392            // Opening tag ends: <tag ...>
393            input.parse::<Token![>]>()?;
394
395            let mut children = Vec::with_capacity(4);
396            while !input.is_empty() && !(input.peek(Token![<]) && input.peek2(Token![/])) {
397                match input.parse::<RsxNode>() {
398                    Ok(child) => children.push(child),
399                    Err(e) => return Err(e),
400                }
401            }
402
403            // Closing tag: </tag>
404            input.parse::<Token![<]>()?;
405            input.parse::<Token![/]>()?;
406            let close_tag = input.parse::<Ident>()?;
407
408            // Validate matching tags
409            if tag != close_tag {
410                return Err(syn::Error::new(
411                    close_tag.span(),
412                    format!(
413                        "Closing tag </{}> doesn't match opening tag <{}>",
414                        close_tag, tag
415                    ),
416                ));
417            }
418
419            input.parse::<Token![>]>()?;
420
421            return Ok(RsxNode::Component {
422                name: tag,
423                props: attributes,
424                children,
425                close_tag: Some(close_tag),
426            });
427        }
428
429        // Text content or expression
430        if input.peek(Lit) {
431            let lit: Lit = input.parse()?;
432            let expr = Expr::Lit(ExprLit {
433                attrs: Vec::new(),
434                lit,
435            });
436            return Ok(RsxNode::Text(expr));
437        }
438        match input.parse::<Block>() {
439            Ok(block) => Ok(RsxNode::Block(block)),
440            Err(_) => match input.parse::<NodeBlock>() {
441                Ok(block) => match block.value {
442                    Some(value) => Ok(RsxNode::Block(value)),
443                    _ => match block.expr {
444                        Some(expr) => Ok(RsxNode::Text(expr)),
445                        _ => Ok(RsxNode::Empty),
446                    },
447                },
448                Err(_) => match input.parse::<Expr>() {
449                    Ok(expr) => Ok(RsxNode::Text(expr)),
450                    Err(_) => Err(syn::Error::new(
451                        Span::call_site(),
452                        "Invalid JSX node, expected a valid rsx block, an expression or plain text",
453                    )),
454                },
455            },
456        }
457    }
458}
459
460impl RsxNode {
461    fn to_tokens(&self) -> TokenStream2 {
462        match self {
463            RsxNode::Component {
464                name,
465                props,
466                children,
467                close_tag,
468            } => {
469                let props_tokens = props.iter().map(|(name, value)| {
470                    if value.is_none() {
471                        quote! {
472                            #name: true,
473                        }
474                    } else {
475                        quote! {
476                            #name: #value.into(),
477                        }
478                    }
479                });
480
481                let children_tokens = if !children.is_empty() {
482                    let child_tokens = children.iter().map(|child| child.to_tokens());
483                    Some(quote! {
484                        children: vec![#(#child_tokens),*],
485                    })
486                } else {
487                    Some(quote! {
488                        children: vec![],
489                    })
490                };
491
492                let close_tag = close_tag.as_ref().and_then(|close_tag| {
493                    Some(quote! {
494                        let #close_tag = #name;
495                    })
496                });
497                let is_component = name.to_string().starts_with(|c: char| c.is_uppercase());
498
499                let use_element = if !is_component {
500                    Some(quote! {use simple_rsx::elements::#name;})
501                } else {
502                    None
503                };
504
505                let default_props = if !is_component {
506                    Some(quote! {
507                        ..Default::default()
508                    })
509                } else {
510                    None
511                };
512
513                quote! {
514                    {
515                        #use_element
516                        type Props = <#name as simple_rsx::Component>::Props;
517                        #close_tag
518                        <#name as simple_rsx::Component>::render(
519                            Props {
520                                #(#props_tokens)*
521                                #children_tokens
522                                #default_props
523                            },
524                        )
525                    }
526                }
527            }
528            RsxNode::Fragment(children) => {
529                let children_tokens = children.iter().map(|child| child.to_tokens());
530
531                quote! {
532                    {
533                        simple_rsx::Node::Fragment(vec![#(#children_tokens)*])
534                    }
535                }
536            }
537            RsxNode::Text(expr) => {
538                quote! {
539                    simple_rsx::Node::Text(#expr.to_string())
540                }
541            }
542            RsxNode::Empty => {
543                quote! {
544                    simple_rsx::Node::Fragment(Vec::new())
545                }
546            }
547            RsxNode::Comment(text) => {
548                quote! {
549                    simple_rsx::Node::Comment(#text.to_string())
550                }
551            }
552            RsxNode::Block(block) => {
553                quote! {
554                    simple_rsx::Node::from(#block)
555                }
556            }
557        }
558    }
559}
560
561fn parse_range(input: &str) -> Option<(usize, usize)> {
562    use regex::Regex;
563    let re = Regex::new(r"(\d+)\.\.(\d+)").ok()?;
564    let captures = re.captures(input)?;
565    let start = captures.get(1)?.as_str().parse::<usize>().ok()?;
566    let end = captures.get(2)?.as_str().parse::<usize>().ok()?;
567
568    Some((start, end))
569}