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#[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#[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#[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#[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), }
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 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), }
253 }
254}
255
256struct 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 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 if input.peek(Token![<]) {
329 input.parse::<Token![<]>()?;
330
331 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 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 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 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 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 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 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 input.parse::<Token![>]>()?;
459
460 let RsxChildren { children } = input.parse()?;
461
462 input.parse::<Token![<]>()?;
464 input.parse::<Token![/]>()?;
465 let close_tag = input.parse::<Ident>()?;
466
467 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 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() .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_"))) .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}