maomi_macro/
component.rs

1use proc_macro::TokenStream;
2use quote::*;
3use syn::parse::*;
4use syn::punctuated::Punctuated;
5use syn::spanned::Spanned;
6use syn::*;
7
8use crate::template::SlotType;
9
10use super::i18n::LocaleGroup;
11use super::template::Template;
12
13struct ComponentAttr {
14    items: Punctuated<ComponentAttrItem, token::Comma>,
15}
16
17impl Parse for ComponentAttr {
18    fn parse(input: ParseStream) -> Result<Self> {
19        let items = Punctuated::parse_terminated(input)?;
20        Ok(Self { items })
21    }
22}
23
24enum ComponentAttrItem {
25    Backend {
26        attr_name: Ident,
27        #[allow(dead_code)]
28        equal_token: token::Eq,
29        impl_token: Option<token::Impl>,
30        path: Path,
31    },
32    SlotData {
33        attr_name: Ident,
34        #[allow(dead_code)]
35        equal_token: token::Eq,
36        path: Path,
37    },
38    Translation {
39        attr_name: Ident,
40        #[allow(dead_code)]
41        equal_token: token::Eq,
42        name: Ident,
43    },
44}
45
46impl Parse for ComponentAttrItem {
47    fn parse(input: ParseStream) -> Result<Self> {
48        let attr_name: Ident = input.parse()?;
49        let ret = match attr_name.to_string().as_str() {
50            "Backend" => Self::Backend {
51                attr_name,
52                equal_token: input.parse()?,
53                impl_token: input.parse()?,
54                path: input.parse()?,
55            },
56            "SlotData" => Self::SlotData {
57                attr_name,
58                equal_token: input.parse()?,
59                path: input.parse()?,
60            },
61            "Translation" => Self::Translation {
62                attr_name,
63                equal_token: input.parse()?,
64                name: input.parse()?,
65            },
66            _ => {
67                return Err(Error::new(attr_name.span(), "Unknown attribute parameter"));
68            }
69        };
70        Ok(ret)
71    }
72}
73
74struct ComponentBody {
75    inner: ItemStruct,
76    component_name: proc_macro2::TokenStream,
77    backend_param: proc_macro2::TokenStream,
78    backend_param_in_impl: Option<GenericParam>,
79    slot_kind: proc_macro2::TokenStream,
80    slot_data_ty: proc_macro2::TokenStream,
81    template: Result<Template>,
82    template_field: Ident,
83    locale_group: LocaleGroup,
84}
85
86impl ComponentBody {
87    fn new(attr: ComponentAttr, mut inner: ItemStruct) -> Result<Self> {
88        // generate backend type params and slot params
89        let mut backend_attr = None;
90        let mut slot_data_attr = None;
91        let mut locale_group_name = None;
92        for item in attr.items {
93            match item {
94                ComponentAttrItem::Backend {
95                    attr_name,
96                    impl_token,
97                    path,
98                    ..
99                } => {
100                    if backend_attr.is_some() {
101                        return Err(Error::new(
102                            attr_name.span(),
103                            "Duplicated attribute parameter",
104                        ));
105                    }
106                    backend_attr = Some((impl_token, path));
107                }
108                ComponentAttrItem::SlotData {
109                    attr_name, path, ..
110                } => {
111                    if slot_data_attr.is_some() {
112                        return Err(Error::new(
113                            attr_name.span(),
114                            "Duplicated attribute parameter",
115                        ));
116                    }
117                    slot_data_attr = Some(path);
118                }
119                ComponentAttrItem::Translation {
120                    attr_name, name, ..
121                } => {
122                    if locale_group_name.is_some() {
123                        return Err(Error::new(
124                            attr_name.span(),
125                            "Duplicated attribute parameter",
126                        ));
127                    }
128                    locale_group_name = Some(name);
129                }
130            }
131        }
132        let backend_param = match &backend_attr {
133            None => quote! { __MBackend },
134            Some((Some(_), path)) => {
135                let span = path.span();
136                quote_spanned! {span=> __MBackend }
137            }
138            Some((None, path)) => {
139                let span = path.span();
140                quote_spanned! {span=> #path }
141            }
142        };
143        let backend_param_in_impl = match backend_attr {
144            None => Some(parse_quote! { __MBackend: maomi::backend::Backend }),
145            Some((Some(_), path)) => {
146                let span = path.span();
147                Some(parse_quote_spanned! {span=> __MBackend: #path })
148            }
149            Some((None, _)) => None,
150        };
151        let slot_data_ty = match slot_data_attr {
152            None => quote! { () },
153            Some(path) => {
154                let span = path.span();
155                quote_spanned! {span=> #path }
156            }
157        };
158        let locale_group = match locale_group_name {
159            None => LocaleGroup::get_default(),
160            Some(x) => LocaleGroup::get(&x.to_string()),
161        };
162
163        // find component name and type params
164        let component_name = {
165            let component_name_ident = &inner.ident;
166            let component_type_params = inner.generics.params.iter().map(|x| {
167                let span = x.span();
168                match x {
169                    GenericParam::Type(x) => {
170                        let x = x.ident.clone();
171                        quote_spanned! {span=> #x }
172                    }
173                    GenericParam::Lifetime(x) => {
174                        let x = x.lifetime.clone();
175                        quote_spanned! {span=> #x }
176                    }
177                    GenericParam::Const(x) => {
178                        let x = x.ident.clone();
179                        quote_spanned! {span=> #x }
180                    }
181                }
182            });
183            quote! {
184                #component_name_ident<#(#component_type_params),*>
185            }
186        };
187
188        // set a default component slot kind
189        let mut slot_kind = quote! {
190            maomi::node::NoneSlot
191        };
192
193        // find `template!` invoke
194        let mut template = None;
195        let mut template_field = None;
196        if let Fields::Named(fields) = &mut inner.fields {
197            for field in &mut fields.named {
198                let mut has_template = false;
199                if let Type::Macro(m) = &mut field.ty {
200                    if m.mac.path.is_ident("template") {
201                        if template.is_some() {
202                            Err(syn::Error::new(
203                                m.span(),
204                                "a component struct can only contain one `template!` field",
205                            ))?;
206                            continue;
207                        }
208                        has_template = true;
209                    }
210                }
211                if has_template {
212                    thread_local! {
213                        static EMPTY_TY: Type = parse_str("()").unwrap();
214                    }
215                    if let Type::Macro(m) = &mut field.ty {
216                        let tokens = m.mac.tokens.clone();
217                        let t = Template::parse.parse2(tokens);
218                        if let Ok(x) = &t {
219                            match x.slot_type() {
220                                SlotType::None => {}
221                                SlotType::StaticSingle => {
222                                    slot_kind = quote! {
223                                        maomi::node::StaticSingleSlot
224                                    };
225                                }
226                                SlotType::Dynamic => {
227                                    slot_kind = quote! {
228                                        maomi::node::DynamicSlot
229                                    };
230                                }
231                            }
232                        }
233                        field.ty = parse_quote! {
234                            maomi::template::Template<
235                                #component_name,
236                                maomi::node::DynNodeList,
237                                #slot_kind<maomi::backend::tree::ForestTokenAddr, (maomi::backend::tree::ForestToken, maomi::prop::Prop<#slot_data_ty>)>,
238                            >
239                        };
240                        template = Some(t);
241                        template_field = field.ident.clone();
242                    } else {
243                        unreachable!()
244                    }
245                }
246            }
247        } else {
248            Err(syn::Error::new(
249                inner.span(),
250                "a component struct must be a named struct",
251            ))?;
252        }
253        let template = if let Some(t) = template {
254            t
255        } else {
256            return Err(syn::Error::new(
257                inner.span(),
258                "a component struct must contain a `template!` field",
259            ));
260        };
261
262        Ok(Self {
263            inner,
264            component_name,
265            backend_param,
266            backend_param_in_impl,
267            slot_kind,
268            slot_data_ty,
269            template,
270            template_field: template_field.unwrap(),
271            locale_group,
272        })
273    }
274}
275
276impl ToTokens for ComponentBody {
277    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
278        let Self {
279            inner,
280            component_name,
281            backend_param,
282            backend_param_in_impl,
283            slot_kind,
284            slot_data_ty,
285            template,
286            template_field,
287            locale_group,
288        } = self;
289
290        // write the struct
291        inner.to_tokens(tokens);
292
293        // find generics for impl
294        let impl_type_params = {
295            let items = inner
296                .generics
297                .params
298                .iter()
299                .chain(backend_param_in_impl.as_ref());
300            quote! {
301                <#(#items),*>
302            }
303        };
304        let impl_type_params_without_backend_param = {
305            let items = inner.generics.params.iter();
306            quote! {
307                <#(#items),*>
308            }
309        };
310
311        // write the component template
312        match template.as_ref() {
313            Ok(template) => {
314                let template_children = template.to_children(backend_param, locale_group);
315                quote! {
316                    impl #impl_type_params_without_backend_param maomi::template::ComponentSlotKind for #component_name {
317                        type SlotChildren<C> = #slot_kind<maomi::backend::tree::ForestTokenAddr, C>;
318                        type SlotData = #slot_data_ty;
319                    }
320                }.to_tokens(tokens);
321                quote! {
322                    impl #impl_type_params maomi::template::ComponentTemplate<#backend_param> for #component_name {
323                        type TemplateField = maomi::template::Template<
324                            Self,
325                            Self::TemplateStructure,
326                            Self::SlotChildren<(maomi::backend::tree::ForestToken, maomi::prop::Prop<Self::SlotData>)>,
327                        >;
328                        type TemplateStructure = maomi::node::DynNodeList;
329
330                        #[inline]
331                        fn template(&self) -> &Self::TemplateField {
332                            &self.#template_field
333                        }
334
335                        #[inline]
336                        fn template_init(&mut self, __m_init: maomi::template::TemplateInit<#component_name>) {
337                            self.#template_field.init(__m_init);
338                        }
339
340                        #[inline]
341                        fn template_create_or_update<'__m_b>(
342                            &'__m_b mut self,
343                            __m_backend_context: &'__m_b maomi::BackendContext<#backend_param>,
344                            __m_backend_element: &'__m_b mut maomi::backend::tree::ForestNodeMut<
345                                <#backend_param as maomi::backend::Backend>::GeneralElement,
346                            >,
347                            __m_slot_fn: &mut dyn FnMut(
348                                maomi::node::SlotChange<
349                                    &mut maomi::backend::tree::ForestNodeMut<
350                                        <#backend_param as maomi::backend::Backend>::GeneralElement,
351                                    >,
352                                    &maomi::backend::tree::ForestToken,
353                                    &Self::SlotData,
354                                >,
355                            ) -> Result<(), maomi::error::Error>,
356                        ) -> Result<(), maomi::error::Error>
357                        where
358                            Self: Sized,
359                        {
360                            let __m_event_self_weak = maomi::template::TemplateHelper::component_weak(
361                                &self.#template_field,
362                            ).unwrap();
363                            let mut __m_slot_scopes = self.#template_field.__m_slot_scopes.borrow_mut();
364                            let mut __m_slot_scopes = maomi::node::SlotKindTrait::update(&mut *__m_slot_scopes);
365                            {
366                                let __m_slot_scopes = &mut __m_slot_scopes;
367                                let __m_self_owner_weak = self.#template_field.__m_self_owner_weak.as_ref().unwrap();
368                                let __m_parent_element = __m_backend_element;
369                                let mut __m_children_results = #template_children;
370                                if let Some(__m_children) = self.#template_field.__m_structure.as_ref() {
371                                    __m_children_results(__m_parent_element, Some(&mut *__m_children.borrow_mut()))?;
372                                } else {
373                                    self.#template_field.__m_structure = Some(std::cell::RefCell::new(
374                                        unsafe { __m_children_results(__m_parent_element, None)?.unwrap_unchecked() }
375                                    ));
376                                }
377                            }
378                            maomi::node::SlotKindUpdateTrait::finish(__m_slot_scopes, |(n, _)| {
379                                __m_slot_fn(maomi::node::SlotChange::Removed(&n))?;
380                                Ok(())
381                            })?;
382                            Ok(())
383                        }
384                    }
385                }.to_tokens(tokens);
386            }
387            Err(err) => {
388                err.to_compile_error().to_tokens(tokens);
389            }
390        }
391    }
392}
393
394pub fn component(attr: TokenStream, item: TokenStream) -> TokenStream {
395    let component_attr = parse_macro_input!(attr as ComponentAttr);
396    match ComponentBody::new(component_attr, parse_macro_input!(item as ItemStruct)) {
397        Ok(component_body) => quote! {
398            #component_body
399        }
400        .into(),
401        Err(err) => err.to_compile_error().into(),
402    }
403}