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");
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 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
99struct TemplateArgs{
101 src: Option<String>,
103 helpers: Vec<String>,
105 minify: bool
107}
108
109fn 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
118impl 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
150struct DisplayParts{
152 name: Ident,
154 generics: proc_macro2::TokenStream,
156 uses: proc_macro2::TokenStream,
158 content: proc_macro2::TokenStream
160}
161
162impl 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 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#[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}