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