use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Error, Expr, ExprLit, Lit, Meta};
#[proc_macro_derive(Template, attributes(template))]
pub fn derive_template(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let (template_path, parser) = match extract_template_attributes(&input) {
Ok(attrs) => attrs,
Err(err) => return err.to_compile_error().into(),
};
let template_name = format!("_LIQUID_TEMPLATE_{}", name).to_uppercase();
let template_ident = syn::Ident::new(&template_name, name.span());
let include_str_relative_to_manifest_dir = quote! {
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/", #template_path))
};
let template_init = if let Some(parser) = parser {
quote! {
static #template_ident: ::std::sync::LazyLock<::liquid::Template> =
::std::sync::LazyLock::new(|| #parser.parse(#include_str_relative_to_manifest_dir).unwrap());
}
} else {
quote! {
static #template_ident: ::std::sync::LazyLock<::liquid::Template> =
::std::sync::LazyLock::new(|| ::liquid::ParserBuilder::with_stdlib().build().unwrap().parse(#include_str_relative_to_manifest_dir).unwrap());
}
};
let water_impl = quote! {
impl ::anvil_liquid::Water for #name {
fn liquid(&self, writer: &mut dyn ::std::io::Write) -> ::std::result::Result<(), ::liquid::Error> {
let object = ::liquid::to_object(self)?;
#template_ident.render_to(writer, &object)
}
}
};
let expanded = quote! {
#template_init
#water_impl
};
TokenStream::from(expanded)
}
fn extract_template_attributes(input: &DeriveInput) -> Result<(String, Option<Expr>), Error> {
for attr in &input.attrs {
if attr.path().is_ident("template") {
if let Ok(expr) = attr.parse_args::<Expr>() {
if let Expr::Lit(ExprLit {
lit: Lit::Str(lit_str),
..
}) = &expr
{
return Ok((lit_str.value(), None));
} else if let Expr::Tuple(expr_tuple) = &expr {
if !expr_tuple.elems.is_empty() {
if let Some(Expr::Lit(ExprLit {
lit: Lit::Str(lit_str),
..
})) = expr_tuple.elems.first()
{
let mut parser = None;
for elem in expr_tuple.elems.iter().skip(1) {
if let Expr::Assign(assign) = elem {
if let Expr::Path(path) = &*assign.left {
if path.path.is_ident("parser") {
parser = Some(*assign.right.clone());
break;
}
}
}
}
return Ok((lit_str.value(), parser));
}
}
}
}
if let Meta::List(_) = &attr.meta {
let mut template_path = None;
let mut parser_expr = None;
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("path") {
if let Ok(Expr::Lit(ExprLit { lit: Lit::Str(lit_str), .. })) = meta.value()?.parse::<Expr>() {
if template_path.is_some() {
return Err(meta.error("Duplicate 'path' attribute"));
}
template_path = Some(lit_str.value());
return Ok(());
}
Err(meta.error("Expected a string literal for 'path' attribute"))
} else if meta.path.is_ident("parser") {
if let Ok(expr) = meta.value()?.parse::<Expr>() {
if parser_expr.is_some() {
return Err(meta.error("Duplicate 'parser' attribute"));
}
parser_expr = Some(expr);
return Ok(());
}
Err(meta.error("Expected an expression for 'parser' attribute"))
} else {
Err(meta.error("Unsupported attribute key inside #[template(...)]. Expected 'path' or 'parser'."))
}
})?;
if let Some(path) = template_path {
return Ok((path, parser_expr));
}
}
return Err(Error::new_spanned(
attr,
"Expected template attribute to be in one of these formats:\n\
1. #[template(\"path/to/template\")]\n\
2. #[template(\"path/to/template\", parser = PARSER)]\n\
3. #[template(path = \"path/to/template\")]\n\
4. #[template(path = \"path/to/template\", parser = PARSER)]\n\
\n\
Note: Template paths are relative to CARGO_MANIFEST_DIR",
));
}
}
Err(Error::new_spanned(
input,
"Missing #[template(...)] attribute. Expected one of these formats:\n\
1. #[template(\"path/to/template\")]\n\
2. #[template(\"path/to/template\", parser = PARSER)]\n\
3. #[template(path = \"path/to/template\")]\n\
4. #[template(path = \"path/to/template\", parser = PARSER)]\n\
\n\
Note: Template paths are relative to CARGO_MANIFEST_DIR",
))
}