1mod utils;
2
3use crate::utils::{DaisyColorAttr, ExprOrString, Mode, PostProc};
4use darling::{FromDeriveInput, FromField, ast::Data};
5use proc_macro::TokenStream;
6use quote::quote;
7use syn::{DeriveInput, Ident};
8
9#[derive(FromField)]
10#[darling(attributes(html_view))]
11struct HtmlViewField {
12 ident: Option<Ident>,
13 #[darling(default)]
14 alias: Option<String>,
15 #[darling(default)]
16 value: Option<ExprOrString>,
17 #[darling(default)]
18 value_display: bool,
19 #[darling(default)]
20 value_debug: bool,
21 #[darling(default)]
22 value_debug_pretty: bool,
23 #[darling(default)]
24 row_class: Option<String>,
25 #[darling(default)]
26 value_class: Option<String>,
27 #[darling(default)]
28 skip: bool,
29}
30
31impl HtmlViewField {
32 fn validate(self) -> darling::Result<Self> {
33 let count = self.value.is_some() as u8
34 + self.value_display as u8
35 + self.value_debug as u8
36 + self.value_debug_pretty as u8;
37 if count > 1 {
38 return Err(darling::Error::custom(
39 "only one of `value`, `value_display`, `value_debug`, or `value_debug_pretty` can be set",
40 ));
41 }
42 Ok(self)
43 }
44}
45
46#[derive(FromDeriveInput)]
47#[darling(attributes(html_view), supports(struct_named))]
48struct HtmlViewInput {
49 ident: Ident,
50 data: Data<(), HtmlViewField>,
51 #[darling(default)]
52 mode: Mode,
53 #[darling(default)]
54 title: Option<ExprOrString>,
55 #[darling(default)]
56 color: Option<DaisyColorAttr>,
57 #[darling(default)]
58 class: Option<ExprOrString>,
59 #[darling(default)]
60 mode_class: Option<ExprOrString>,
61 #[darling(default)]
62 postproc: PostProc,
63}
64
65#[proc_macro_derive(HtmlView, attributes(html_view))]
66pub fn derive_html_view(input: TokenStream) -> TokenStream {
67 let input = syn::parse_macro_input!(input as DeriveInput);
68 let parsed = match HtmlViewInput::from_derive_input(&input) {
69 Ok(v) => v,
70 Err(e) => return e.write_errors().into(),
71 };
72 let mode = parsed.mode;
73
74 let fields = parsed.data.take_struct().expect("expected named struct");
76 let field_items = fields
77 .into_iter()
78 .map(|o| o.validate().unwrap())
79 .filter(|o| !o.skip)
80 .map(|o| {
81 let field_ident = o.ident.unwrap();
82 let key = o.alias.unwrap_or_else(|| field_ident.to_string());
83 let value = match (
84 o.value,
85 o.value_display,
86 o.value_debug,
87 o.value_debug_pretty,
88 ) {
89 (Some(ExprOrString(expr)), false, false, false) => quote! { #expr },
90 (None, true, false, false) => quote! { format!("{}", self.#field_ident) },
91 (None, false, true, false) => quote! { format!("{:?}", self.#field_ident) },
92 (None, false, false, true) => {
93 quote! { pre().add(format!("{:#?}", self.#field_ident)).class("text-wrap") }
94 }
95 (None, false, false, false) => {
96 quote! { (&self.#field_ident).html_content() }
97 }
98 _ => unreachable!(),
99 };
100 let row_class = o.row_class.unwrap_or_else(|| "p-1".to_string());
101 let value_class_call = o.value_class.map(|x| quote! { .class(#x) });
102 match mode {
103 Mode::List => quote! {
104 .add(
105 html_list_row(
106 div().class("font-bold").add(#key),
107 div()#value_class_call.add(#value)
108 )
109 .add_class(#row_class)
110 )
111 },
112 Mode::Table => quote! {
113 .add(
114 tr()
115 .add(th().add(#key))
116 .add(td().add(#value))
117 )
118 },
119 Mode::TableRight => quote! {
120 .add(
121 tr()
122 .add(th().class("text-right").add(#key))
123 .add(td().add(#value))
124 )
125 },
126 }
127 });
128
129 let struct_name = parsed.ident;
130
131 let mode_class_call = parsed
132 .mode_class
133 .map(|ExprOrString(expr)| quote! { .add_class(#expr) });
134 let content = match mode {
135 Mode::List => quote! {
136 dc_list() #mode_class_call #(#field_items)*
137 },
138 Mode::Table | Mode::TableRight => quote! {
139 div()
140 .class("overflow-x-auto")
141 .add(dc_table() #mode_class_call .add(tbody() #(#field_items)*))
142 },
143 };
144
145 let title = match parsed.title {
146 Some(ExprOrString(expr)) => quote! { Some(#expr.as_ref()) },
147 None => quote! { None },
148 };
149 let color_call = parsed
150 .color
151 .map(|x| x.to_tokens())
152 .map(|x| quote! { .add_class(#x) });
153 let class_call = parsed
154 .class
155 .map(|ExprOrString(expr)| quote! { .add_class(#expr) });
156 let card = quote! { mk_card(#title, self.html_content()) #color_call #class_call };
157 let postproc_card = match parsed.postproc {
158 PostProc::None => quote! { #card },
159 PostProc::Flag => quote! { self.postproc(#card) },
160 PostProc::Custom(expr) => quote! { #expr(#card) },
161 };
162
163 quote! {
164 impl HtmlView for #struct_name {
165 fn html_content(&self) -> HtmlNode {
166 #content .into_node()
167 }
168
169 fn html_view(&self) -> HtmlNode {
170 #postproc_card .into_node()
171 }
172 }
173 }
174 .into()
175}