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        .find_map(|input| match input {
145            FnArg::Typed(PatType { ty, .. }) => Some(quote! {type Props = #ty;}),
146            _ => panic!("Only typed inputs are supported"),
147        })
148        .unwrap_or_else(|| quote! {type Props = ();});
149
150    if inputs.is_empty() {
151        inputs.push(FnArg::Typed(PatType {
152            attrs: Vec::new(),
153            pat: parse_quote!(_),
154            colon_token: Colon::default(),
155            ty: parse_quote!(Self::Props),
156        }));
157    }
158
159    let expanded = quote! {
160        #vis #(#attrs)* struct #ident;
161
162        impl simple_rsx::Component for #ident {
163            #prop_type
164            #fn_token render(#inputs) #output #block
165        }
166    };
167
168    expanded.into()
169}
170
171/// Represents the different types of JSX nodes
172enum RsxNode {
173    Fragment(Vec<RsxNode>),
174    Component {
175        name: Ident,
176        props: Vec<(Ident, Option<Block>)>,
177        children: Vec<RsxNode>,
178        close_tag: Option<Ident>,
179    },
180    Text(Expr),
181    Block(Block),
182    Empty,
183    Comment(String), // HTML comments
184}
185
186struct NodeBlock {
187    expr: Option<Expr>,
188    value: Option<Block>,
189}
190
191impl Parse for NodeBlock {
192    fn parse(input: ParseStream) -> Result<Self> {
193        if input.peek(LitStr) {
194            let parsed: LitStr = input.parse()?;
195            return Ok(NodeBlock {
196                value: None,
197                expr: Some(syn::Expr::Macro(syn::ExprMacro {
198                    attrs: Vec::new(),
199                    mac: Macro {
200                        path: parse_quote!(format),
201                        bang_token: Not::default(),
202                        delimiter: syn::MacroDelimiter::Paren(syn::token::Paren::default()),
203                        tokens: quote::quote!(#parsed),
204                    },
205                })),
206            });
207        }
208
209        let is_block = input.to_string().trim().starts_with('{');
210
211        if is_block {
212            let value: Block = input.parse()?;
213            return Ok(NodeBlock {
214                value: Some(value),
215                expr: None,
216            });
217        }
218
219        let mut str = String::new();
220        let mut in_string = false;
221        let mut last_end = 0;
222
223        while !input.is_empty() {
224            if input.lookahead1().peek(Token![<]) && !in_string {
225                // Found a non-literal '<', stop here without consuming it
226                break;
227            }
228
229            match input.parse::<proc_macro2::TokenTree>() {
230                Ok(token) => {
231                    match &token {
232                        proc_macro2::TokenTree::Literal(lit) => {
233                            let lit_str = lit.to_string();
234                            in_string = lit_str.starts_with('"') || lit_str.starts_with('\'');
235                        }
236                        _ => in_string = false,
237                    }
238
239                    let span_info = format!("{:?}", token.span());
240                    let (start, end) = parse_range(&span_info).unwrap_or((0, 0));
241
242                    let mut value = token.to_string();
243
244                    if value.starts_with('{') && value.ends_with('}') {
245                        value = value.replace("{ ", "{");
246                        value = value.replace(" }", "}");
247                    }
248
249                    if start > last_end {
250                        str.push(' ');
251                        last_end = end;
252                    }
253                    str.push_str(&value);
254                }
255                Err(_) => break, // End of input
256            }
257        }
258
259        let lit = LitStr::new(str.trim(), Span::call_site());
260
261        Ok(NodeBlock {
262            value: None,
263            expr: Some(syn::Expr::Macro(syn::ExprMacro {
264                attrs: Vec::new(),
265                mac: Macro {
266                    path: parse_quote!(format),
267                    bang_token: Not::default(),
268                    delimiter: syn::MacroDelimiter::Paren(syn::token::Paren::default()),
269                    tokens: quote::quote!(#lit),
270                },
271            })),
272        })
273    }
274}
275
276/// Represents an attribute name-value pair
277struct NodeValue {
278    name: Ident,
279    value: Option<Block>,
280}
281
282impl Parse for NodeValue {
283    fn parse(input: ParseStream) -> Result<Self> {
284        let name = input.parse()?;
285        if !input.peek(Token![=]) {
286            return Ok(NodeValue { name, value: None });
287        }
288        input.parse::<Token![=]>()?;
289        let NodeBlock { value, expr } = input.parse()?;
290        Ok(NodeValue {
291            name,
292            value: value.or_else(|| {
293                expr.map(|expr| Block {
294                    brace_token: Brace::default(),
295                    stmts: vec![syn::Stmt::Expr(expr, None)],
296                })
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                if input.to_string().trim().starts_with('{') {
374                    let expr = input.parse::<Block>()?;
375                    // check if expr matches {Ident} pattern
376                    if let Some(Stmt::Expr(expr, token)) = expr.stmts.first() {
377                        if let Expr::Path(expr_path) = expr {
378                            match expr_path.path.segments.first() {
379                                Some(segment) => {
380                                    let ident = segment.ident.clone();
381                                    attributes.push((
382                                        ident,
383                                        Some(Block {
384                                            brace_token: Brace::default(),
385                                            stmts: vec![syn::Stmt::Expr(expr.clone(), *token)],
386                                        }),
387                                    ));
388                                }
389                                _ => {
390                                    return Err(syn::Error::new(
391                                        expr_path.span(),
392                                        "Only Ident expressions are supported",
393                                    ));
394                                }
395                            }
396                        }
397                    }
398                } else {
399                    match input.parse::<NodeValue>() {
400                        Ok(attr) => attributes.push((attr.name, attr.value)),
401                        Err(e) => return Err(e),
402                    }
403                }
404            }
405
406            // Self-closing tag: <tag ... /> or <Component... />
407            if input.peek(Token![/]) {
408                input.parse::<Token![/]>()?;
409                input.parse::<Token![>]>()?;
410
411                return Ok(RsxNode::Component {
412                    name: tag.clone(),
413                    props: attributes,
414                    children: Vec::new(),
415                    close_tag: None,
416                });
417            }
418
419            // Opening tag ends: <tag ...>
420            input.parse::<Token![>]>()?;
421
422            let mut children = Vec::with_capacity(4);
423            while !input.is_empty() && !(input.peek(Token![<]) && input.peek2(Token![/])) {
424                match input.parse::<RsxNode>() {
425                    Ok(child) => children.push(child),
426                    Err(e) => return Err(e),
427                }
428            }
429
430            // Closing tag: </tag>
431            input.parse::<Token![<]>()?;
432            input.parse::<Token![/]>()?;
433            let close_tag = input.parse::<Ident>()?;
434
435            // Validate matching tags
436            if tag != close_tag {
437                return Err(syn::Error::new(
438                    close_tag.span(),
439                    format!(
440                        "Closing tag </{}> doesn't match opening tag <{}>",
441                        close_tag, tag
442                    ),
443                ));
444            }
445
446            input.parse::<Token![>]>()?;
447
448            return Ok(RsxNode::Component {
449                name: tag,
450                props: attributes,
451                children,
452                close_tag: Some(close_tag),
453            });
454        }
455
456        // Text content or expression
457        if input.peek(Lit) {
458            let lit: Lit = input.parse()?;
459            let expr = Expr::Lit(ExprLit {
460                attrs: Vec::new(),
461                lit,
462            });
463            return Ok(RsxNode::Text(expr));
464        }
465        match input.parse::<Block>() {
466            Ok(block) => Ok(RsxNode::Block(block)),
467            Err(_) => match input.parse::<NodeBlock>() {
468                Ok(block) => match block.value {
469                    Some(value) => Ok(RsxNode::Block(value)),
470                    _ => match block.expr {
471                        Some(expr) => Ok(RsxNode::Text(expr)),
472                        _ => Ok(RsxNode::Empty),
473                    },
474                },
475                Err(_) => match input.parse::<Expr>() {
476                    Ok(expr) => Ok(RsxNode::Text(expr)),
477                    Err(_) => Err(syn::Error::new(
478                        Span::call_site(),
479                        "Invalid JSX node, expected a valid rsx block, an expression or plain text",
480                    )),
481                },
482            },
483        }
484    }
485}
486
487impl RsxNode {
488    fn to_tokens(&self) -> TokenStream2 {
489        match self {
490            RsxNode::Component {
491                name,
492                props,
493                children,
494                close_tag,
495            } => {
496                let is_element = name.to_string().starts_with(|c: char| !c.is_uppercase());
497                let attrs = props
498                    .iter() // filter out data- attributes for elements
499                    .map(|(name, value)| {
500                        let value = value
501                            .as_ref()
502                            .map(|v| quote! {#v})
503                            .or_else(|| Some(quote! {true}));
504                        (name, value)
505                    });
506                let data_props = (is_element
507                    && props
508                        .iter()
509                        .any(|(name, _)| name.to_string().starts_with("data_")))
510                .then(|| {
511                    let timestamp = std::time::SystemTime::now()
512                        .duration_since(std::time::UNIX_EPOCH)
513                        .unwrap_or_else(|_| std::time::Duration::from_secs(0))
514                        .as_nanos()
515                        .to_string();
516                    let ident =
517                        syn::Ident::new(&format!("attr_data_{}", timestamp), Span::call_site());
518                    let data = attrs
519                        .clone()
520                        .filter(|(name, _)| name.to_string().starts_with("data_"))
521                        .map(|(name, value)| {
522                            quote! {
523                                let #name = #value.value();
524                                #ident.insert(stringify!(#name).to_string(), #name);
525                            }
526                        });
527                    quote! {
528                        r#data: {
529                            let mut #ident = std::collections::HashMap::new();
530                            {
531                                #(#data)*
532                            }
533                            #ident
534                        },
535                    }
536                });
537                let props_tokens = attrs
538                    .filter(|(name, _)| {
539                        !is_element || (is_element && !name.to_string().starts_with("data_"))
540                    }) // filter out data- attributes for elements
541                    .map(|(name, value)| quote! { #name: #value, });
542
543                let child_tokens = children.iter().map(|child| child.to_tokens());
544                let children_tokens = quote! {
545                    children: vec![#(#child_tokens),*],
546                };
547
548                let close_tag = close_tag.as_ref().map(|close_tag| {
549                    quote! {
550                        let #close_tag = #name;
551                    }
552                });
553
554                let use_element = is_element.then(|| quote! {use simple_rsx::elements::#name;});
555                let default_props = is_element.then(|| quote! {..Default::default()});
556
557                let component = if !is_element {
558                    quote! { #name }
559                } else {
560                    quote! { simple_rsx::elements::#name }
561                };
562
563                quote! {
564                    {
565                        type Props = <#component as simple_rsx::Component>::Props;
566                        let props = Props {
567                            #(#props_tokens)*
568                            #children_tokens
569                            #data_props
570                            #default_props
571                        };
572                        let render = {
573                            #use_element
574                            #close_tag
575                            <#name as simple_rsx::Component>::render(props)
576                        };
577                        render
578                    }
579                }
580            }
581            RsxNode::Fragment(children) => {
582                let children_tokens = children.iter().map(|child| child.to_tokens());
583
584                quote! {
585                    {
586                        simple_rsx::Node::Fragment(vec![#(#children_tokens)*])
587                    }
588                }
589            }
590            RsxNode::Text(expr) => {
591                quote! {
592                    simple_rsx::Node::Text(#expr.to_string())
593                }
594            }
595            RsxNode::Empty => {
596                quote! {
597                    simple_rsx::Node::Fragment(Vec::new())
598                }
599            }
600            RsxNode::Comment(text) => {
601                quote! {
602                    simple_rsx::Node::Comment(#text.to_string())
603                }
604            }
605            RsxNode::Block(block) => {
606                quote! {
607                    simple_rsx::Node::from(#block)
608                }
609            }
610        }
611    }
612}
613
614fn parse_range(input: &str) -> Option<(usize, usize)> {
615    use regex::Regex;
616    let re = Regex::new(r"(\d+)\.\.(\d+)").ok()?;
617    let captures = re.captures(input)?;
618    let start = captures.get(1)?.as_str().parse::<usize>().ok()?;
619    let end = captures.get(2)?.as_str().parse::<usize>().ok()?;
620
621    Some((start, end))
622}