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, Stmt};
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/// let show = true;
49/// either!(show => <p>"Show me"</p>);
50/// ```
51#[proc_macro]
52pub fn either(input: TokenStream) -> TokenStream {
53    let input = parse_macro_input!(input as Either);
54    let expanded = input.to_tokens();
55    expanded.into()
56}
57
58struct Either {
59    condition: Expr,
60    true_value: RsxNode,
61    false_value: Option<RsxNode>,
62}
63
64impl Parse for Either {
65    fn parse(input: ParseStream) -> Result<Self> {
66        let condition = input.parse()?;
67        input.parse::<Token![=>]>()?;
68        let true_value = input.parse()?;
69        let false_value = if input.peek(Token![else]) {
70            input.parse::<Token![else]>()?;
71            Some(input.parse()?)
72        } else {
73            None
74        };
75        Ok(Either {
76            condition,
77            true_value,
78            false_value,
79        })
80    }
81}
82
83impl Either {
84    fn to_tokens(&self) -> TokenStream2 {
85        let condition = &self.condition;
86        let false_value = &self
87            .false_value
88            .as_ref()
89            .map(|v| v.to_tokens())
90            .or_else(|| Some(quote! {simple_rsx::Node::Fragment(vec![])}));
91        let true_value = self.true_value.to_tokens();
92
93        quote! {
94            if #condition {
95                #true_value.into()
96            } else {
97                #false_value
98            }
99        }
100    }
101}
102
103/// A procedural macro that transforms a rust function into a component.
104///
105/// # Examples
106///
107/// ```rust
108/// use simple_rsx::*;
109///
110/// #[component]
111/// fn HelloWorld() -> Node {
112///     rsx!(<div>Hello World</div>)
113/// }
114/// ```
115#[proc_macro_attribute]
116pub fn component(_attr: TokenStream, input: TokenStream) -> TokenStream {
117    let ItemFn {
118        vis,
119        attrs,
120        sig,
121        block,
122    } = parse_macro_input!(input as ItemFn);
123    let Signature {
124        ident,
125        asyncness,
126        constness,
127        unsafety,
128        mut inputs,
129        output,
130        fn_token,
131        ..
132    } = sig;
133
134    if asyncness.is_some() || constness.is_some() || unsafety.is_some() {
135        panic!("async, const, and unsafe functions are not supported");
136    }
137
138    if inputs.len() > 1 {
139        panic!("Components can only take a single prop as input");
140    }
141
142    let prop_type = inputs
143        .iter()
144        .map(|input| match input {
145            FnArg::Typed(PatType { ty, .. }) => quote! {type Props = #ty;},
146            _ => panic!("Only typed inputs are supported"),
147        })
148        .next()
149        .unwrap_or_else(|| quote! {type Props = ();});
150
151    if inputs.is_empty() {
152        inputs.push(FnArg::Typed(PatType {
153            attrs: Vec::new(),
154            pat: parse_quote!(_),
155            colon_token: Colon::default(),
156            ty: parse_quote!(Self::Props),
157        }));
158    }
159
160    let expanded = quote! {
161        #vis #(#attrs)* struct #ident;
162
163        impl simple_rsx::Component for #ident {
164            #prop_type
165            #fn_token render(#inputs) #output #block
166        }
167    };
168
169    expanded.into()
170}
171
172/// Represents the different types of JSX nodes
173#[derive(Debug)]
174enum RsxNode {
175    Fragment(Vec<RsxNode>),
176    Component {
177        name: Ident,
178        props: Vec<(Ident, Option<Block>)>,
179        children: Vec<RsxNode>,
180        close_tag: Option<Ident>,
181    },
182    Text(Expr),
183    Block(Block),
184    Empty,
185    Comment(Expr), // HTML comments
186}
187
188struct NodeBlock {
189    expr: Option<Expr>,
190    value: Option<Block>,
191}
192
193impl Parse for NodeBlock {
194    fn parse(input: ParseStream) -> Result<Self> {
195        if input.peek(LitStr) {
196            let parsed: LitStr = input.parse()?;
197            return Ok(NodeBlock {
198                value: None,
199                expr: Some(syn::Expr::Macro(syn::ExprMacro {
200                    attrs: Vec::new(),
201                    mac: Macro {
202                        path: parse_quote!(format),
203                        bang_token: Not::default(),
204                        delimiter: syn::MacroDelimiter::Paren(syn::token::Paren::default()),
205                        tokens: quote::quote!(#parsed),
206                    },
207                })),
208            });
209        }
210
211        let is_block = input.to_string().trim().starts_with('{');
212
213        if is_block {
214            let value: Block = input.parse()?;
215            return Ok(NodeBlock {
216                value: Some(value),
217                expr: None,
218            });
219        }
220
221        if input.lookahead1().peek(Token![<]) {
222            // Found a non-literal '<', stop here without consuming it
223            return Ok(NodeBlock {
224                value: None,
225                expr: None,
226            });
227        }
228
229        match input.parse::<proc_macro2::TokenTree>() {
230            Ok(token) => match &token {
231                proc_macro2::TokenTree::Group(group) => {
232                    let stream = group.stream();
233                    let expr = syn::parse2::<Expr>(stream)?;
234                    Ok(NodeBlock {
235                        value: None,
236                        expr: Some(expr),
237                    })
238                }
239                _ => {
240                    let value = token.to_string();
241                    let str_expr = syn::Expr::Lit(ExprLit {
242                        attrs: Vec::new(),
243                        lit: Lit::Str(LitStr::new(&value, token.span())),
244                    });
245                    Ok(NodeBlock {
246                        value: None,
247                        expr: Some(str_expr),
248                    })
249                }
250            },
251            Err(e) => Err(e), // End of input
252        }
253    }
254}
255
256/// Represents an attribute name-value pair
257struct NodeValue {
258    name: Ident,
259    value: Option<Block>,
260}
261
262impl Parse for NodeValue {
263    fn parse(input: ParseStream) -> Result<Self> {
264        let name = input.parse()?;
265        if !input.peek(Token![=]) {
266            return Ok(NodeValue { name, value: None });
267        }
268        input.parse::<Token![=]>()?;
269        let NodeBlock { value, expr } = input.parse()?;
270        Ok(NodeValue {
271            name,
272            value: value.or_else(|| {
273                expr.map(|expr| Block {
274                    brace_token: Brace::default(),
275                    stmts: vec![syn::Stmt::Expr(expr, None)],
276                })
277            }),
278        })
279    }
280}
281
282struct RsxChildren {
283    children: Vec<RsxNode>,
284}
285
286impl Parse for RsxChildren {
287    fn parse(input: ParseStream) -> Result<Self> {
288        let mut children = Vec::with_capacity(4);
289        let mut last_end = 0;
290        while !(input.is_empty() || input.peek(Token![<]) && input.peek2(Token![/])) {
291            let span_info = format!("{:?}", input.span());
292            let (start, end) = parse_range(&span_info).unwrap_or((0, 0));
293            match input.parse::<RsxNode>() {
294                Ok(child) => children.push(child),
295                Err(_) => {
296                    let mut value = String::new();
297                    let token = input.parse::<proc_macro2::TokenTree>()?;
298
299                    if !matches!(token, proc_macro2::TokenTree::Punct(_)) {
300                        let gap_size = start - last_end;
301                        if gap_size > 0 && last_end > 0 {
302                            // Add spaces to represent the gap
303                            value.push_str(&" ".repeat(gap_size as usize));
304                        }
305                    }
306                    value.push_str(&token.to_string());
307
308                    children.push(RsxNode::Text(syn::Expr::Lit(ExprLit {
309                        attrs: Vec::new(),
310                        lit: Lit::Str(LitStr::new(&value, token.span())),
311                    })));
312                }
313            }
314            last_end = end;
315        }
316
317        Ok(RsxChildren { children })
318    }
319}
320
321impl Parse for RsxNode {
322    fn parse(input: ParseStream) -> Result<Self> {
323        if input.is_empty() {
324            return Ok(RsxNode::Empty);
325        }
326
327        // Look ahead to see if we start with a '<'
328        if input.peek(Token![<]) {
329            input.parse::<Token![<]>()?;
330
331            // Comments: <!-- ... -->
332            if input.peek(Token![!]) && input.peek2(Token![-]) && input.peek3(Token![-]) {
333                input.parse::<Token![!]>()?;
334                input.parse::<Token![-]>()?;
335                input.parse::<Token![-]>()?;
336
337                let mut last_end = 0;
338
339                let mut nodes = Vec::new();
340                while !(input.is_empty()
341                    || input.peek(Token![-]) && input.peek2(Token![-]) && input.peek3(Token![>]))
342                {
343                    let mut comment = String::new();
344                    let token = input.parse::<proc_macro2::TokenTree>()?;
345                    let span_info = format!("{:?}", token.span());
346                    let (start, end) = parse_range(&span_info).unwrap_or((0, 0));
347                    let gap_size = start - last_end;
348                    if gap_size > 0 && last_end > 0 {
349                        last_end = end;
350                        comment.push_str(&" ".repeat(gap_size as usize));
351                    }
352                    comment.push_str(&token.to_string());
353
354                    nodes.push(LitStr::new(&comment, token.span()));
355                }
356
357                let token = input.parse::<Token![-]>()?;
358                let span_info = format!("{:?}", token.span());
359                let (start, _) = parse_range(&span_info).unwrap_or((0, 0));
360                if start > last_end {
361                    nodes.push(LitStr::new(" ", token.span()));
362                }
363                input.parse::<Token![-]>()?;
364                input.parse::<Token![>]>()?;
365
366                // concat all nodes into a single lit
367                // Convert each LitStr to an Expr::Lit
368                let exprs: Vec<Expr> = nodes
369                    .into_iter()
370                    .map(|lit| {
371                        Expr::Lit(syn::ExprLit {
372                            attrs: vec![],
373                            lit: syn::Lit::Str(lit),
374                        })
375                    })
376                    .collect();
377
378                // Build a binary expression tree with the + operator
379                let mut result = syn::parse_str::<Expr>("String::new()").unwrap();
380
381                for expr in exprs.into_iter() {
382                    result = Expr::Binary(syn::ExprBinary {
383                        attrs: vec![],
384                        left: Box::new(result),
385                        op: syn::BinOp::Add(syn::token::Plus::default()),
386                        right: Box::new(expr),
387                    });
388                }
389
390                return Ok(RsxNode::Comment(result));
391            }
392
393            // Fragment: <>...</>
394            if input.peek(Token![>]) {
395                input.parse::<Token![>]>()?;
396
397                let RsxChildren { children } = input.parse()?;
398
399                input.parse::<Token![<]>()?;
400                input.parse::<Token![/]>()?;
401                input.parse::<Token![>]>()?;
402
403                return Ok(RsxNode::Fragment(children));
404            }
405
406            // Element: <tag ...>...</tag> or <tag ... />
407            let tag = input.parse::<Ident>()?;
408
409            let mut attributes = Vec::with_capacity(4);
410            while !input.peek(Token![>]) && !input.peek(Token![/]) {
411                if input.to_string().trim().starts_with('{') {
412                    let expr = input.parse::<Block>()?;
413                    // check if expr matches {Ident} pattern
414                    if let Some(Stmt::Expr(expr, token)) = expr.stmts.first() {
415                        if let Expr::Path(expr_path) = expr {
416                            match expr_path.path.segments.first() {
417                                Some(segment) => {
418                                    let ident = segment.ident.clone();
419                                    attributes.push((
420                                        ident,
421                                        Some(Block {
422                                            brace_token: Brace::default(),
423                                            stmts: vec![syn::Stmt::Expr(expr.clone(), *token)],
424                                        }),
425                                    ));
426                                }
427                                _ => {
428                                    return Err(syn::Error::new(
429                                        expr_path.span(),
430                                        "Only Ident expressions are supported",
431                                    ));
432                                }
433                            }
434                        }
435                    }
436                } else {
437                    match input.parse::<NodeValue>() {
438                        Ok(attr) => attributes.push((attr.name, attr.value)),
439                        Err(e) => return Err(e),
440                    }
441                }
442            }
443
444            // Self-closing tag: <tag ... /> or <Component... />
445            if input.peek(Token![/]) {
446                input.parse::<Token![/]>()?;
447                input.parse::<Token![>]>()?;
448
449                return Ok(RsxNode::Component {
450                    name: tag.clone(),
451                    props: attributes,
452                    children: Vec::new(),
453                    close_tag: None,
454                });
455            }
456
457            // Opening tag ends: <tag ...>
458            input.parse::<Token![>]>()?;
459
460            let RsxChildren { children } = input.parse()?;
461
462            // Closing tag: </tag>
463            input.parse::<Token![<]>()?;
464            input.parse::<Token![/]>()?;
465            let close_tag = input.parse::<Ident>()?;
466
467            // Validate matching tags
468            if tag != close_tag {
469                return Err(syn::Error::new(
470                    close_tag.span(),
471                    format!(
472                        "Closing tag </{}> doesn't match opening tag <{}>",
473                        close_tag, tag
474                    ),
475                ));
476            }
477
478            input.parse::<Token![>]>()?;
479
480            return Ok(RsxNode::Component {
481                name: tag,
482                props: attributes,
483                children,
484                close_tag: Some(close_tag),
485            });
486        }
487
488        // Text content or expression
489        if input.peek(Lit) {
490            let lit: Lit = input.parse()?;
491            let expr = Expr::Lit(ExprLit {
492                attrs: Vec::new(),
493                lit,
494            });
495            return Ok(RsxNode::Text(expr));
496        }
497        match input.parse::<Block>() {
498            Ok(block) => Ok(RsxNode::Block(block)),
499            Err(_) => Err(syn::Error::new(
500                Span::call_site(),
501                "Invalid JSX node, expected a valid rsx block, an expression or plain text",
502            )),
503        }
504    }
505}
506
507impl RsxNode {
508    fn to_tokens(&self) -> TokenStream2 {
509        match self {
510            RsxNode::Component {
511                name,
512                props,
513                children,
514                close_tag,
515            } => {
516                let is_element = name.to_string().starts_with(|c: char| !c.is_uppercase());
517                let attrs = props
518                    .iter() // filter out data- attributes for elements
519                    .map(|(name, value)| {
520                        let value = value
521                            .as_ref()
522                            .map(|v| quote! {#v})
523                            .or_else(|| Some(quote! {true}));
524                        (name, value)
525                    });
526                let data_props = (is_element
527                    && props
528                        .iter()
529                        .any(|(name, _)| name.to_string().starts_with("data_")))
530                .then(|| {
531                    let timestamp = std::time::SystemTime::now()
532                        .duration_since(std::time::UNIX_EPOCH)
533                        .unwrap_or_else(|_| std::time::Duration::from_secs(0))
534                        .as_nanos()
535                        .to_string();
536                    let ident =
537                        syn::Ident::new(&format!("attr_data_{}", timestamp), Span::call_site());
538                    let data = attrs
539                        .clone()
540                        .filter(|(name, _)| name.to_string().starts_with("data_"))
541                        .map(|(name, value)| {
542                            quote! {
543                                let #name = #value;
544                                #ident.insert(stringify!(#name).to_string(), #name);
545                            }
546                        });
547                    quote! {
548                        r#data: {
549                            let mut #ident = std::collections::HashMap::new();
550                            {
551                                #(#data)*
552                            }
553                            #ident
554                        },
555                    }
556                });
557                let props_tokens = attrs
558                    .filter(|(name, _)| !(is_element && name.to_string().starts_with("data_"))) // filter out data- attributes for elements
559                    .map(|(name, value)| quote! { #name: {#value}.into(), });
560
561                let child_tokens = children.iter().map(|child| child.to_tokens());
562                let children_tokens = quote! {
563                    children: vec![#(#child_tokens),*],
564                };
565
566                let close_tag = close_tag.as_ref().map(|close_tag| {
567                    quote! {
568                        let #close_tag = #name;
569                    }
570                });
571
572                let use_element = is_element.then(|| quote! {use simple_rsx::elements::#name;});
573                let default_props = is_element.then(|| quote! {..Default::default()});
574
575                let component = if !is_element {
576                    quote! { #name }
577                } else {
578                    quote! { simple_rsx::elements::#name }
579                };
580
581                quote! {
582                    {
583                        type Props = <#component as simple_rsx::Component>::Props;
584                        let props = Props {
585                            #(#props_tokens)*
586                            #children_tokens
587                            #data_props
588                            #default_props
589                        };
590                        let render = {
591                            #use_element
592                            #close_tag
593                            <#name as simple_rsx::Component>::render(props)
594                        };
595                        render
596                    }
597                }
598            }
599            RsxNode::Fragment(children) => {
600                let children_tokens = children.iter().map(|child| child.to_tokens());
601
602                quote! {
603                    {
604                        simple_rsx::Node::Fragment(vec![#(#children_tokens),*])
605                    }
606                }
607            }
608            RsxNode::Text(expr) => {
609                quote! {
610                    {
611                        simple_rsx::Node::from(#expr)
612                    }
613                }
614            }
615            RsxNode::Empty => {
616                quote! {
617                    simple_rsx::Node::Empty
618                }
619            }
620            RsxNode::Comment(expr) => {
621                quote! {
622                    simple_rsx::Node::Comment(#expr)
623                }
624            }
625            RsxNode::Block(block) => {
626                quote! {
627                    simple_rsx::Node::from(#block)
628                }
629            }
630        }
631    }
632}
633
634fn parse_range(input: &str) -> Option<(usize, usize)> {
635    use regex::Regex;
636    let re = Regex::new(r"(\d+)\.\.(\d+)").ok()?;
637    let captures = re.captures(input)?;
638    let start = captures.get(1)?.as_str().parse::<usize>().ok()?;
639    let end = captures.get(2)?.as_str().parse::<usize>().ok()?;
640
641    Some((start, end))
642}