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