1use proc_macro::TokenStream;
2use proc_macro2::TokenStream as TokenStream2;
3use quote::{quote, ToTokens};
4use syn::parse::{Parse, ParseStream};
5use syn::{FnArg, Result};
6
7struct Component(syn::ItemFn);
8
9impl Parse for Component {
10 fn parse(input: ParseStream) -> Result<Self> {
11 let func = input.parse::<syn::ItemFn>()?;
12
13 if let Some(async_token) = func.sig.asyncness {
14 let message = "Component cannot be async";
15
16 return Err(syn::Error::new(async_token.span, message));
17 }
18
19 let name = &func.sig.ident;
20 let first_char = name.to_string().chars().next().unwrap();
21
22 if first_char.is_ascii_lowercase() || name.to_string().contains('_') {
23 let message = "Name of the component must be in PascalCase";
24
25 return Err(syn::Error::new(name.span(), message));
26 }
27
28 Ok(Self(func))
29 }
30}
31
32impl ToTokens for Component {
33 fn to_tokens(&self, tokens: &mut TokenStream2) {
34 let func = &self.0;
35
36 let args = func.sig.inputs.iter();
37 let prop_names = args.clone().map(|a| {
38 let FnArg::Typed(a) = a else { unreachable!() };
39 let syn::Pat::Ident(pt) = a.pat.as_ref() else {
40 unreachable!()
41 };
42 pt.ident.clone()
43 });
44 let props = args.map(Prop::from).collect::<Vec<_>>();
45
46 let body = func.block.as_ref();
47 let name = &func.sig.ident;
48 let vis = &func.vis;
49 let generics = &func.sig.generics;
50
51 quote!(
52 #[derive(::cercis::system::typed_builder::TypedBuilder)]
53 #[builder(doc, crate_module_path=::cercis::system::typed_builder)]
54 #vis struct #name #generics {#(#props,)*}
55
56 impl #generics ::cercis::html::component::Component for #name #generics {
57 fn render(&self) -> Element {
58 use ::cercis::system::*;
59 use ::cercis::prelude::*;
60 let Self { #(#prop_names,)* } = self;
61 #body
62 }
63 }
64 )
65 .to_tokens(tokens)
66 }
67}
68
69struct Prop {
70 prop: FnArg,
71 is_opt: bool,
72}
73
74impl From<&FnArg> for Prop {
75 fn from(value: &FnArg) -> Self {
76 let mut is_opt = false;
77 let value = value.clone();
78
79 if let FnArg::Typed(pt) = &value {
80 is_opt = pt.ty.to_token_stream().to_string().contains("Option <");
81 }
82
83 Self {
84 prop: value,
85 is_opt,
86 }
87 }
88}
89
90impl ToTokens for Prop {
91 fn to_tokens(&self, tokens: &mut TokenStream2) {
92 let attr = quote!(#[builder(default, setter(strip_option))]);
93 let prop = &self.prop;
94
95 if let FnArg::Typed(pt) = prop {
96 if pt.ty.to_token_stream().to_string().contains("Element <") {
97 quote!(#[builder(default = Element::default())] #prop).to_tokens(tokens);
98 return;
99 }
100 }
101
102 match self.is_opt {
103 true => quote!(#attr #prop),
104 false => quote!(#prop),
105 }
106 .to_tokens(tokens)
107 }
108}
109
110#[proc_macro_attribute]
157pub fn component(_: TokenStream, input: TokenStream) -> TokenStream {
158 match syn::parse::<Component>(input.clone()) {
159 Ok(component) => {
160 let body = component.into_token_stream();
161 body.into()
162 }
163 Err(err) => err.to_compile_error().into(),
164 }
165}