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 .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
171enum 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), }
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 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, }
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
276struct 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 if input.peek(Token![<]) {
310 input.parse::<Token![<]>()?;
311
312 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 if input.peek(Token![>]) {
347 input.parse::<Token![>]>()?;
348
349 let mut children = Vec::with_capacity(4); 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 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 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 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 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 input.parse::<Token![<]>()?;
432 input.parse::<Token![/]>()?;
433 let close_tag = input.parse::<Ident>()?;
434
435 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 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() .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 }) .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}