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 crate_name: Option<String>
106}
107
108fn 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
117impl 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
151struct DisplayParts{
153 name: Ident,
155 generics: proc_macro2::TokenStream,
157 uses: proc_macro2::TokenStream,
159 content: proc_macro2::TokenStream
161}
162
163impl 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 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#[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}