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 row_class: Option<String>,
19 #[darling(default)]
20 value_class: Option<String>,
21 #[darling(default)]
22 skip: bool,
23}
24
25#[derive(FromDeriveInput)]
26#[darling(attributes(html_view), supports(struct_named))]
27struct HtmlViewInput {
28 ident: Ident,
29 data: Data<(), HtmlViewField>,
30 #[darling(default)]
31 mode: Mode,
32 #[darling(default)]
33 title: Option<ExprOrString>,
34 #[darling(default)]
35 color: Option<DaisyColorAttr>,
36 #[darling(default)]
37 class: Option<ExprOrString>,
38 #[darling(default)]
39 mode_class: Option<ExprOrString>,
40 #[darling(default)]
41 postproc: PostProc,
42}
43
44#[proc_macro_derive(HtmlView, attributes(html_view))]
45pub fn derive_html_view(input: TokenStream) -> TokenStream {
46 let input = syn::parse_macro_input!(input as DeriveInput);
47 let parsed = match HtmlViewInput::from_derive_input(&input) {
48 Ok(v) => v,
49 Err(e) => return e.write_errors().into(),
50 };
51 let mode = parsed.mode;
52
53 let fields = parsed.data.take_struct().expect("expected named struct");
55 let field_items = fields.into_iter().filter(|o| !o.skip).map(|o| {
56 let field_ident = o.ident.unwrap();
57 let key = o.alias.unwrap_or_else(|| field_ident.to_string());
58 let value = match o.value {
59 Some(ExprOrString(expr)) => quote! { #expr },
60 None => quote! { self.#field_ident.html_content() },
61 };
62 let row_class = o.row_class.unwrap_or_else(|| "p-1".to_string());
63 let value_class_call = o.value_class.map(|x| quote! { .class(#x) });
64 match mode {
65 Mode::List => quote! {
66 .add(
67 html_list_row(
68 div().class("font-bold").add(#key),
69 div()#value_class_call.add(#value)
70 )
71 .add_class(#row_class)
72 )
73 },
74 Mode::Table => quote! {
75 .add(
76 tr()
77 .add(th().add(#key))
78 .add(td().add(#value))
79 )
80 },
81 Mode::TableRight => quote! {
82 .add(
83 tr()
84 .add(th().class("text-right").add(#key))
85 .add(td().add(#value))
86 )
87 },
88 }
89 });
90
91 let struct_name = parsed.ident;
92
93 let mode_class_call = parsed
94 .mode_class
95 .map(|ExprOrString(expr)| quote! { .add_class(#expr) });
96 let content = match mode {
97 Mode::List => quote! {
98 dc_list() #mode_class_call #(#field_items)*
99 },
100 Mode::Table | Mode::TableRight => quote! {
101 div()
102 .class("overflow-x-auto")
103 .add(dc_table() #mode_class_call .add(tbody() #(#field_items)*))
104 },
105 };
106
107 let title = match parsed.title {
108 Some(ExprOrString(expr)) => quote! { Some(#expr.as_ref()) },
109 None => quote! { None },
110 };
111 let color_call = parsed
112 .color
113 .map(|x| x.to_tokens())
114 .map(|x| quote! { .add_class(#x) });
115 let class_call = parsed
116 .class
117 .map(|ExprOrString(expr)| quote! { .add_class(#expr) });
118 let card = quote! { mk_card(#title, self.html_content()) #color_call #class_call };
119 let postproc_card = match parsed.postproc {
120 PostProc::None => quote! { #card },
121 PostProc::Flag => quote! { self.postproc(#card) },
122 PostProc::Custom(expr) => quote! { #expr(#card) },
123 };
124
125 quote! {
126 impl HtmlView for #struct_name {
127 fn html_content(&self) -> HtmlNode {
128 #content .into_node()
129 }
130
131 fn html_view(&self) -> HtmlNode {
132 #postproc_card .into_node()
133 }
134 }
135 }
136 .into()
137}