Skip to main content

rsx_macros/
lib.rs

1use {
2    heck::ToPascalCase,
3    proc_macro::TokenStream,
4    quote::quote,
5    rsx_parser::tokens::Element,
6    std::collections::HashSet,
7    syn::{Expr, visit::Visit},
8};
9
10// Visitor to extract identifiers that might need cloning
11struct IdentifierVisitor {
12    identifiers: HashSet<String>,
13    in_closure_params: bool,
14    closure_params: HashSet<String>,
15}
16
17impl IdentifierVisitor {
18    fn new() -> Self { Self { identifiers: HashSet::new(), in_closure_params: false, closure_params: HashSet::new() } }
19}
20
21impl<'ast> Visit<'ast> for IdentifierVisitor {
22    fn visit_expr_closure(&mut self, node: &'ast syn::ExprClosure) {
23        let old_in_params = self.in_closure_params;
24        self.in_closure_params = true;
25
26        for input in &node.inputs {
27            syn::visit::visit_pat(self, input);
28        }
29
30        self.in_closure_params = old_in_params;
31
32        syn::visit::visit_expr(self, &node.body);
33    }
34
35    fn visit_pat_ident(&mut self, node: &'ast syn::PatIdent) {
36        if self.in_closure_params {
37            self.closure_params.insert(node.ident.to_string());
38        }
39        syn::visit::visit_pat_ident(self, node);
40    }
41
42    fn visit_expr_path(&mut self, node: &'ast syn::ExprPath) {
43        if let Some(ident) = node.path.get_ident() {
44            let name = ident.to_string();
45            if !self.closure_params.contains(&name) && !self.in_closure_params {
46                self.identifiers.insert(name);
47            }
48        }
49    }
50}
51
52fn extract_captured_variables(expr: &Expr) -> HashSet<String> {
53    let mut visitor = IdentifierVisitor::new();
54    visitor.visit_expr(expr);
55    visitor.identifiers
56}
57
58fn modify_closure_to_use_clones(expr: &Expr, captured_vars: &HashSet<String>) -> proc_macro2::TokenStream {
59    use syn::fold::{Fold, fold_expr};
60
61    struct CloneReplacer<'a> {
62        captured_vars: &'a HashSet<String>,
63    }
64
65    impl<'a> Fold for CloneReplacer<'a> {
66        fn fold_expr_path(&mut self, mut node: syn::ExprPath) -> syn::ExprPath {
67            // Only replace simple identifiers, not complex paths
68            if let Some(ident) = node.path.get_ident() {
69                let name = ident.to_string();
70                if self.captured_vars.contains(&name) {
71                    let new_ident = syn::Ident::new(&format!("{}_clone", name), ident.span());
72                    node.path = syn::Path::from(new_ident);
73                }
74            }
75            node
76        }
77    }
78
79    let mut replacer = CloneReplacer { captured_vars };
80    let modified_expr = fold_expr(&mut replacer, expr.clone());
81    quote! { #modified_expr }
82}
83
84fn generate_component_code(element: &Element) -> proc_macro2::TokenStream {
85    let component_name = &element.ident;
86    let props_struct_name = syn::Ident::new(&format!("{}Props", component_name), component_name.span());
87
88    // Generate props struct instantiation from attributes
89    let mut prop_assignments = Vec::new();
90
91    for prop in &element.props {
92        let prop_name = &prop.name;
93        let prop_value = &prop.value;
94
95        prop_assignments.push(quote! {
96            #prop_name: #prop_value
97        });
98    }
99
100    // Call the component function with the props
101    quote! {
102        #component_name(#props_struct_name {
103            #(#prop_assignments),*
104        })
105    }
106}
107
108fn generate_dom_code(element: &Element) -> proc_macro2::TokenStream {
109    let tag_name = &element.ident;
110    let tag_str = tag_name.to_string();
111
112    // Check if this is a component (starts with uppercase)
113    let first_char = tag_str.chars().next().unwrap_or('a');
114    if first_char.is_uppercase() {
115        // This is a component - generate component instantiation code
116        return generate_component_code(element);
117    }
118
119    // Generate attributes for HTML elements
120    let mut methods = Vec::new();
121
122    for prop in &element.props {
123        let attr_code = generate_attribute_code(prop, &tag_str);
124        methods.push(attr_code);
125    }
126
127    // Handle style and script tags specially - their children should be treated as raw text
128    // Exception: script tags with 'src' attribute should be treated as normal HTML elements
129    let has_src_attr = tag_str == "script" && element.props.iter().any(|prop| prop.name == "src");
130
131    if (tag_str == "style" || tag_str == "script") && !has_src_attr {
132        if !element.children.is_empty() {
133            let raw_content = extract_raw_content(&element.children);
134            let text_method = quote! {
135                .text(#raw_content)
136            };
137            methods.push(text_method);
138        }
139    } else {
140        // Generate children if any (normal HTML elements)
141        if !element.children.is_empty() {
142            let mut children = Vec::new();
143            for child in &element.children {
144                let child_code = generate_child_code(child);
145                children.push(child_code);
146            }
147
148            let children = children.into_iter().flatten().collect::<Vec<_>>();
149            let children_method = quote! {
150                .children(&mut [
151                    #(#children),*
152                ])
153            };
154            methods.push(children_method);
155        }
156    }
157
158    // Generate dominator code structure
159    quote! {
160        html!(#tag_str, {
161            #(#methods)*
162        })
163    }
164}
165
166fn generate_attribute_code(prop: &rsx_parser::tokens::Prop, tag_name: &str) -> proc_macro2::TokenStream {
167    let attr_name = prop.name.to_string();
168    let value = &prop.value;
169
170    // Check if this is an event handler
171    if attr_name.starts_with("on") {
172        // Map HTML event names to dominator events
173        let event_type = match attr_name.as_str() {
174            "onclick" => quote! { dominator::events::Click },
175            "onmousedown" => quote! { dominator::events::MouseDown },
176            "onmouseup" => quote! { dominator::events::MouseUp },
177            "onmousemove" => quote! { dominator::events::MouseMove },
178            "ondblclick" => quote! { dominator::events::DoubleClick },
179            "oncontextmenu" => quote! { dominator::events::ContextMenu },
180            "onpointerover" => quote! { dominator::events::PointerOver },
181            "onpointerenter" => quote! { dominator::events::PointerEnter },
182            "onpointerdown" => quote! { dominator::events::PointerDown },
183            "onpointermove" => quote! { dominator::events::PointerMove },
184            "onpointerup" => quote! { dominator::events::PointerUp },
185            "onpointercancel" => quote! { dominator::events::PointerCancel },
186            "onpointerout" => quote! { dominator::events::PointerOut },
187            "onpointerleave" => quote! { dominator::events::PointerLeave },
188            "ongotpointercapture" => quote! { dominator::events::GotPointerCapture },
189            "onlostpointercapture" => quote! { dominator::events::LostPointerCapture },
190            "onkeydown" => quote! { dominator::events::KeyDown },
191            "onkeyup" => quote! { dominator::events::KeyUp },
192            "onfocus" => quote! { dominator::events::Focus },
193            "onblur" => quote! { dominator::events::Blur },
194            "onfocusin" => quote! { dominator::events::FocusIn },
195            "onfocusout" => quote! { dominator::events::FocusOut },
196            "ondragstart" => quote! { dominator::events::DragStart },
197            "ondrag" => quote! { dominator::events::Drag },
198            "ondragend" => quote! { dominator::events::DragEnd },
199            "ondragover" => quote! { dominator::events::DragOver },
200            "ondragenter" => quote! { dominator::events::DragEnter },
201            "ondragleave" => quote! { dominator::events::DragLeave },
202            "ondrop" => quote! { dominator::events::Drop },
203            "oninput" => quote! { dominator::events::Input },
204            "onbeforeinput" => quote! { dominator::events::BeforeInput },
205            "onanimationstart" => quote! { dominator::events::AnimationStart },
206            "onanimationiteration" => quote! { dominator::events::AnimationIteration },
207            "onanimationcancel" => quote! { dominator::events::AnimationCancel },
208            "onanimationend" => quote! { dominator::events::AnimationEnd },
209            "onwheel" => quote! { dominator::events::Wheel },
210            "onload" => quote! { dominator::events::Load },
211            "onerror" => quote! { dominator::events::Error },
212            "onscroll" => quote! { dominator::events::Scroll },
213            "onscrollend" => quote! { dominator::events::ScrollEnd },
214            "onsubmit" => quote! { dominator::events::Submit },
215            "onresize" => quote! { dominator::events::Resize },
216            "onselectionchange" => quote! { dominator::events::SelectionChange },
217            "onchange" => quote! { dominator::events::Change },
218            "ontouchcancel" => quote! { dominator::events::TouchCancel },
219            "ontouchend" => quote! { dominator::events::TouchEnd },
220            "ontouchmove" => quote! { dominator::events::TouchMove },
221            "ontouchstart" => quote! { dominator::events::TouchStart },
222            _ => quote! { web_sys::Event }, // fallback
223        };
224
225        // Generate event handler code with automatic cloning
226        let captured_vars = extract_captured_variables(value);
227
228        if captured_vars.is_empty() {
229            // No captured variables, use original handler
230            quote! {
231                .event(move |event: #event_type| (#value)(event))
232            }
233        } else {
234            // Generate clone statements for captured variables
235            let clone_stmts: Vec<_> = captured_vars
236                .iter()
237                .map(|var| {
238                    let var_ident = syn::Ident::new(var, proc_macro2::Span::call_site());
239                    let clone_ident = syn::Ident::new(&format!("{}_clone", var), proc_macro2::Span::call_site());
240                    quote! { let #clone_ident = #var_ident.clone(); }
241                })
242                .collect();
243
244            // Create a new closure that uses the cloned variables
245            let modified_closure = modify_closure_to_use_clones(value, &captured_vars);
246
247            quote! {
248                .event({
249                    #(#clone_stmts)*
250                    move |event: #event_type| (#modified_closure)(event)
251                })
252            }
253        }
254    } else {
255        // Some attributes need to be set as properties instead of attributes
256        // to trigger browser behavior correctly (e.g., script loading)
257        let should_use_prop = matches!(
258            (tag_name, attr_name.as_str()),
259            ("script", "src")
260                | ("img", "src")
261                | ("iframe", "src")
262                | ("video", "src")
263                | ("audio", "src")
264                | ("source", "src")
265                | ("input", "value")
266                | ("textarea", "value")
267                | ("select", "value")
268                | ("input", "checked")
269                | ("option", "selected")
270        );
271
272        if should_use_prop {
273            // Use .prop() for properties that need to trigger browser behavior
274            quote! {
275                .prop(#attr_name, #value)
276            }
277        } else {
278            // Use .attr() for regular HTML attributes
279            quote! {
280                .attr(#attr_name, #value)
281            }
282        }
283    }
284}
285
286fn extract_raw_content(children: &[Box<rsx_parser::tokens::Node>]) -> String {
287    let mut content = String::new();
288
289    for child in children {
290        match child.as_ref() {
291            rsx_parser::tokens::Node::Text(text) => {
292                content.push_str(text);
293            }
294            rsx_parser::tokens::Node::Expression(expr) => {
295                // For style/script tags, we want to preserve the expression syntax as-is
296                content.push('{');
297                content.push_str(&quote!(#expr).to_string());
298                content.push('}');
299            }
300            rsx_parser::tokens::Node::Element(element) => {
301                // Convert nested elements to text representation
302                content.push('<');
303                content.push_str(&element.ident.to_string());
304
305                // Add attributes
306                for prop in &element.props {
307                    content.push(' ');
308                    content.push_str(&prop.name.to_string());
309                    content.push('=');
310                    content.push('"');
311                    let prop_value = &prop.value;
312                    content.push_str(&quote!(#prop_value).to_string());
313                    content.push('"');
314                }
315
316                if element.children.is_empty() {
317                    content.push_str("/>");
318                } else {
319                    content.push('>');
320                    content.push_str(&extract_raw_content(&element.children));
321                    content.push_str("</");
322                    content.push_str(&element.ident.to_string());
323                    content.push('>');
324                }
325            }
326        }
327    }
328
329    content
330}
331
332fn generate_child_code(child: &rsx_parser::tokens::Node) -> Vec<proc_macro2::TokenStream> {
333    use rsx_parser::tokens::Node;
334    match child {
335        Node::Element(element) => vec![generate_dom_code(element)],
336        Node::Text(text) => {
337            let mut depth = 0;
338            let mut buf = String::new();
339            let mut doms = Vec::<proc_macro2::TokenStream>::new();
340            let mut itr = text.chars().peekable();
341
342            while let Some(c) = itr.next() {
343                if c == '{' && itr.peek() != Some(&'{') {
344                    depth += 1;
345                    if depth == 1 {
346                        // Starting a new expression - push any accumulated text
347                        if !buf.is_empty() {
348                            doms.push(quote! { Dom::text(#buf) });
349                            buf.clear();
350                        }
351                    } else {
352                        // Nested brace inside expression - add to buffer
353                        buf.push(c);
354                    }
355                } else if c == '}' && itr.peek() != Some(&'}') {
356                    if depth > 0 {
357                        depth -= 1;
358                        if depth == 0 {
359                            // Closing the expression - parse it
360                            if !buf.is_empty() {
361                                if let Ok(expr) = syn::parse_str::<Expr>(&buf) {
362                                    doms.push(generate_child_code(&Node::Expression(expr)).into_iter().next().unwrap());
363                                }
364                                buf.clear();
365                            }
366                        } else {
367                            // Nested brace inside expression - add to buffer
368                            buf.push(c);
369                        }
370                    } else {
371                        // Regular text
372                        buf.push(c);
373                    }
374                } else {
375                    buf.push(c);
376                }
377            }
378
379            if depth != 0 {
380                panic!("Expected '}}', found EOF")
381            }
382
383            if !buf.is_empty() {
384                doms.push(quote! { Dom::text(#buf) });
385                buf.clear();
386            }
387
388            doms
389        }
390        Node::Expression(expr) => {
391            // Check if the expression already returns a signal type
392            // Look for method calls that return signals: .signal(), .signal_cloned(), .signal_ref(), .map(), etc.
393            let expr_str = quote!(#expr).to_string();
394
395            if expr_str.contains(".signal()")
396                || expr_str.contains(".signal_cloned()")
397                || expr_str.contains(".signal_ref(")
398                || expr_str.contains(".map(")
399                || expr_str.contains(".map_ref(")
400            {
401                // Expression is already a signal, just ensure it produces String
402                vec![quote! {
403                    Dom::text_signal({
404                        use futures_signals::signal::SignalExt;
405                        (#expr).map(|x| format!("{}", x))
406                    })
407                }]
408            } else {
409                // Expression is a Mutable, call .signal() on it
410                vec![quote! {
411                    Dom::text_signal({
412                        use futures_signals::signal::SignalExt;
413                        #expr.signal_cloned().map(|x| format!("{}", x))
414                    })
415                }]
416            }
417        }
418    }
419}
420
421#[proc_macro]
422pub fn rsx(input: TokenStream) -> TokenStream {
423    let input2: proc_macro2::TokenStream = input.into();
424    if let Ok(element) = syn::parse2::<Element>(input2) {
425        let dom_code = generate_dom_code(&element);
426        TokenStream::from(dom_code)
427    } else {
428        syn::Error::new(
429            proc_macro2::Span::call_site(),
430            "Invalid JSX syntax - make sure to use proper HTML-like syntax: rsx!(<tag>content</tag>)",
431        )
432        .to_compile_error()
433        .into()
434    }
435}
436
437#[proc_macro_attribute]
438pub fn component(_attr: TokenStream, item: TokenStream) -> TokenStream {
439    let input = syn::parse_macro_input!(item as syn::ItemFn);
440
441    // Extract function name and convert to PascalCase for component and props struct names
442    let fn_name = &input.sig.ident;
443    let fn_name_str = fn_name.to_string();
444    let component_name = syn::Ident::new(&fn_name_str.to_pascal_case(), fn_name.span());
445    let props_struct_name = syn::Ident::new(&format!("{}Props", fn_name_str.to_pascal_case()), fn_name.span());
446
447    // Extract parameters and generate props struct fields
448    let mut prop_fields = Vec::new();
449    let mut fn_params = Vec::new();
450    let mut original_params = Vec::new();
451    let mut generic_params = Vec::new();
452    let mut where_clauses = Vec::new();
453
454    for param in &input.sig.inputs {
455        if let syn::FnArg::Typed(pat_type) = param {
456            // Extract parameter name
457            let param_name = if let syn::Pat::Ident(pat_ident) = &*pat_type.pat {
458                &pat_ident.ident
459            } else {
460                continue;
461            };
462
463            // Check if this is a Mutable<T> parameter
464            if is_mutable_type(&pat_type.ty) {
465                let inner_type = extract_inner_type(&pat_type.ty);
466                let generic_name =
467                    syn::Ident::new(&format!("__{}", param_name.to_string().to_uppercase()), param_name.span());
468
469                // Add generic parameter for the prop type
470                generic_params.push(quote! { #generic_name });
471
472                // Add where clause to constrain the generic
473                where_clauses.push(quote! {
474                    #generic_name: Into<::futures_signals::signal::Mutable<#inner_type>>
475                });
476
477                // Use the generic type in the props field
478                prop_fields.push(quote! {
479                    pub #param_name: #generic_name
480                });
481
482                // Convert the prop value to Mutable<T>
483                fn_params.push(quote! { props.#param_name.into() });
484            } else {
485                // For non-Mutable types, pass through directly
486                let field_type = &pat_type.ty;
487                prop_fields.push(quote! {
488                    pub #param_name: #field_type
489                });
490
491                fn_params.push(quote! { props.#param_name });
492            }
493
494            // Keep original parameters for the impl function
495            original_params.push(pat_type);
496        }
497    }
498
499    let fn_vis = &input.vis;
500    let fn_block = &input.block;
501    let fn_return = &input.sig.output;
502
503    // Create a wrapper function that takes props and calls the impl
504    let impl_fn_name = syn::Ident::new(&format!("{}_impl", fn_name), fn_name.span());
505
506    // Generate the props struct with generics if needed
507    let (props_generics, where_clause) = if !generic_params.is_empty() {
508        (quote! { <#(#generic_params),*> }, quote! { where #(#where_clauses),* })
509    } else {
510        (quote! {}, quote! {})
511    };
512
513    let expanded = quote! {
514        #[derive(Clone)]
515        #[allow(non_snake_case)]
516        #fn_vis struct #props_struct_name #props_generics
517        #where_clause
518        {
519            #(#prop_fields),*
520        }
521
522        #[allow(non_snake_case)]
523        #fn_vis fn #component_name #props_generics (props: #props_struct_name #props_generics) #fn_return
524        #where_clause
525        {
526            #impl_fn_name(#(#fn_params),*)
527        }
528
529        #[allow(non_snake_case)]
530        fn #impl_fn_name(#(#original_params),*) #fn_return {
531            #fn_block
532        }
533    };
534
535    TokenStream::from(expanded)
536}
537
538// Helper function to check if a type is Mutable<T>
539fn is_mutable_type(ty: &syn::Type) -> bool {
540    if let syn::Type::Path(type_path) = ty
541        && let Some(segment) = type_path.path.segments.last()
542    {
543        return segment.ident == "Mutable";
544    }
545    false
546}
547
548// Helper function to extract T from Mutable<T> or return the type as-is
549fn extract_inner_type(ty: &syn::Type) -> syn::Type {
550    if let syn::Type::Path(type_path) = ty
551        && let Some(segment) = type_path.path.segments.last()
552    {
553        // Check if this is Mutable<T>
554        if segment.ident == "Mutable"
555            && let syn::PathArguments::AngleBracketed(args) = &segment.arguments
556            && let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first()
557        {
558            return inner_ty.clone();
559        }
560    }
561    ty.clone()
562}