cercis_component/
lib.rs

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/// Macro ```#[component]``` write component for ```rsx!``` like default Rust function
111///
112/// > Name of the component must be in PascalCase
113///
114/// # Examples
115///
116/// ## Declaration
117///
118/// ```
119/// use cercis::prelude::*;
120///
121/// #[component]
122/// fn MyComponent() -> Element {
123///   rsx!(h1 { "My component!" })
124/// }
125/// ```
126///
127/// ## Props
128///
129/// ```
130/// use cercis::prelude::*;
131///
132/// #[component]
133/// fn MyComponent<'a>(text: &'a str) -> Element {
134///   rsx!(div {
135///     h1 { "My component!" }
136///     p { "{text}" }
137///   })
138/// }
139/// ```
140///
141/// ## Optional props
142///
143/// ```
144/// use cercis::prelude::*;
145///
146/// #[component]
147/// fn MyComponent<'a>(text: Option<&'a str>) -> Element {
148///   let text = text.unwrap_or("empty");
149///
150///   rsx!(div {
151///     h1 { "My component!" }
152///     p { "{text}" }
153///   })
154/// }
155/// ```
156#[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}