From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- third_party/rust/askama_derive/src/config.rs | 582 ++++++ third_party/rust/askama_derive/src/generator.rs | 2171 ++++++++++++++++++++ third_party/rust/askama_derive/src/heritage.rs | 126 ++ third_party/rust/askama_derive/src/input.rs | 231 +++ third_party/rust/askama_derive/src/lib.rs | 100 + third_party/rust/askama_derive/src/parser/expr.rs | 346 ++++ third_party/rust/askama_derive/src/parser/mod.rs | 317 +++ third_party/rust/askama_derive/src/parser/node.rs | 682 ++++++ third_party/rust/askama_derive/src/parser/tests.rs | 668 ++++++ 9 files changed, 5223 insertions(+) create mode 100644 third_party/rust/askama_derive/src/config.rs create mode 100644 third_party/rust/askama_derive/src/generator.rs create mode 100644 third_party/rust/askama_derive/src/heritage.rs create mode 100644 third_party/rust/askama_derive/src/input.rs create mode 100644 third_party/rust/askama_derive/src/lib.rs create mode 100644 third_party/rust/askama_derive/src/parser/expr.rs create mode 100644 third_party/rust/askama_derive/src/parser/mod.rs create mode 100644 third_party/rust/askama_derive/src/parser/node.rs create mode 100644 third_party/rust/askama_derive/src/parser/tests.rs (limited to 'third_party/rust/askama_derive/src') diff --git a/third_party/rust/askama_derive/src/config.rs b/third_party/rust/askama_derive/src/config.rs new file mode 100644 index 0000000000..cf22a720f0 --- /dev/null +++ b/third_party/rust/askama_derive/src/config.rs @@ -0,0 +1,582 @@ +use std::collections::{BTreeMap, HashSet}; +use std::convert::TryFrom; +use std::path::{Path, PathBuf}; +use std::{env, fs}; + +#[cfg(feature = "serde")] +use serde::Deserialize; + +use crate::CompileError; + +#[derive(Debug)] +pub(crate) struct Config<'a> { + pub(crate) dirs: Vec, + pub(crate) syntaxes: BTreeMap>, + pub(crate) default_syntax: &'a str, + pub(crate) escapers: Vec<(HashSet, String)>, + pub(crate) whitespace: WhitespaceHandling, +} + +impl<'a> Config<'a> { + pub(crate) fn new( + s: &'a str, + template_whitespace: Option<&String>, + ) -> std::result::Result, CompileError> { + let root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let default_dirs = vec![root.join("templates")]; + + let mut syntaxes = BTreeMap::new(); + syntaxes.insert(DEFAULT_SYNTAX_NAME.to_string(), Syntax::default()); + + let raw = if s.is_empty() { + RawConfig::default() + } else { + RawConfig::from_toml_str(s)? + }; + + let (dirs, default_syntax, mut whitespace) = match raw.general { + Some(General { + dirs, + default_syntax, + whitespace, + }) => ( + dirs.map_or(default_dirs, |v| { + v.into_iter().map(|dir| root.join(dir)).collect() + }), + default_syntax.unwrap_or(DEFAULT_SYNTAX_NAME), + whitespace, + ), + None => ( + default_dirs, + DEFAULT_SYNTAX_NAME, + WhitespaceHandling::default(), + ), + }; + if let Some(template_whitespace) = template_whitespace { + whitespace = match template_whitespace.as_str() { + "suppress" => WhitespaceHandling::Suppress, + "minimize" => WhitespaceHandling::Minimize, + "preserve" => WhitespaceHandling::Preserve, + s => return Err(format!("invalid value for `whitespace`: \"{s}\"").into()), + }; + } + + if let Some(raw_syntaxes) = raw.syntax { + for raw_s in raw_syntaxes { + let name = raw_s.name; + + if syntaxes + .insert(name.to_string(), Syntax::try_from(raw_s)?) + .is_some() + { + return Err(format!("syntax \"{name}\" is already defined").into()); + } + } + } + + if !syntaxes.contains_key(default_syntax) { + return Err(format!("default syntax \"{default_syntax}\" not found").into()); + } + + let mut escapers = Vec::new(); + if let Some(configured) = raw.escaper { + for escaper in configured { + escapers.push(( + escaper + .extensions + .iter() + .map(|ext| (*ext).to_string()) + .collect(), + escaper.path.to_string(), + )); + } + } + for (extensions, path) in DEFAULT_ESCAPERS { + escapers.push((str_set(extensions), (*path).to_string())); + } + + Ok(Config { + dirs, + syntaxes, + default_syntax, + escapers, + whitespace, + }) + } + + pub(crate) fn find_template( + &self, + path: &str, + start_at: Option<&Path>, + ) -> std::result::Result { + if let Some(root) = start_at { + let relative = root.with_file_name(path); + if relative.exists() { + return Ok(relative); + } + } + + for dir in &self.dirs { + let rooted = dir.join(path); + if rooted.exists() { + return Ok(rooted); + } + } + + Err(format!( + "template {:?} not found in directories {:?}", + path, self.dirs + ) + .into()) + } +} + +#[derive(Debug)] +pub(crate) struct Syntax<'a> { + pub(crate) block_start: &'a str, + pub(crate) block_end: &'a str, + pub(crate) expr_start: &'a str, + pub(crate) expr_end: &'a str, + pub(crate) comment_start: &'a str, + pub(crate) comment_end: &'a str, +} + +impl Default for Syntax<'static> { + fn default() -> Self { + Self { + block_start: "{%", + block_end: "%}", + expr_start: "{{", + expr_end: "}}", + comment_start: "{#", + comment_end: "#}", + } + } +} + +impl<'a> TryFrom> for Syntax<'a> { + type Error = CompileError; + + fn try_from(raw: RawSyntax<'a>) -> std::result::Result { + let default = Syntax::default(); + let syntax = Self { + block_start: raw.block_start.unwrap_or(default.block_start), + block_end: raw.block_end.unwrap_or(default.block_end), + expr_start: raw.expr_start.unwrap_or(default.expr_start), + expr_end: raw.expr_end.unwrap_or(default.expr_end), + comment_start: raw.comment_start.unwrap_or(default.comment_start), + comment_end: raw.comment_end.unwrap_or(default.comment_end), + }; + + if syntax.block_start.len() != 2 + || syntax.block_end.len() != 2 + || syntax.expr_start.len() != 2 + || syntax.expr_end.len() != 2 + || syntax.comment_start.len() != 2 + || syntax.comment_end.len() != 2 + { + return Err("length of delimiters must be two".into()); + } + + let bs = syntax.block_start.as_bytes()[0]; + let be = syntax.block_start.as_bytes()[1]; + let cs = syntax.comment_start.as_bytes()[0]; + let ce = syntax.comment_start.as_bytes()[1]; + let es = syntax.expr_start.as_bytes()[0]; + let ee = syntax.expr_start.as_bytes()[1]; + if !((bs == cs && bs == es) || (be == ce && be == ee)) { + return Err(format!("bad delimiters block_start: {}, comment_start: {}, expr_start: {}, needs one of the two characters in common", syntax.block_start, syntax.comment_start, syntax.expr_start).into()); + } + + Ok(syntax) + } +} + +#[cfg_attr(feature = "serde", derive(Deserialize))] +#[derive(Default)] +struct RawConfig<'a> { + #[cfg_attr(feature = "serde", serde(borrow))] + general: Option>, + syntax: Option>>, + escaper: Option>>, +} + +impl RawConfig<'_> { + #[cfg(feature = "config")] + fn from_toml_str(s: &str) -> std::result::Result, CompileError> { + basic_toml::from_str(s) + .map_err(|e| format!("invalid TOML in {CONFIG_FILE_NAME}: {e}").into()) + } + + #[cfg(not(feature = "config"))] + fn from_toml_str(_: &str) -> std::result::Result, CompileError> { + Err("TOML support not available".into()) + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "serde", derive(Deserialize))] +#[cfg_attr(feature = "serde", serde(field_identifier, rename_all = "lowercase"))] +pub(crate) enum WhitespaceHandling { + /// The default behaviour. It will leave the whitespace characters "as is". + Preserve, + /// It'll remove all the whitespace characters before and after the jinja block. + Suppress, + /// It'll remove all the whitespace characters except one before and after the jinja blocks. + /// If there is a newline character, the preserved character in the trimmed characters, it will + /// the one preserved. + Minimize, +} + +impl Default for WhitespaceHandling { + fn default() -> Self { + WhitespaceHandling::Preserve + } +} + +#[cfg_attr(feature = "serde", derive(Deserialize))] +struct General<'a> { + #[cfg_attr(feature = "serde", serde(borrow))] + dirs: Option>, + default_syntax: Option<&'a str>, + #[cfg_attr(feature = "serde", serde(default))] + whitespace: WhitespaceHandling, +} + +#[cfg_attr(feature = "serde", derive(Deserialize))] +struct RawSyntax<'a> { + name: &'a str, + block_start: Option<&'a str>, + block_end: Option<&'a str>, + expr_start: Option<&'a str>, + expr_end: Option<&'a str>, + comment_start: Option<&'a str>, + comment_end: Option<&'a str>, +} + +#[cfg_attr(feature = "serde", derive(Deserialize))] +struct RawEscaper<'a> { + path: &'a str, + extensions: Vec<&'a str>, +} + +pub(crate) fn read_config_file( + config_path: Option<&str>, +) -> std::result::Result { + let root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let filename = match config_path { + Some(config_path) => root.join(config_path), + None => root.join(CONFIG_FILE_NAME), + }; + + if filename.exists() { + fs::read_to_string(&filename) + .map_err(|_| format!("unable to read {:?}", filename.to_str().unwrap()).into()) + } else if config_path.is_some() { + Err(format!("`{}` does not exist", root.display()).into()) + } else { + Ok("".to_string()) + } +} + +fn str_set(vals: &[T]) -> HashSet +where + T: ToString, +{ + vals.iter().map(|s| s.to_string()).collect() +} + +#[allow(clippy::match_wild_err_arm)] +pub(crate) fn get_template_source(tpl_path: &Path) -> std::result::Result { + match fs::read_to_string(tpl_path) { + Err(_) => Err(format!( + "unable to open template file '{}'", + tpl_path.to_str().unwrap() + ) + .into()), + Ok(mut source) => { + if source.ends_with('\n') { + let _ = source.pop(); + } + Ok(source) + } + } +} + +static CONFIG_FILE_NAME: &str = "askama.toml"; +static DEFAULT_SYNTAX_NAME: &str = "default"; +static DEFAULT_ESCAPERS: &[(&[&str], &str)] = &[ + (&["html", "htm", "xml"], "::askama::Html"), + (&["md", "none", "txt", "yml", ""], "::askama::Text"), + (&["j2", "jinja", "jinja2"], "::askama::Html"), +]; + +#[cfg(test)] +mod tests { + use std::env; + use std::path::{Path, PathBuf}; + + use super::*; + + #[test] + fn get_source() { + let path = Config::new("", None) + .and_then(|config| config.find_template("b.html", None)) + .unwrap(); + assert_eq!(get_template_source(&path).unwrap(), "bar"); + } + + #[test] + fn test_default_config() { + let mut root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + root.push("templates"); + let config = Config::new("", None).unwrap(); + assert_eq!(config.dirs, vec![root]); + } + + #[cfg(feature = "config")] + #[test] + fn test_config_dirs() { + let mut root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + root.push("tpl"); + let config = Config::new("[general]\ndirs = [\"tpl\"]", None).unwrap(); + assert_eq!(config.dirs, vec![root]); + } + + fn assert_eq_rooted(actual: &Path, expected: &str) { + let mut root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + root.push("templates"); + let mut inner = PathBuf::new(); + inner.push(expected); + assert_eq!(actual.strip_prefix(root).unwrap(), inner); + } + + #[test] + fn find_absolute() { + let config = Config::new("", None).unwrap(); + let root = config.find_template("a.html", None).unwrap(); + let path = config.find_template("sub/b.html", Some(&root)).unwrap(); + assert_eq_rooted(&path, "sub/b.html"); + } + + #[test] + #[should_panic] + fn find_relative_nonexistent() { + let config = Config::new("", None).unwrap(); + let root = config.find_template("a.html", None).unwrap(); + config.find_template("c.html", Some(&root)).unwrap(); + } + + #[test] + fn find_relative() { + let config = Config::new("", None).unwrap(); + let root = config.find_template("sub/b.html", None).unwrap(); + let path = config.find_template("c.html", Some(&root)).unwrap(); + assert_eq_rooted(&path, "sub/c.html"); + } + + #[test] + fn find_relative_sub() { + let config = Config::new("", None).unwrap(); + let root = config.find_template("sub/b.html", None).unwrap(); + let path = config.find_template("sub1/d.html", Some(&root)).unwrap(); + assert_eq_rooted(&path, "sub/sub1/d.html"); + } + + #[cfg(feature = "config")] + #[test] + fn add_syntax() { + let raw_config = r#" + [general] + default_syntax = "foo" + + [[syntax]] + name = "foo" + block_start = "{<" + + [[syntax]] + name = "bar" + expr_start = "{!" + "#; + + let default_syntax = Syntax::default(); + let config = Config::new(raw_config, None).unwrap(); + assert_eq!(config.default_syntax, "foo"); + + let foo = config.syntaxes.get("foo").unwrap(); + assert_eq!(foo.block_start, "{<"); + assert_eq!(foo.block_end, default_syntax.block_end); + assert_eq!(foo.expr_start, default_syntax.expr_start); + assert_eq!(foo.expr_end, default_syntax.expr_end); + assert_eq!(foo.comment_start, default_syntax.comment_start); + assert_eq!(foo.comment_end, default_syntax.comment_end); + + let bar = config.syntaxes.get("bar").unwrap(); + assert_eq!(bar.block_start, default_syntax.block_start); + assert_eq!(bar.block_end, default_syntax.block_end); + assert_eq!(bar.expr_start, "{!"); + assert_eq!(bar.expr_end, default_syntax.expr_end); + assert_eq!(bar.comment_start, default_syntax.comment_start); + assert_eq!(bar.comment_end, default_syntax.comment_end); + } + + #[cfg(feature = "config")] + #[test] + fn add_syntax_two() { + let raw_config = r#" + syntax = [{ name = "foo", block_start = "{<" }, + { name = "bar", expr_start = "{!" } ] + + [general] + default_syntax = "foo" + "#; + + let default_syntax = Syntax::default(); + let config = Config::new(raw_config, None).unwrap(); + assert_eq!(config.default_syntax, "foo"); + + let foo = config.syntaxes.get("foo").unwrap(); + assert_eq!(foo.block_start, "{<"); + assert_eq!(foo.block_end, default_syntax.block_end); + assert_eq!(foo.expr_start, default_syntax.expr_start); + assert_eq!(foo.expr_end, default_syntax.expr_end); + assert_eq!(foo.comment_start, default_syntax.comment_start); + assert_eq!(foo.comment_end, default_syntax.comment_end); + + let bar = config.syntaxes.get("bar").unwrap(); + assert_eq!(bar.block_start, default_syntax.block_start); + assert_eq!(bar.block_end, default_syntax.block_end); + assert_eq!(bar.expr_start, "{!"); + assert_eq!(bar.expr_end, default_syntax.expr_end); + assert_eq!(bar.comment_start, default_syntax.comment_start); + assert_eq!(bar.comment_end, default_syntax.comment_end); + } + + #[cfg(feature = "toml")] + #[should_panic] + #[test] + fn use_default_at_syntax_name() { + let raw_config = r#" + syntax = [{ name = "default" }] + "#; + + let _config = Config::new(raw_config, None).unwrap(); + } + + #[cfg(feature = "toml")] + #[should_panic] + #[test] + fn duplicated_syntax_name_on_list() { + let raw_config = r#" + syntax = [{ name = "foo", block_start = "~<" }, + { name = "foo", block_start = "%%" } ] + "#; + + let _config = Config::new(raw_config, None).unwrap(); + } + + #[cfg(feature = "toml")] + #[should_panic] + #[test] + fn is_not_exist_default_syntax() { + let raw_config = r#" + [general] + default_syntax = "foo" + "#; + + let _config = Config::new(raw_config, None).unwrap(); + } + + #[cfg(feature = "config")] + #[test] + fn escape_modes() { + let config = Config::new( + r#" + [[escaper]] + path = "::askama::Js" + extensions = ["js"] + "#, + None, + ) + .unwrap(); + assert_eq!( + config.escapers, + vec![ + (str_set(&["js"]), "::askama::Js".into()), + (str_set(&["html", "htm", "xml"]), "::askama::Html".into()), + ( + str_set(&["md", "none", "txt", "yml", ""]), + "::askama::Text".into() + ), + (str_set(&["j2", "jinja", "jinja2"]), "::askama::Html".into()), + ] + ); + } + + #[cfg(feature = "config")] + #[test] + fn test_whitespace_parsing() { + let config = Config::new( + r#" + [general] + whitespace = "suppress" + "#, + None, + ) + .unwrap(); + assert_eq!(config.whitespace, WhitespaceHandling::Suppress); + + let config = Config::new(r#""#, None).unwrap(); + assert_eq!(config.whitespace, WhitespaceHandling::Preserve); + + let config = Config::new( + r#" + [general] + whitespace = "preserve" + "#, + None, + ) + .unwrap(); + assert_eq!(config.whitespace, WhitespaceHandling::Preserve); + + let config = Config::new( + r#" + [general] + whitespace = "minimize" + "#, + None, + ) + .unwrap(); + assert_eq!(config.whitespace, WhitespaceHandling::Minimize); + } + + #[cfg(feature = "toml")] + #[test] + fn test_whitespace_in_template() { + // Checking that template arguments have precedence over general configuration. + // So in here, in the template arguments, there is `whitespace = "minimize"` so + // the `WhitespaceHandling` should be `Minimize` as well. + let config = Config::new( + r#" + [general] + whitespace = "suppress" + "#, + Some(&"minimize".to_owned()), + ) + .unwrap(); + assert_eq!(config.whitespace, WhitespaceHandling::Minimize); + + let config = Config::new(r#""#, Some(&"minimize".to_owned())).unwrap(); + assert_eq!(config.whitespace, WhitespaceHandling::Minimize); + } + + #[test] + fn test_config_whitespace_error() { + let config = Config::new(r#""#, Some(&"trim".to_owned())); + if let Err(err) = config { + assert_eq!(err.msg, "invalid value for `whitespace`: \"trim\""); + } else { + panic!("Config::new should have return an error"); + } + } +} diff --git a/third_party/rust/askama_derive/src/generator.rs b/third_party/rust/askama_derive/src/generator.rs new file mode 100644 index 0000000000..05b3fc3245 --- /dev/null +++ b/third_party/rust/askama_derive/src/generator.rs @@ -0,0 +1,2171 @@ +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