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        //println!("workspace {:?}", workspace);
78        let cargo = workspace.join("Cargo.toml");
79        if cargo.exists(){
80            let contents = std::fs::read_to_string(&cargo).map(|contents| Value::from_str(&contents).unwrap()).unwrap();
81            if let Some(members) = contents.get("workspace")
82            .and_then(|workspace| workspace.get("members"))
83            .and_then(|members| members.as_array()){
84                //println!("searching for {} in members {:?}", name, members);
85                if members.iter().find(|item| item.as_str().unwrap() == name).is_some(){
86                    return workspace;
87                }
88            }
89        }
90        name = match workspace.file_name(){
91            None => return path,
92            Some(base) => format!("{}/{}", base.to_str().unwrap(), name)
93        };
94        local = workspace;
95        continue;
96    }
97}
98
99/// Arguments for the template attribute
100struct TemplateArgs{
101    /// Path to the template file
102    src: Option<String>,
103    /// List of custom helper functions
104    helpers: Vec<String>,
105    /// Whether to minify the HTML output
106    minify: bool
107}
108
109/// Parses helper function names from the attribute
110fn parse_helpers(input: ParseStream, helpers: &mut Vec<String>) -> Result<()>{
111    input.parse::<proc_macro2::Group>()?.stream().into_iter().for_each(|item| {
112        let helper = item.to_string();
113        helpers.push(helper[1..helper.len() - 1].to_string());
114    });
115    Ok(())
116}
117
118/// Parses the template attribute arguments
119impl Parse for TemplateArgs{
120    fn parse(input: ParseStream) -> Result<Self> {
121        let mut src : Option<String> = None;
122        let mut minify = true;
123        let mut helpers = Vec::<String>::new();
124        loop {
125            let ident = input.parse::<Ident>()?;
126            let label = ident.to_string();
127            input.parse::<Token!(=)>()?;
128            match label.as_str(){
129                "minify" => minify = input.parse::<LitBool>()?.value(),
130                "path" => src = Some(input.parse::<LitStr>()?.value()),
131                "helpers" => parse_helpers(input, &mut helpers)?,
132                _ => return Err(
133                    syn::Error::new(
134                        ident.span(),
135                        format!("unknown attribute {}", label)
136                    )
137                )
138            }
139            if input.is_empty(){
140                break;
141            }
142            input.parse::<Token!(,)>()?;
143        }
144        Ok(TemplateArgs{
145            src, helpers, minify
146        })
147    }
148}
149
150/// Parts needed for generating the Display implementation
151struct DisplayParts{
152    /// Name of the struct being derived
153    name: Ident,
154    /// Generic parameters
155    generics: proc_macro2::TokenStream,
156    /// Required use statements
157    uses: proc_macro2::TokenStream,
158    /// Generated template code
159    content: proc_macro2::TokenStream
160}
161
162/// Parses the derive input and generates the implementation
163impl Parse for DisplayParts{
164    fn parse(input: ParseStream) -> Result<Self> {
165        let input = input.parse::<DeriveInput>()?;
166        let lifetimes = input.generics.into_token_stream();
167        let name = input.ident;
168        let attr = match input.attrs.get(0){
169            None => return Err(
170                syn::Error::new(
171                    name.span(),
172                    "missing template macro"
173                )
174            ),
175            Some(attr) => attr
176        };
177        let args = attr.parse_args::<TemplateArgs>()?;
178        let src = match args.src{
179            None => return Err(
180                syn::Error::new(
181                    attr.span(),
182                    "missing path attribute in template macro"
183                )
184            ),
185            Some(src) => src
186        };
187        let path = find_path().join(src);
188        //println!("reading {:?}", path);
189        let mut buf = match std::fs::read_to_string(&path){
190            Ok(src) => src,
191            Err(err) => return Err(
192                syn::Error::new(
193                    attr.span(),
194                    format!(
195                        "unable to read {:?}, {}", path, err.to_string()
196                    )
197                )
198            )
199        };
200        #[cfg(feature = "minify-html")]
201        if args.minify{
202            unsafe {
203                buf = String::from_utf8_unchecked(minify(buf.as_bytes(), &build_helper::COMPRESS_CONFIG));
204            }
205        }
206        let mut factories = BlockMap::new();
207        add_builtins(&mut factories);
208        let mut rust = match Compiler::new(Options{
209            write_var_name: "f",
210            root_var_name: Some("self")
211        }, factories).compile(&buf){
212            Ok(rust) => rust,
213            Err(err) => {
214                return Err(
215                    syn::Error::new(
216                        attr.span(),
217                        err.to_string()
218                    )
219                )
220            }
221        };
222        rust.using.insert("WithRustyHandlebars".to_string());
223        rust.using.insert(USE_AS_DISPLAY.to_string());
224        for helper in args.helpers{
225            rust.using.insert(helper);
226        }
227        Ok(Self{
228            name, generics: lifetimes,
229            uses: proc_macro2::token_stream::TokenStream::from_str(&rust.uses().to_string())?,
230            content: proc_macro2::token_stream::TokenStream::from_str(&rust.code)?
231        })
232    }
233}
234
235/// Derive macro for implementing Handlebars templating
236#[proc_macro_derive(WithRustyHandlebars, attributes(template))]
237pub fn make_renderable(raw: TokenStream) -> TokenStream{
238    let DisplayParts{
239        name, generics, uses, content
240    } = parse_macro_input!(raw as DisplayParts);
241
242    let mod_name = proc_macro2::token_stream::TokenStream::from_str((
243        format!("{}_with_rusty_handlebars_impl", name.to_string().to_lowercase())
244    ).as_str()).unwrap();
245    let generics_str = generics.to_string();
246    let cleaned_generics = proc_macro2::token_stream::TokenStream::from_str(Regex::new(r":[^,>]+").unwrap().replace(&generics_str, "").as_ref()).unwrap();
247    TokenStream::from(quote! {
248        mod #mod_name{
249            use std::fmt::Display;
250            #uses;
251            use super::#name;
252            impl #generics Display for #name #cleaned_generics {
253                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
254                    #content
255                    Ok(())
256                }
257            }
258            impl #generics WithRustyHandlebars for #name #cleaned_generics {}
259            impl #generics AsDisplay for #name #cleaned_generics {
260                fn as_display(&self) -> impl Display{
261                    self
262                }
263            }
264        }
265    })
266}