lookbook_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::{quote, ToTokens};
3use syn::{parse_macro_input, spanned::Spanned, Attribute, Expr, FnArg, ItemFn, Lit, Meta};
4
5#[proc_macro_attribute]
6pub fn preview(_attrs: TokenStream, input: TokenStream) -> TokenStream {
7    let item = parse_macro_input!(input as ItemFn);
8
9    let docs = collect_docs(&item.attrs);
10
11    let ident = item.sig.ident.clone();
12    let block = item.block.clone();
13    let vis = item.vis.clone();
14
15    let s = ident.to_string();
16    let name = s.strip_suffix("Preview").unwrap_or(&s);
17
18    let mut states = Vec::new();
19    let mut from_states = Vec::new();
20    let mut controls = Vec::new();
21    for arg in item.sig.inputs {
22        match arg {
23            FnArg::Typed(typed_arg) => {
24                let mut docs = String::new();
25                let mut default = None;
26
27                for attr in typed_arg.attrs {
28                    let path = attr.path().get_ident().unwrap().to_string();
29                    if path == "doc" {
30                        let meta = attr.meta.require_name_value().unwrap();
31                        if let Expr::Lit(expr) = &meta.value {
32                            if let Lit::Str(lit) = &expr.lit {
33                                docs.push_str(&lit.value());
34                                docs.push('\n');
35                            }
36                        }
37                    } else if path == "lookbook" {
38                        let path = attr.meta.require_list().unwrap();
39                        let meta: Meta = syn::parse2(path.tokens.clone()).unwrap();
40
41                        if let Meta::NameValue(meta_name_value) = meta {
42                            if meta_name_value.path.is_ident("default") {
43                                let value = meta_name_value.value;
44                                default = Some(value);
45                            }
46                        }
47                    }
48                }
49
50                let ty = typed_arg.ty;
51                let pat = typed_arg.pat;
52                let pat_name = pat.to_token_stream().to_string();
53
54                states.push(quote! {
55                    let default = <#ty>::state(Some(#default));
56                    let #pat = use_signal(|| default);
57                });
58                from_states.push(quote!(let #pat = <#ty>::from_state(&*#pat.read());));
59
60                let ty_name = ty.span().source_text().unwrap();
61                let default_string = default
62                    .map(|expr| expr.span().source_text().unwrap())
63                    .unwrap_or_default();
64
65                controls.push(quote!(tr {
66                    border_bottom: "2px solid #e7e7e7",
67                    td { padding_left: "20px", p { color: "#222", font_weight: 600, #pat_name } }
68                    td { code { #ty_name } }
69                    td { p { #docs } }
70                    td { code { #default_string } }
71                    td { padding_right: "20px", { <#ty>::control(#pat_name, #pat) } }
72                }));
73            }
74            _ => todo!(),
75        }
76    }
77
78    let controls = render_with_location(quote!(#( #controls )*), name, 0);
79
80    let look = render_with_location(
81        quote!(
82            lookbook::Look { name: #name, docs: #docs, controls: controls,
83                #block
84            }
85        ),
86        name,
87        1,
88    );
89
90    let expanded = quote! {
91        #[allow(non_upper_case_globals)]
92        #vis static #ident: lookbook::Preview = lookbook::Preview::new(#name, |()| {
93            use dioxus::prelude::*;
94            use lookbook::Control;
95
96            fn f() -> Element {
97                #(#states)*
98
99                let controls = #controls.ok_or_else(|| todo!());
100
101                #(#from_states)*
102
103                rsx!(lookbook::Look { name: #name, docs: #docs, controls: controls,
104                    #block
105                })
106            }
107            f()
108        });
109    };
110    expanded.into()
111}
112
113fn render_with_location(
114    tokens: proc_macro2::TokenStream,
115    name: &str,
116    idx: u8,
117) -> proc_macro2::TokenStream {
118    let location = format!("__lookbook/{name}.rs:0:0:{idx}");
119    let rsx: dioxus_rsx::CallBody = syn::parse2(tokens).unwrap();
120    rsx.render_with_location(location)
121}
122
123fn collect_docs(attrs: &[Attribute]) -> String {
124    let mut docs = String::new();
125    for attr in attrs {
126        if attr.path().get_ident().unwrap().to_string() == "doc" {
127            let meta = attr.meta.require_name_value().unwrap();
128            if let Expr::Lit(expr) = &meta.value {
129                if let Lit::Str(lit) = &expr.lit {
130                    docs.push_str(&lit.value());
131                    docs.push('\n');
132                }
133            }
134        }
135    }
136    docs
137}