Skip to main content

fhtmx_derive/
lib.rs

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    // Extract fields from the parsed data
75    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}