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
10struct 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 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 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 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 let first_char = tag_str.chars().next().unwrap_or('a');
114 if first_char.is_uppercase() {
115 return generate_component_code(element);
117 }
118
119 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 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 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 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 if attr_name.starts_with("on") {
172 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 }, };
224
225 let captured_vars = extract_captured_variables(value);
227
228 if captured_vars.is_empty() {
229 quote! {
231 .event(move |event: #event_type| (#value)(event))
232 }
233 } else {
234 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 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 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 quote! {
275 .prop(#attr_name, #value)
276 }
277 } else {
278 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 content.push('{');
297 content.push_str("e!(#expr).to_string());
298 content.push('}');
299 }
300 rsx_parser::tokens::Node::Element(element) => {
301 content.push('<');
303 content.push_str(&element.ident.to_string());
304
305 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("e!(#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 if !buf.is_empty() {
348 doms.push(quote! { Dom::text(#buf) });
349 buf.clear();
350 }
351 } else {
352 buf.push(c);
354 }
355 } else if c == '}' && itr.peek() != Some(&'}') {
356 if depth > 0 {
357 depth -= 1;
358 if depth == 0 {
359 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 buf.push(c);
369 }
370 } else {
371 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 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 vec![quote! {
403 Dom::text_signal({
404 use futures_signals::signal::SignalExt;
405 (#expr).map(|x| format!("{}", x))
406 })
407 }]
408 } else {
409 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 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 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 let param_name = if let syn::Pat::Ident(pat_ident) = &*pat_type.pat {
458 &pat_ident.ident
459 } else {
460 continue;
461 };
462
463 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 generic_params.push(quote! { #generic_name });
471
472 where_clauses.push(quote! {
474 #generic_name: Into<::futures_signals::signal::Mutable<#inner_type>>
475 });
476
477 prop_fields.push(quote! {
479 pub #param_name: #generic_name
480 });
481
482 fn_params.push(quote! { props.#param_name.into() });
484 } else {
485 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 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 let impl_fn_name = syn::Ident::new(&format!("{}_impl", fn_name), fn_name.span());
505
506 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
538fn 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
548fn 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 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}