horfimbor_client_derive/
lib.rs

1use proc_macro::{self, TokenStream};
2
3use proc_macro2::{Span, TokenStream as TokenStream2};
4use syn::__private::quote::quote;
5use syn::{Data, DeriveInput, Error, parse_macro_input};
6
7macro_rules! derive_error {
8    ($string: tt) => {
9        Error::new(Span::call_site(), $string)
10            .to_compile_error()
11            .into()
12    };
13}
14
15#[allow(clippy::too_many_lines)]
16#[proc_macro_derive(WebComponent, attributes(component, optionnal))]
17pub fn derive_web_component(input: TokenStream) -> TokenStream {
18    let input: DeriveInput = parse_macro_input!(input as DeriveInput);
19
20    let attrs = &input.attrs;
21
22    let Some(component) = attrs.iter().find(|attr| attr.path().is_ident("component")) else {
23        return derive_error!("WebComponent need a base component");
24    };
25
26    let component: syn::Ident = match component.parse_args() {
27        Ok(s) => s,
28        Err(_) => {
29            return derive_error!("attribute 'state' cannot be parsed");
30        }
31    };
32
33    let name = &input.ident;
34    let data = &input.data;
35
36    let mut opt_struct = TokenStream2::new();
37    let mut default_props = TokenStream2::new();
38    let mut observed = TokenStream2::new();
39    let mut get_attributes = TokenStream2::new();
40    let mut check_none = TokenStream2::new();
41    let mut real_props = TokenStream2::new();
42
43    match data {
44        Data::Struct(data_struct) => {
45            for field in &data_struct.fields {
46                let Some(ident) = &field.ident else {
47                    break;
48                };
49                let kind = &field.ty;
50                let attribute_html = format!("{ident}").replace('_', "-");
51
52                let attrs = &field.attrs;
53
54                if attrs.iter().any(|a| a.path().is_ident("optionnal")) {
55                    opt_struct.extend(quote! {
56                        #ident : #kind,
57                    });
58
59                    check_none.extend(quote! {
60                        let #ident = match ctx.props().#ident.clone(){
61                            None => None,
62                            Some(s) => {
63                                if s.is_empty(){ None }
64                                else{ Some(s) }
65                            }
66                        };
67                    });
68                } else {
69                    opt_struct.extend(quote! {
70                        #ident : Option<#kind>,
71                    });
72
73                    check_none.extend(quote! {
74                        let Some(#ident) = ctx.props().#ident.clone() else{
75                            return html!();
76                        };
77                        if #ident.is_empty() {
78                            return html!();
79                        }
80                    });
81                }
82                default_props.extend(quote! {
83                    #ident : this.get_attribute(#attribute_html),
84                });
85                observed.extend(quote! {
86                    #attribute_html,
87                });
88                get_attributes.extend(quote! {
89                    #ident: this.get_attribute(#attribute_html),
90                });
91                real_props.extend(quote! {
92                    #ident={{#ident.clone()}}
93                });
94            }
95        }
96        _ => return derive_error!("WebComponent is only implemented for struct"),
97    }
98
99    let output = quote! {
100        pub mod optional {
101
102            use yew::AppHandle;
103            use yew::prelude::*;
104            use custom_elements::CustomElement;
105            use web_sys::HtmlElement;
106            use super::#component;
107
108            #[derive(Default, Properties, PartialEq)]
109            pub struct #name {
110                #opt_struct
111            }
112
113            struct WrappedComponent{}
114
115            impl Component for WrappedComponent{
116                type Message = ();
117                type Properties = #name;
118
119                fn create(_ctx: &Context<Self>) -> Self {
120                    Self {}
121                }
122
123                fn changed(&mut self, _ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
124                    true
125                }
126
127                fn view(&self, ctx: &Context<Self>) -> Html {
128                    #check_none
129                    html! {
130                        <#component #real_props>
131                        </#component>
132                    }
133                }
134
135            }
136
137            #[derive(Default)]
138            pub struct ComponentWrapper {
139                content: Option<AppHandle<WrappedComponent>>,
140            }
141
142            impl CustomElement for ComponentWrapper {
143                fn inject_children(&mut self, this: &HtmlElement) {
144                    let props = #name {
145                        #default_props
146                    };
147
148                    self.content = Some(
149                        yew::Renderer::<WrappedComponent>::with_root_and_props(this.clone().into(), props).render(),
150                    );
151                }
152
153                fn shadow() -> bool {
154                    false
155                }
156
157                fn observed_attributes() -> &'static [&'static str] {
158                    &[#observed]
159                }
160
161                fn attribute_changed_callback(
162                    &mut self,
163                    this: &HtmlElement,
164                    name: String,
165                    old_value: Option<String>,
166                    new_value: Option<String>,
167                ) {
168                    let props = #name {
169                        #get_attributes
170                    };
171                    match &mut self.content {
172                        None => {}
173                        Some(handle) => {
174                            handle.update(props);
175                        }
176                    }
177                }
178            }
179        }
180    };
181
182    output.into()
183}