rusty_handlebars_derive/
lib.rs1use 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
67fn 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
97struct TemplateArgs{
99 src: Option<String>,
101 helpers: Vec<String>,
103 minify: bool
105}
106
107fn 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
116impl 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
148struct DisplayParts{
150 name: Ident,
152 generics: proc_macro2::TokenStream,
154 uses: proc_macro2::TokenStream,
156 content: proc_macro2::TokenStream
158}
159
160impl 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 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#[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}