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}