use crate::config::{get_template_source, read_config_file, Config, WhitespaceHandling}; use crate::heritage::{Context, Heritage}; use crate::input::{Print, Source, TemplateArgs, TemplateInput}; use crate::CompileError; use parser::node::{ Call, Comment, CondTest, If, Include, Let, Lit, Loop, Match, Target, Whitespace, Ws, }; use parser::{Expr, Node, Parsed}; use quote::quote; use std::collections::hash_map::{Entry, HashMap}; use std::path::{Path, PathBuf}; use std::{cmp, hash, mem, str}; /// 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. pub(crate) 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 templates = HashMap::new(); find_used_templates(&input, &mut templates, source)?; let mut contexts = HashMap::new(); for (path, parsed) in &templates { contexts.insert( path.as_path(), Context::new(input.config, path, parsed.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!("{:?}", templates[input.path.as_path()].nodes()); } 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) } 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() { let parsed = Parsed::new(source, input.syntax)?; for n in parsed.nodes() { match n { Node::Extends(extends) => { let extends = input.config.find_template(extends.path, 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.path, Some(&path))?; let source = get_template_source(&import)?; check.push((import, source)); } _ => {} } } map.insert(path, parsed); } 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>>, // Cache ASTs for included templates includes: HashMap, // 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, includes: HashMap::default(), locals, next_ws: None, skip_ws: WhitespaceHandling::Preserve, super_block: None, buf_writable: vec![], named: 0, 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