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