rusty_handlebars_derive/
lib.rs

1//! Derive macro for Handlebars templating
2//!
3//! This crate provides the derive macro implementation for the `WithRustyHandlebars` trait.
4//! It processes Handlebars templates at compile time and generates the necessary
5//! implementations for template rendering.
6//!
7//! # Usage
8//!
9//! ```rust
10//! use rusty_handlebars::WithRustyHandlebars;
11//!
12//! #[derive(WithRustyHandlebars)]
13//! #[template(path = "templates/hello.hbs")]
14//! struct HelloTemplate {
15//!     name: String,
16//! }
17//! ```
18//!
19//! # Template Attributes
20//!
21//! The `#[template(...)]` attribute supports the following options:
22//!
23//! - `path`: Path to the Handlebars template file (required)
24//! - `minify`: Whether to minify the HTML output (default: true)
25//! - `helpers`: List of custom helper functions to use in the template
26//!
27//! # Example with All Options
28//!
29//! ```rust
30//! use rusty_handlebars::WithRustyHandlebars;
31//!
32//! #[derive(WithRustyHandlebars)]
33//! #[template(
34//!     path = "templates/hello.hbs",
35//!     minify = true,
36//!     helpers = ["format_date", "capitalize"]
37//! )]
38//! struct HelloTemplate {
39//!     name: String,
40//!     date: String,
41//! }
42//! ```
43//!
44//! # Implementation Details
45//!
46//! The derive macro:
47//! 1. Reads and processes the Handlebars template at compile time
48//! 2. Generates a Display implementation for the struct
49//! 3. Implements the WithRustyHandlebars trait
50//! 4. Implements the AsDisplay trait
51//! 5. Optionally minifies the HTML output
52//! 6. Adds support for custom helper functions
53
54use minify_html::minify;
55use regex::Regex;
56use rusty_handlebars_parser::{add_builtins, build_helper, BlockMap, Compiler, Options, USE_AS_DISPLAY};
57use proc_macro::TokenStream;
58use quote::{quote, ToTokens};
59use std::env;
60use std::path::{Path, PathBuf};
61use std::str::FromStr;
62use syn::parse::{Parse, ParseStream};
63use syn::{parse_macro_input, DeriveInput, Ident, LitBool, LitStr, Result, Token};
64use syn::spanned::Spanned;
65use toml::Value;
66
67/// Finds the workspace root path for template resolution
68fn find_path() -> PathBuf{
69    let path = Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap()).to_path_buf();
70    let mut name = path.file_name().unwrap().to_str().unwrap().to_string();
71    let mut local = path.clone();
72    loop{  
73        let workspace = match local.parent(){
74            None => return path,
75            Some(parent) => parent.to_path_buf()
76        };
77        let cargo = workspace.join("Cargo.toml");
78        if cargo.exists(){
79            let contents = std::fs::read_to_string(&cargo).map(|contents| Value::from_str(&contents).unwrap()).unwrap();
80            if let Some(members) = contents.get("workspace")
81            .and_then(|workspace| workspace.get("members"))
82            .and_then(|members| members.as_array()){
83                if members.iter().find(|item| item.as_str().unwrap() == name).is_some(){
84                    return workspace;
85                }
86            }
87        }
88        name = match workspace.file_name(){
89            None => return path,
90            Some(base) => format!("{}/{}", base.to_str().unwrap(), name)
91        };
92        local = workspace;
93        continue;
94    }
95}
96
97/// Arguments for the template attribute
98struct TemplateArgs{
99    /// Path to the template file
100    src: Option<String>,
101    /// List of custom helper functions
102    helpers: Vec<String>,
103    /// Whether to minify the HTML output
104    minify: bool
105}
106
107/// Parses helper function names from the attribute
108fn parse_helpers(input: ParseStream, helpers: &mut Vec<String>) -> Result<()>{
109    input.parse::<proc_macro2::Group>()?.stream().into_iter().for_each(|item| {
110        let helper = item.to_string();
111        helpers.push(helper[1..helper.len() - 1].to_string());
112    });
113    Ok(())
114}
115
116/// Parses the template attribute arguments
117impl Parse for TemplateArgs{
118    fn parse(input: ParseStream) -> Result<Self> {
119        let mut src : Option<String> = None;
120        let mut minify = true;
121        let mut helpers = Vec::<String>::new();
122        loop {
123            let ident = input.parse::<Ident>()?;
124            let label = ident.to_string();
125            input.parse::<Token!(=)>()?;
126            match label.as_str(){
127                "minify" => minify = input.parse::<LitBool>()?.value(),
128                "path" => src = Some(input.parse::<LitStr>()?.value()),
129                "helpers" => parse_helpers(input, &mut helpers)?,
130                _ => return Err(
131                    syn::Error::new(
132                        ident.span(),
133                        format!("unknown attribute {}", label)
134                    )
135                )
136            }
137            if input.is_empty(){
138                break;
139            }
140            input.parse::<Token!(,)>()?;
141        }
142        Ok(TemplateArgs{
143            src, helpers, minify
144        })
145    }
146}
147
148/// Parts needed for generating the Display implementation
149struct DisplayParts{
150    /// Name of the struct being derived
151    name: Ident,
152    /// Generic parameters
153    generics: proc_macro2::TokenStream,
154    /// Required use statements
155    uses: proc_macro2::TokenStream,
156    /// Generated template code
157    content: proc_macro2::TokenStream
158}
159
160/// Parses the derive input and generates the implementation
161impl Parse for DisplayParts{
162    fn parse(input: ParseStream) -> Result<Self> {
163        let input = input.parse::<DeriveInput>()?;
164        let lifetimes = input.generics.into_token_stream();
165        let name = input.ident;
166        let attr = match input.attrs.get(0){
167            None => return Err(
168                syn::Error::new(
169                    name.span(),
170                    "missing template macro"
171                )
172            ),
173            Some(attr) => attr
174        };
175        let args = attr.parse_args::<TemplateArgs>()?;
176        let src = match args.src{
177            None => return Err(
178                syn::Error::new(
179                    attr.span(),
180                    "missing path attribute in template macro"
181                )
182            ),
183            Some(src) => src
184        };
185        let path = find_path().join(src);
186        //println!("reading {:?}", path);
187        let mut buf = match std::fs::read_to_string(&path){
188            Ok(src) => src,
189            Err(err) => return Err(
190                syn::Error::new(
191                    attr.span(),
192                    format!(
193                        "unable to read {:?}, {}", path, err.to_string()
194                    )
195                )
196            )
197        };
198        #[cfg(feature = "minify-html")]
199        if args.minify{
200            unsafe {
201                buf = String::from_utf8_unchecked(minify(buf.as_bytes(), &build_helper::COMPRESS_CONFIG));
202            }
203        }
204        let mut factories = BlockMap::new();
205        add_builtins(&mut factories);
206        let mut rust = match Compiler::new(Options{
207            write_var_name: "f",
208            root_var_name: Some("self")
209        }, factories).compile(&buf){
210            Ok(rust) => rust,
211            Err(err) => {
212                return Err(
213                    syn::Error::new(
214                        attr.span(),
215                        err.to_string()
216                    )
217                )
218            }
219        };
220        rust.using.insert("WithRustyHandlebars".to_string());
221        rust.using.insert(USE_AS_DISPLAY.to_string());
222        for helper in args.helpers{
223            rust.using.insert(helper);
224        }
225        Ok(Self{
226            name, generics: lifetimes,
227            uses: proc_macro2::token_stream::TokenStream::from_str(&rust.uses().to_string())?,
228            content: proc_macro2::token_stream::TokenStream::from_str(&rust.code)?
229        })
230    }
231}
232
233/// Derive macro for implementing Handlebars templating
234#[proc_macro_derive(WithRustyHandlebars, attributes(template))]
235pub fn make_renderable(raw: TokenStream) -> TokenStream{
236    let DisplayParts{
237        name, generics, uses, content
238    } = parse_macro_input!(raw as DisplayParts);
239
240    let mod_name = proc_macro2::token_stream::TokenStream::from_str((
241        format!("{}_with_rusty_handlebars_impl", name.to_string().to_lowercase())
242    ).as_str()).unwrap();
243    let generics_str = generics.to_string();
244    let cleaned_generics = proc_macro2::token_stream::TokenStream::from_str(Regex::new(r":[^,>]+").unwrap().replace(&generics_str, "").as_ref()).unwrap();
245    TokenStream::from(quote! {
246        mod #mod_name{
247            use std::fmt::Display;
248            #uses;
249            use super::#name;
250            impl #generics Display for #name #cleaned_generics {
251                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252                    #content
253                    Ok(())
254                }
255            }
256            impl #generics WithRustyHandlebars for #name #cleaned_generics {}
257            impl #generics AsDisplay for #name #cleaned_generics {
258                fn as_display(&self) -> impl Display{
259                    self
260                }
261            }
262        }
263    })
264}