use crate::config::{get_template_source, read_config_file, Config, WhitespaceHandling}; use crate::heritage::{Context, Heritage}; use crate::input::{Print, Source, TemplateInput}; use crate::parser::{parse, Cond, CondTest, Expr, Loop, Node, Target, When, Whitespace, Ws}; use crate::CompileError; use proc_macro::TokenStream; use quote::{quote, ToTokens}; use syn::punctuated::Punctuated; use std::collections::hash_map::{Entry, HashMap}; use std::path::{Path, PathBuf}; use std::{cmp, hash, mem, str}; /// The actual implementation for askama_derive::Template pub(crate) fn derive_template(input: TokenStream) -> TokenStream { let ast: syn::DeriveInput = syn::parse(input).unwrap(); match build_template(&ast) { Ok(source) => source.parse().unwrap(), Err(e) => e.into_compile_error(), } } /// Takes a `syn::DeriveInput` and generates source code for it /// /// Reads the metadata from the `template()` attribute to get the template /// metadata, then fetches the source from the filesystem. The source is /// parsed, and the parse tree is fed to the code generator. Will print /// the parse tree and/or generated source according to the `print` key's /// value as passed to the `template()` attribute. fn build_template(ast: &syn::DeriveInput) -> Result { let template_args = TemplateArgs::new(ast)?; let config_toml = read_config_file(template_args.config_path.as_deref())?; let config = Config::new(&config_toml, template_args.whitespace.as_ref())?; let input = TemplateInput::new(ast, &config, template_args)?; let source: String = match input.source { Source::Source(ref s) => s.clone(), Source::Path(_) => get_template_source(&input.path)?, }; let mut sources = HashMap::new(); find_used_templates(&input, &mut sources, source)?; let mut parsed = HashMap::new(); for (path, src) in &sources { parsed.insert(path.as_path(), parse(src, input.syntax)?); } let mut contexts = HashMap::new(); for (path, nodes) in &parsed { contexts.insert(*path, Context::new(input.config, path, nodes)?); } let ctx = &contexts[input.path.as_path()]; let heritage = if !ctx.blocks.is_empty() || ctx.extends.is_some() { Some(Heritage::new(ctx, &contexts)) } else { None }; if input.print == Print::Ast || input.print == Print::All { eprintln!("{:?}", parsed[input.path.as_path()]); } let code = Generator::new( &input, &contexts, heritage.as_ref(), MapChain::new(), config.whitespace, ) .build(&contexts[input.path.as_path()])?; if input.print == Print::Code || input.print == Print::All { eprintln!("{code}"); } Ok(code) } #[derive(Default)] pub(crate) struct TemplateArgs { pub(crate) source: Option, pub(crate) print: Print, pub(crate) escaping: Option, pub(crate) ext: Option, pub(crate) syntax: Option, pub(crate) config_path: Option, pub(crate) whitespace: Option, } impl TemplateArgs { fn new(ast: &'_ syn::DeriveInput) -> Result { // Check that an attribute called `template()` exists once and that it is // the proper type (list). let mut template_args = None; for attr in &ast.attrs { if !attr.path().is_ident("template") { continue; } match attr.parse_args_with(Punctuated::::parse_terminated) { Ok(args) if template_args.is_none() => template_args = Some(args), Ok(_) => return Err("duplicated 'template' attribute".into()), Err(e) => return Err(format!("unable to parse template arguments: {e}").into()), }; } let template_args = template_args.ok_or_else(|| CompileError::from("no attribute 'template' found"))?; let mut args = Self::default(); // Loop over the meta attributes and find everything that we // understand. Return a CompileError if something is not right. // `source` contains an enum that can represent `path` or `source`. for item in template_args { let pair = match item { syn::Meta::NameValue(pair) => pair, _ => { return Err(format!( "unsupported attribute argument {:?}", item.to_token_stream() ) .into()) } }; let ident = match pair.path.get_ident() { Some(ident) => ident, None => unreachable!("not possible in syn::Meta::NameValue(…)"), }; let value = match pair.value { syn::Expr::Lit(lit) => lit, syn::Expr::Group(group) => match *group.expr { syn::Expr::Lit(lit) => lit, _ => { return Err(format!("unsupported argument value type for {ident:?}").into()) } }, _ => return Err(format!("unsupported argument value type for {ident:?}").into()), }; if ident == "path" { if let syn::Lit::Str(s) = value.lit { if args.source.is_some() { return Err("must specify 'source' or 'path', not both".into()); } args.source = Some(Source::Path(s.value())); } else { return Err("template path must be string literal".into()); } } else if ident == "source" { if let syn::Lit::Str(s) = value.lit { if args.source.is_some() { return Err("must specify 'source' or 'path', not both".into()); } args.source = Some(Source::Source(s.value())); } else { return Err("template source must be string literal".into()); } } else if ident == "print" { if let syn::Lit::Str(s) = value.lit { args.print = s.value().parse()?; } else { return Err("print value must be string literal".into()); } } else if ident == "escape" { if let syn::Lit::Str(s) = value.lit { args.escaping = Some(s.value()); } else { return Err("escape value must be string literal".into()); } } else if ident == "ext" { if let syn::Lit::Str(s) = value.lit { args.ext = Some(s.value()); } else { return Err("ext value must be string literal".into()); } } else if ident == "syntax" { if let syn::Lit::Str(s) = value.lit { args.syntax = Some(s.value()) } else { return Err("syntax value must be string literal".into()); } } else if ident == "config" { if let syn::Lit::Str(s) = value.lit { args.config_path = Some(s.value()) } else { return Err("config value must be string literal".into()); } } else if ident == "whitespace" { if let syn::Lit::Str(s) = value.lit { args.whitespace = Some(s.value()) } else { return Err("whitespace value must be string literal".into()); } } else { return Err(format!("unsupported attribute key {ident:?} found").into()); } } Ok(args) } } fn find_used_templates( input: &TemplateInput<'_>, map: &mut HashMap, source: String, ) -> Result<(), CompileError> { let mut dependency_graph = Vec::new(); let mut check = vec![(input.path.clone(), source)]; while let Some((path, source)) = check.pop() { for n in parse(&source, input.syntax)? { match n { Node::Extends(extends) => { let extends = input.config.find_template(extends, Some(&path))?; let dependency_path = (path.clone(), extends.clone()); if dependency_graph.contains(&dependency_path) { return Err(format!( "cyclic dependency in graph {:#?}", dependency_graph .iter() .map(|e| format!("{:#?} --> {:#?}", e.0, e.1)) .collect::>() ) .into()); } dependency_graph.push(dependency_path); let source = get_template_source(&extends)?; check.push((extends, source)); } Node::Import(_, import, _) => { let import = input.config.find_template(import, Some(&path))?; let source = get_template_source(&import)?; check.push((import, source)); } _ => {} } } map.insert(path, source); } Ok(()) } struct Generator<'a> { // The template input state: original struct AST and attributes input: &'a TemplateInput<'a>, // All contexts, keyed by the package-relative template path contexts: &'a HashMap<&'a Path, Context<'a>>, // The heritage contains references to blocks and their ancestry heritage: Option<&'a Heritage<'a>>, // Variables accessible directly from the current scope (not redirected to context) locals: MapChain<'a, &'a str, LocalMeta>, // Suffix whitespace from the previous literal. Will be flushed to the // output buffer unless suppressed by whitespace suppression on the next // non-literal. next_ws: Option<&'a str>, // Whitespace suppression from the previous non-literal. Will be used to // determine whether to flush prefix whitespace from the next literal. skip_ws: WhitespaceHandling, // If currently in a block, this will contain the name of a potential parent block super_block: Option<(&'a str, usize)>, // buffer for writable buf_writable: Vec>, // Counter for write! hash named arguments named: usize, // If set to `suppress`, the whitespace characters will be removed by default unless `+` is // used. whitespace: WhitespaceHandling, } impl<'a> Generator<'a> { fn new<'n>( input: &'n TemplateInput<'_>, contexts: &'n HashMap<&'n Path, Context<'n>>, heritage: Option<&'n Heritage<'_>>, locals: MapChain<'n, &'n str, LocalMeta>, whitespace: WhitespaceHandling, ) -> Generator<'n> { Generator { input, contexts, heritage, locals, next_ws: None, skip_ws: WhitespaceHandling::Preserve, super_block: None, buf_writable: vec![], named: 0, whitespace, } } fn child(&mut self) -> Generator<'_> { let locals = MapChain::with_parent(&self.locals); Self::new( self.input, self.contexts, self.heritage, locals, self.whitespace, ) } // Takes a Context and generates the relevant implementations. fn build(mut self, ctx: &'a Context<'_>) -> Result { let mut buf = Buffer::new(0); self.impl_template(ctx, &mut buf)?; self.impl_display(&mut buf)?; #[cfg(feature = "with-actix-web")] self.impl_actix_web_responder(&mut buf)?; #[cfg(feature = "with-axum")] self.impl_axum_into_response(&mut buf)?; #[cfg(feature = "with-gotham")] self.impl_gotham_into_response(&mut buf)?; #[cfg(feature = "with-hyper")] self.impl_hyper_into_response(&mut buf)?; #[cfg(feature = "with-mendes")] self.impl_mendes_responder(&mut buf)?; #[cfg(feature = "with-rocket")] self.impl_rocket_responder(&mut buf)?; #[cfg(feature = "with-tide")] self.impl_tide_integrations(&mut buf)?; #[cfg(feature = "with-warp")] self.impl_warp_reply(&mut buf)?; Ok(buf.buf) } // Implement `Template` for the given context struct. fn impl_template( &mut self, ctx: &'a Context<'_>, buf: &mut Buffer, ) -> Result<(), CompileError> { self.write_header(buf, "::askama::Template", None)?; buf.writeln( "fn render_into(&self, writer: &mut (impl ::std::fmt::Write + ?Sized)) -> \ ::askama::Result<()> {", )?; // Make sure the compiler understands that the generated code depends on the template files. for path in self.contexts.keys() { // Skip the fake path of templates defined in rust source. let path_is_valid = match self.input.source { Source::Path(_) => true, Source::Source(_) => path != &self.input.path, }; if path_is_valid { let path = path.to_str().unwrap(); buf.writeln( "e! { include_bytes!(#path); } .to_string(), )?; } } let size_hint = if let Some(heritage) = self.heritage { self.handle(heritage.root, heritage.root.nodes, buf, AstLevel::Top) } else { self.handle(ctx, ctx.nodes, buf, AstLevel::Top) }?; self.flush_ws(Ws(None, None)); buf.writeln("::askama::Result::Ok(())")?; buf.writeln("}")?; buf.writeln("const EXTENSION: ::std::option::Option<&'static ::std::primitive::str> = ")?; buf.writeln(&format!("{:?}", self.input.extension()))?; buf.writeln(";")?; buf.writeln("const SIZE_HINT: ::std::primitive::usize = ")?; buf.writeln(&format!("{size_hint}"))?; buf.writeln(";")?; buf.writeln("const MIME_TYPE: &'static ::std::primitive::str = ")?; buf.writeln(&format!("{:?}", &self.input.mime_type))?; buf.writeln(";")?; buf.writeln("}")?; Ok(()) } // Implement `Display` for the given context struct. fn impl_display(&mut self, buf: &mut Buffer) -> Result<(), CompileError> { self.write_header(buf, "::std::fmt::Display", None)?; buf.writeln("#[inline]")?; buf.writeln("fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {")?; buf.writeln("::askama::Template::render_into(self, f).map_err(|_| ::std::fmt::Error {})")?; buf.writeln("}")?; buf.writeln("}") } // Implement Actix-web's `Responder`. #[cfg(feature = "with-actix-web")] fn impl_actix_web_responder(&mut self, buf: &mut Buffer) -> Result<(), CompileError> { self.write_header(buf, "::askama_actix::actix_web::Responder", None)?; buf.writeln("type Body = ::askama_actix::actix_web::body::BoxBody;")?; buf.writeln("#[inline]")?; buf.writeln( "fn respond_to(self, _req: &::askama_actix::actix_web::HttpRequest) \ -> ::askama_actix::actix_web::HttpResponse {", )?; buf.writeln("::to_response(&self)")?; buf.writeln("}")?; buf.writeln("}") } // Implement Axum's `IntoResponse`. #[cfg(feature = "with-axum")] fn impl_axum_into_response(&mut self, buf: &mut Buffer) -> Result<(), CompileError> { self.write_header(buf, "::askama_axum::IntoResponse", None)?; buf.writeln("#[inline]")?; buf.writeln( "fn into_response(self)\ -> ::askama_axum::Response {", )?; buf.writeln("::askama_axum::into_response(&self)")?; buf.writeln("}")?; buf.writeln("}") } // Implement gotham's `IntoResponse`. #[cfg(feature = "with-gotham")] fn impl_gotham_into_response(&mut self, buf: &mut Buffer) -> Result<(), CompileError> { self.write_header(buf, "::askama_gotham::IntoResponse", None)?; buf.writeln("#[inline]")?; buf.writeln( "fn into_response(self, _state: &::askama_gotham::State)\ -> ::askama_gotham::Response<::askama_gotham::Body> {", )?; buf.writeln("::askama_gotham::respond(&self)")?; buf.writeln("}")?; buf.writeln("}") } // Implement `From