silkenweb_macros/
lib.rs

1use parse::Transpile;
2use proc_macro::TokenStream;
3use proc_macro2::Span;
4use proc_macro_error::{abort, abort_call_site, proc_macro_error};
5use quote::quote;
6use silkenweb_css::{Css, NameMapping};
7use syn::{
8    parse_macro_input, Attribute, Data, DataStruct, DeriveInput, Field, Fields, FieldsNamed,
9    FieldsUnnamed, Ident, Index, LitBool,
10};
11
12use crate::parse::Input;
13
14mod parse;
15
16macro_rules! derive_empty(
17    (
18        $($proc_name:ident ( $type_path:path, $type_name:ident ); )*
19    ) => {$(
20        #[doc = concat!("Derive `", stringify!($type_name), "`")]
21        #[doc = ""]
22        #[doc = "This will derive an instance with an empty body:"]
23        #[doc = ""]
24        #[doc = concat!("`impl ", stringify!($type_name), " for MyType {}`")]
25        #[doc = ""]
26        #[doc = "Types with generic parameters are supported."]
27        #[proc_macro_derive($type_name)]
28        pub fn $proc_name(item: TokenStream) -> TokenStream {
29            let item: DeriveInput = parse_macro_input!(item);
30            let (impl_generics, ty_generics, where_clause) = item.generics.split_for_impl();
31            let name = item.ident;
32
33            quote!(
34                impl #impl_generics ::silkenweb::$type_path::$type_name
35                for #name #ty_generics #where_clause {}
36            ).into()
37        }
38    )*}
39);
40
41derive_empty!(
42    derive_value(value, Value);
43    derive_html_element(elements, HtmlElement);
44    derive_aria_element(elements, AriaElement);
45    derive_html_element_events(elements, HtmlElementEvents);
46    derive_element_events(elements, ElementEvents);
47);
48
49#[proc_macro_derive(StrAttribute)]
50pub fn str_attribute(item: TokenStream) -> TokenStream {
51    let item: DeriveInput = parse_macro_input!(item);
52    let item_name = item.ident;
53    let (impl_generics, ty_generics, where_clause) = item.generics.split_for_impl();
54
55    quote!(
56        impl #impl_generics ::silkenweb::attribute::Attribute
57        for #item_name #ty_generics #where_clause {
58            type Text<'a> = &'a str;
59
60            fn text(&self) -> Option<<Self as ::silkenweb::attribute::Attribute>::Text<'_>> {
61                Some(self.as_ref())
62            }
63        }
64
65        impl #impl_generics ::silkenweb::attribute::AsAttribute<#item_name>
66        for #item_name #ty_generics #where_clause {}
67    )
68    .into()
69}
70
71#[proc_macro_derive(ChildElement, attributes(child_element))]
72#[proc_macro_error]
73pub fn derive_child_element(item: TokenStream) -> TokenStream {
74    let item: DeriveInput = parse_macro_input!(item);
75    let (impl_generics, ty_generics, where_clause) = item.generics.split_for_impl();
76    let item_name = item.ident;
77
78    let fields = fields(item.data);
79    let target_index = target_field_index("child_element", &fields);
80
81    let target_field = fields[target_index].clone();
82    let target_type = target_field.ty;
83    let target = field_token(target_index, target_field.ident);
84    let dom_type = quote!(<#target_type as ::silkenweb::dom::InDom>::Dom);
85
86    quote!(
87        impl #impl_generics ::std::convert::From<#item_name #ty_generics>
88        for ::silkenweb::node::element::GenericElement<
89            #dom_type,
90            ::silkenweb::node::element::Const
91        >
92        #where_clause
93        {
94            fn from(value: #item_name #ty_generics) -> Self {
95                value.#target.into()
96            }
97        }
98
99        impl #impl_generics ::std::convert::From<#item_name #ty_generics>
100        for ::silkenweb::node::Node<#dom_type>
101        #where_clause
102        {
103            fn from(value: #item_name #ty_generics) -> Self {
104                value.#target.into()
105            }
106        }
107
108        impl #impl_generics ::silkenweb::value::Value
109        for #item_name #ty_generics #where_clause {}
110    )
111    .into()
112}
113
114#[proc_macro_derive(Element, attributes(element))]
115#[proc_macro_error]
116pub fn derive_element(item: TokenStream) -> TokenStream {
117    let item: DeriveInput = parse_macro_input!(item);
118    let (impl_generics, ty_generics, where_clause) = item.generics.split_for_impl();
119    let item_name = item.ident;
120
121    let fields = fields(item.data);
122    let target_index = target_field_index("element", &fields);
123
124    let field = fields[target_index].clone();
125    let target_type = field.ty;
126
127    let other_field_idents = fields.into_iter().enumerate().filter_map(|(index, field)| {
128        (index != target_index).then(|| field_token(index, field.ident))
129    });
130    let other_fields = quote!(#(, #other_field_idents: self.#other_field_idents)*);
131
132    let target = field_token(0, field.ident);
133
134    quote!(
135        impl #impl_generics ::silkenweb::node::element::Element
136        for #item_name #ty_generics #where_clause {
137            type Dom = <#target_type as ::silkenweb::node::element::Element>::Dom;
138            type DomElement = <#target_type as ::silkenweb::node::element::Element>::DomElement;
139
140            fn class<'a, T>(self, class: impl ::silkenweb::value::RefSignalOrValue<'a, Item = T>) -> Self
141            where
142                T: 'a + AsRef<str>
143            {
144                Self {#target: self.#target.class(class) #other_fields}
145            }
146
147            fn classes<'a, T, Iter>(
148                self,
149                classes: impl ::silkenweb::value::RefSignalOrValue<'a, Item = Iter>,
150            ) -> Self
151            where
152                T: 'a + AsRef<str>,
153                Iter: 'a + IntoIterator<Item = T>,
154            {
155                    Self {#target: self.#target.classes(classes) #other_fields}
156            }
157
158            fn attribute<'a>(
159                mut self,
160                name: &str,
161                value: impl ::silkenweb::value::RefSignalOrValue<'a, Item = impl ::silkenweb::attribute::Attribute>,
162            ) -> Self {
163                Self{#target: self.#target.attribute(name, value) #other_fields}
164            }
165
166            fn style_property<'a>(
167                self,
168                name: impl Into<String>,
169                value: impl ::silkenweb::value::RefSignalOrValue<'a, Item = impl AsRef<str> + 'a>
170            ) -> Self {
171                Self{#target: self.#target.style_property(name, value) #other_fields}
172            }
173
174            fn effect(self, f: impl FnOnce(&Self::DomElement) + 'static) -> Self {
175                Self{#target: self.#target.effect(f) #other_fields}
176            }
177
178            fn effect_signal<T: 'static>(
179                self,
180                sig: impl ::silkenweb::macros::Signal<Item = T> + 'static,
181                f: impl Fn(&Self::DomElement, T) + Clone + 'static,
182            ) -> Self {
183                Self{#target: self.#target.effect_signal(sig, f) #other_fields}
184            }
185
186            fn map_element(self, f: impl FnOnce(&Self::DomElement) + 'static) -> Self {
187                Self{#target: self.#target.map_element(f) #other_fields}
188            }
189
190            fn map_element_signal<T: 'static>(
191                self,
192                sig: impl ::silkenweb::macros::Signal<Item = T> + 'static,
193                f: impl Fn(&Self::DomElement, T) + Clone + 'static,
194            ) -> Self {
195                Self{#target: self.#target.map_element_signal(sig, f) #other_fields}
196            }
197
198            fn handle(&self) -> ::silkenweb::node::element::ElementHandle<Self::Dom, Self::DomElement> {
199                self.#target.handle()
200            }
201
202            fn spawn_future(self, future: impl ::std::future::Future<Output = ()> + 'static) -> Self {
203                Self{#target: self.#target.spawn_future(future) #other_fields}
204            }
205
206            fn on(self, name: &'static str, f: impl FnMut(::silkenweb::macros::JsValue) + 'static) -> Self {
207                Self{#target: self.#target.on(name, f) #other_fields}
208            }
209        }
210    )
211    .into()
212}
213
214/// Find the index of the field with `#[<attr_name>(target)]`
215fn target_field_index(attr_name: &str, fields: &[Field]) -> usize {
216    let mut target_index = None;
217
218    for (index, field) in fields.iter().enumerate() {
219        for attr in &field.attrs {
220            if target_index.is_some() {
221                abort!(attr, "Only one target field can be specified");
222            }
223
224            check_attr_matches(attr, attr_name, "target");
225            target_index = Some(index);
226        }
227    }
228
229    target_index.unwrap_or_else(|| {
230        if fields.len() != 1 {
231            abort_call_site!(
232                "There must be exactly one field, or specify `#[{}(target)]` on a single field",
233                attr_name
234            );
235        }
236
237        0
238    })
239}
240
241/// Make sure an attribute matches #[<name>(<value>)]
242fn check_attr_matches(attr: &Attribute, name: &str, value: &str) {
243    let path = attr.path();
244
245    if !path.is_ident(name) {
246        abort!(path, "Expected `{}`", name);
247    }
248
249    attr.parse_nested_meta(|meta| {
250        if !meta.path.is_ident(value) {
251            abort!(meta.path, "Expected `{}`", value);
252        }
253
254        if !meta.input.is_empty() {
255            abort!(meta.input.span(), "Unexpected token");
256        }
257
258        Ok(())
259    })
260    .unwrap()
261}
262
263fn fields(struct_data: Data) -> Vec<Field> {
264    let fields = match struct_data {
265        Data::Struct(DataStruct { fields, .. }) => fields,
266        _ => abort_call_site!("Only structs are supported"),
267    };
268
269    match fields {
270        Fields::Unnamed(FieldsUnnamed { unnamed, .. }) => unnamed,
271        Fields::Named(FieldsNamed { named, .. }) => named,
272        Fields::Unit => abort!(fields, "There must be at least one field"),
273    }
274    .into_iter()
275    .collect()
276}
277
278fn field_token(index: usize, ident: Option<Ident>) -> proc_macro2::TokenStream {
279    if let Some(ident) = ident {
280        quote!(#ident)
281    } else {
282        let index = Index::from(index);
283        quote!(#index)
284    }
285}
286
287#[proc_macro_attribute]
288#[proc_macro_error]
289pub fn cfg_browser(attr: TokenStream, item: TokenStream) -> TokenStream {
290    let in_browser: LitBool = parse_macro_input!(attr);
291
292    let cfg_check = if in_browser.value() {
293        quote!(#[cfg(all(target_arch = "wasm32", target_os = "unknown"))])
294    } else {
295        quote!(#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))])
296    };
297    let item = proc_macro2::TokenStream::from(item);
298
299    quote!(
300        #cfg_check
301        #item
302    )
303    .into()
304}
305
306#[proc_macro]
307#[proc_macro_error]
308pub fn css(input: TokenStream) -> TokenStream {
309    let Input {
310        mut source,
311        public,
312        prefix,
313        include_prefixes,
314        exclude_prefixes,
315        validate,
316        auto_mount,
317        transpile,
318    } = parse_macro_input!(input);
319
320    let name_mappings = source
321        .transpile(validate, transpile.map(Transpile::into))
322        .unwrap_or_else(|e| abort_call_site!(e));
323
324    let variables = source.variable_names().map(|variable| NameMapping {
325        plain: variable.clone(),
326        mangled: format!("--{variable}"),
327    });
328
329    let classes = name_mappings.unwrap_or_else(|| {
330        source
331            .class_names()
332            .map(|class| NameMapping {
333                plain: class.clone(),
334                mangled: class,
335            })
336            .collect()
337    });
338
339    let classes = only_matching_prefixes(&include_prefixes, &exclude_prefixes, classes.into_iter());
340    let variables = only_matching_prefixes(&include_prefixes, &exclude_prefixes, variables);
341
342    if let Some(prefix) = prefix {
343        let classes = strip_prefixes(&prefix, classes);
344        let variables = strip_prefixes(&prefix, variables);
345        code_gen(&source, public, auto_mount, classes, variables)
346    } else {
347        code_gen(&source, public, auto_mount, classes, variables)
348    }
349}
350
351fn only_matching_prefixes<'a>(
352    include_prefixes: &'a Option<Vec<String>>,
353    exclude_prefixes: &'a [String],
354    names: impl Iterator<Item = NameMapping> + 'a,
355) -> impl Iterator<Item = NameMapping> + 'a {
356    names.filter(move |mapping| {
357        let ident = &mapping.plain;
358
359        let include = if let Some(include_prefixes) = include_prefixes.as_ref() {
360            any_prefix_matches(ident, include_prefixes)
361        } else {
362            true
363        };
364
365        let exclude = any_prefix_matches(ident, exclude_prefixes);
366
367        include && !exclude
368    })
369}
370
371fn strip_prefixes<'a>(
372    prefix: &'a str,
373    names: impl Iterator<Item = NameMapping> + 'a,
374) -> impl Iterator<Item = NameMapping> + 'a {
375    names.filter_map(move |NameMapping { plain, mangled }| {
376        plain
377            .strip_prefix(prefix)
378            .map(str::to_string)
379            .map(|plain| NameMapping { plain, mangled })
380    })
381}
382
383fn any_prefix_matches(x: &str, prefixes: &[String]) -> bool {
384    prefixes.iter().any(|prefix| x.starts_with(prefix))
385}
386
387fn code_gen(
388    source: &Css,
389    public: bool,
390    auto_mount: bool,
391    classes: impl Iterator<Item = NameMapping>,
392    variables: impl Iterator<Item = NameMapping>,
393) -> TokenStream {
394    let classes = classes.map(|name| define_css_entity(name, auto_mount));
395    let variables = variables.map(|name| define_css_entity(name, auto_mount));
396
397    let dependency = source.dependency().into_iter();
398    let content = source.content();
399    let visibility = if public { quote!(pub) } else { quote!() };
400
401    quote!(
402        #(const _: &[u8] = ::std::include_bytes!(#dependency);)*
403
404        #visibility mod class {
405            #(#classes)*
406        }
407
408        #visibility mod var {
409            #(#variables)*
410        }
411
412        #visibility mod stylesheet {
413            use ::silkenweb::document::Document;
414
415            pub fn mount() {
416                use ::silkenweb::dom::DefaultDom;
417                mount_dom::<DefaultDom>();
418            }
419
420            pub fn mount_dom<D: Document>() {
421                use ::std::{panic::Location, sync::Once};
422                use ::silkenweb::{
423                    document::{Document, DocumentHead},
424                    node::element::TextParentElement,
425                    elements::html::style,
426                };
427
428                static INIT: Once = Once::new();
429
430                INIT.call_once(|| {
431                    let location = Location::caller();
432                    let head = DocumentHead::new().child(style().text(text()));
433
434                    D::mount_in_head(
435                        &format!(
436                            "silkenweb-style:{}:{}:{}",
437                            location.file(),
438                            location.line(),
439                            location.column()
440                        ),
441                        head
442                    );
443                });
444            }
445
446            pub fn text() -> &'static str {
447                #content
448            }
449        }
450    )
451    .into()
452}
453
454fn define_css_entity(name: NameMapping, auto_mount: bool) -> proc_macro2::TokenStream {
455    let NameMapping { plain, mangled } = name;
456
457    if !plain.starts_with(char::is_alphabetic) {
458        abort_call_site!(
459            "Identifier '{}' doesn't start with an alphabetic character",
460            plain
461        );
462    }
463
464    let ident = plain.replace(|c: char| !c.is_alphanumeric(), "_");
465
466    if auto_mount {
467        let ident = Ident::new(&ident.to_lowercase(), Span::call_site());
468        quote!(pub fn #ident() -> &'static str {
469            super::stylesheet::mount();
470            #mangled
471        })
472    } else {
473        let ident = Ident::new(&ident.to_uppercase(), Span::call_site());
474        quote!(pub const #ident: &str = #mangled;)
475    }
476}
477
478/// Convert a rust ident to an html ident by stripping any "r#" prefix and
479/// replacing '_' with '-'.
480#[doc(hidden)]
481#[proc_macro]
482#[proc_macro_error]
483pub fn rust_to_html_ident(input: TokenStream) -> TokenStream {
484    let rust_ident: Ident = parse_macro_input!(input);
485    let html_ident = rust_ident.to_string().replace('_', "-");
486    let html_ident_name = html_ident.strip_prefix("r#").unwrap_or(&html_ident);
487
488    quote!(#html_ident_name).into()
489}