diff --git a/askama_derive/src/lib.rs b/askama_derive/src/lib.rs index cc50ac861..38eba1a87 100644 --- a/askama_derive/src/lib.rs +++ b/askama_derive/src/lib.rs @@ -6,7 +6,7 @@ use askama_shared::heritage::{Context, Heritage}; use askama_shared::input::{Print, Source, TemplateInput}; use askama_shared::parser::{parse, Expr, Node}; use askama_shared::{ - generator, get_template_source, read_config_file, CompileError, Config, Integrations, + generator, get_template_source, read_config_file, CompileError, Config, Integrations, Syntax, }; use proc_macro::TokenStream; use proc_macro2::Span; @@ -14,7 +14,7 @@ use proc_macro2::Span; use std::collections::HashMap; use std::path::PathBuf; -#[proc_macro_derive(Template, attributes(template))] +#[proc_macro_derive(Template, attributes(template, multi_template))] pub fn derive_template(input: TokenStream) -> TokenStream { let ast: syn::DeriveInput = syn::parse(input).unwrap(); match build_template(&ast) { @@ -42,7 +42,29 @@ fn build_template(ast: &syn::DeriveInput) -> Result { }; let mut sources = HashMap::new(); - find_used_templates(&input, &mut sources, source)?; + find_used_templates( + &config, + input.syntax, + &mut sources, + input.path.clone(), + source, + )?; + + if let Some((_, multi)) = &input.multi { + for alternative in multi { + let source: String = match alternative.source { + Source::Source(ref s) => s.clone(), + Source::Path(_) => get_template_source(&alternative.path)?, + }; + find_used_templates( + &config, + alternative.syntax, + &mut sources, + alternative.path.clone(), + source, + )?; + } + } let mut parsed = HashMap::new(); for (path, src) in &sources { @@ -54,18 +76,21 @@ fn build_template(ast: &syn::DeriveInput) -> Result { 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 - }; + let heritages = contexts + .iter() + .filter_map( + |(&path, ctx)| match !ctx.blocks.is_empty() || ctx.extends.is_some() { + true => Some((path, Heritage::new(ctx, &contexts))), + false => None, + }, + ) + .collect(); if input.print == Print::Ast || input.print == Print::All { eprintln!("{:?}", parsed[input.path.as_path()]); } - let code = generator::generate(&input, &contexts, heritage.as_ref(), INTEGRATIONS)?; + let code = generator::generate(&input, &contexts, &heritages, INTEGRATIONS)?; if input.print == Print::Code || input.print == Print::All { eprintln!("{}", code); } @@ -73,17 +98,22 @@ fn build_template(ast: &syn::DeriveInput) -> Result { } fn find_used_templates( - input: &TemplateInput<'_>, + config: &Config<'_>, + syntax: &Syntax<'_>, map: &mut HashMap, + path: PathBuf, source: String, ) -> Result<(), CompileError> { let mut dependency_graph = Vec::new(); - let mut check = vec![(input.path.clone(), source)]; + let mut check = vec![(path, source)]; while let Some((path, source)) = check.pop() { - for n in parse(&source, input.syntax)? { + if map.contains_key(&path) { + continue; + } + for n in parse(&source, syntax)? { match n { Node::Extends(Expr::StrLit(extends)) => { - let extends = input.config.find_template(extends, Some(&path))?; + let extends = config.find_template(extends, Some(&path))?; let dependency_path = (path.clone(), extends.clone()); if dependency_graph.contains(&dependency_path) { return Err(CompileError::String(format!( @@ -99,7 +129,7 @@ fn find_used_templates( check.push((extends, source)); } Node::Import(_, import, _) => { - let import = input.config.find_template(import, Some(&path))?; + let import = config.find_template(import, Some(&path))?; let source = get_template_source(&import)?; check.push((import, source)); } diff --git a/askama_shared/src/generator.rs b/askama_shared/src/generator.rs index 53729c694..5e6d8ed74 100644 --- a/askama_shared/src/generator.rs +++ b/askama_shared/src/generator.rs @@ -1,34 +1,40 @@ use super::{get_template_source, CompileError, Integrations}; -use crate::filters; use crate::heritage::{Context, Heritage}; -use crate::input::{Source, TemplateInput}; -use crate::parser::{parse, Cond, CondTest, Expr, Loop, Node, Target, When, Ws}; +use crate::input::{MultiTemplateInput, Source, TemplateInput}; +use crate::parser::{ + one_expr, one_target, parse, Cond, CondTest, Expr, Loop, Node, Target, When, Ws, +}; +use crate::{filters, Config}; use proc_macro2::Span; - -use quote::{quote, ToTokens}; +use quote::quote; use std::collections::HashMap; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::{cmp, hash, mem, str}; pub fn generate( input: &TemplateInput<'_>, contexts: &HashMap<&Path, Context<'_>, S>, - heritage: Option<&Heritage<'_>>, + heritages: &HashMap<&Path, Heritage<'_>, S>, integrations: Integrations, ) -> Result { - Generator::new(input, contexts, heritage, integrations, MapChain::new()) - .build(&contexts[input.path.as_path()]) + Generator::new(input, contexts, heritages, integrations, MapChain::new()).build() } struct Generator<'a, S: std::hash::BuildHasher> { // The template input state: original struct AST and attributes - input: &'a TemplateInput<'a>, + ast: &'a syn::DeriveInput, + current: MultiTemplateInput<'a>, + multi: Option<(&'a str, &'a [MultiTemplateInput<'a>])>, + extension: Option<&'a str>, + mime_type: &'a str, + config: &'a Config<'a>, // All contexts, keyed by the package-relative template path contexts: &'a HashMap<&'a Path, Context<'a>, S>, // The heritage contains references to blocks and their ancestry heritage: Option<&'a Heritage<'a>>, + heritages: &'a HashMap<&'a Path, Heritage<'a>, S>, // What integrations need to be generated integrations: Integrations, // Variables accessible directly from the current scope (not redirected to context) @@ -51,15 +57,27 @@ struct Generator<'a, S: std::hash::BuildHasher> { impl<'a, S: std::hash::BuildHasher> Generator<'a, S> { fn new<'n>( input: &'n TemplateInput<'_>, - contexts: &'n HashMap<&'n Path, Context<'n>, S>, - heritage: Option<&'n Heritage<'_>>, + contexts: &'n HashMap<&Path, Context<'_>, S>, + heritages: &'n HashMap<&Path, Heritage<'_>, S>, integrations: Integrations, locals: MapChain<'n, &'n str, LocalMeta>, ) -> Generator<'n, S> { Generator { - input, + ast: input.ast, + current: MultiTemplateInput { + syntax: input.syntax, + pattern: "".to_owned(), + path: input.path.clone(), + source: input.source.clone(), + escaper: input.escaper, + }, + multi: input.multi.as_ref().map(|(a, b)| (a.as_ref(), b.as_ref())), + extension: input.extension(), + mime_type: &input.mime_type, + config: input.config, contexts, - heritage, + heritage: None, + heritages, integrations, locals, next_ws: None, @@ -71,26 +89,31 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> { } fn child(&mut self) -> Generator<'_, S> { - let locals = MapChain::with_parent(&self.locals); - Self::new( - self.input, - self.contexts, - self.heritage, - self.integrations, - locals, - ) + Generator { + ast: self.ast, + current: self.current.clone(), + multi: self.multi, + extension: self.extension, + mime_type: self.mime_type, + config: self.config, + contexts: self.contexts, + heritage: None, + heritages: self.heritages, + integrations: self.integrations, + locals: MapChain::with_parent(&self.locals), + next_ws: None, + skip_ws: false, + super_block: None, + buf_writable: vec![], + named: 0, + } } // Takes a Context and generates the relevant implementations. - fn build(mut self, ctx: &'a Context<'_>) -> Result { + fn build(mut self) -> Result { let mut buf = Buffer::new(0); - if !ctx.blocks.is_empty() { - if let Some(parent) = self.input.parent { - self.deref_to_parent(&mut buf, parent)?; - } - }; - self.impl_template(ctx, &mut buf)?; + self.impl_template(&mut buf)?; self.impl_display(&mut buf)?; if self.integrations.actix { @@ -117,12 +140,28 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> { Ok(buf.buf) } - // Implement `Template` for the given context struct. - fn impl_template( + fn impl_single_template( &mut self, - ctx: &'a Context<'_>, buf: &mut Buffer, - ) -> Result<(), CompileError> { + path: PathBuf, + ) -> Result { + match self.heritages.get(&*path) { + Some(heritage) => { + self.heritage = Some(heritage); + let size_hint = + self.handle(heritage.root, heritage.root.nodes, buf, AstLevel::Top)?; + self.heritage = None; + Ok(size_hint) + } + None => { + let ctx = &self.contexts[&*path]; + self.handle(ctx, ctx.nodes, buf, AstLevel::Top) + } + } + } + + // Implement `Template` for the given context struct. + fn impl_template(&mut self, 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)) -> \ @@ -132,9 +171,9 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> { // 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 { + let path_is_valid = match self.current.source { Source::Path(_) => true, - Source::Source(_) => path != &self.input.path, + Source::Source(_) => path != &self.current.path, }; if path_is_valid { let path = path.to_str().unwrap(); @@ -147,50 +186,58 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> { } } - 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) - }?; + let mut size_hints = vec![]; + if let Some((localizer, alternatives)) = self.multi { + let old_current = self.current.clone(); + + let localizer = one_expr(localizer).unwrap(); + let expr_code = self.visit_expr_root(&localizer)?; + buf.writeln(&format!("match &{} {{", expr_code))?; + + for alternative in alternatives { + self.current = alternative.clone(); + + self.locals.push(); + let pattern = one_target(&alternative.pattern).unwrap(); + self.visit_target(buf, true, true, &pattern); + buf.writeln(" => {")?; + size_hints.push(self.impl_single_template(buf, alternative.path.clone())?); + buf.writeln("}")?; + self.locals.pop(); + } + buf.writeln("_ => {")?; + + self.current = old_current; + } + + size_hints.push(self.impl_single_template(buf, self.current.path.clone())?); + + if self.multi.is_some() { + buf.writeln("}")?; + buf.writeln("}")?; + } self.flush_ws(Ws(false, false)); 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(&format!("{:?}", self.extension))?; buf.writeln(";")?; + let sum: u128 = size_hints.iter().map(|i| *i as u128).sum(); buf.writeln("const SIZE_HINT: ::std::primitive::usize = ")?; - buf.writeln(&format!("{}", size_hint))?; + buf.writeln(&format!("{}", sum / size_hints.len() as u128))?; buf.writeln(";")?; buf.writeln("const MIME_TYPE: &'static ::std::primitive::str = ")?; - buf.writeln(&format!("{:?}", &self.input.mime_type))?; + buf.writeln(&format!("{:?}", self.mime_type))?; buf.writeln(";")?; buf.writeln("}")?; Ok(()) } - // Implement `Deref` for an inheriting context struct. - fn deref_to_parent( - &mut self, - buf: &mut Buffer, - parent_type: &syn::Type, - ) -> Result<(), CompileError> { - self.write_header(buf, "::std::ops::Deref", None)?; - buf.writeln(&format!( - "type Target = {};", - parent_type.into_token_stream() - ))?; - buf.writeln("#[inline]")?; - buf.writeln("fn deref(&self) -> &Self::Target {")?; - buf.writeln("&self._parent")?; - buf.writeln("}")?; - buf.writeln("}") - } - // 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)?; @@ -223,7 +270,7 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> { "fn into_response(self)\ -> ::askama_axum::Response<::askama_axum::BoxBody> {", )?; - let ext = self.input.extension().unwrap_or("txt"); + let ext = self.extension.unwrap_or("txt"); buf.writeln(&format!("::askama_axum::into_response(&self, {:?})", ext))?; buf.writeln("}")?; buf.writeln("}") @@ -237,7 +284,7 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> { "fn into_response(self, _state: &::askama_gotham::State)\ -> ::askama_gotham::Response<::askama_gotham::Body> {", )?; - let ext = self.input.extension().unwrap_or("txt"); + let ext = self.extension.unwrap_or("txt"); buf.writeln(&format!("::askama_gotham::respond(&self, {:?})", ext))?; buf.writeln("}")?; buf.writeln("}") @@ -247,9 +294,9 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> { fn impl_mendes_responder(&mut self, buf: &mut Buffer) -> Result<(), CompileError> { let param = syn::parse_str("A: ::mendes::Application").unwrap(); - let mut generics = self.input.ast.generics.clone(); + let mut generics = self.ast.generics.clone(); generics.params.push(param); - let (_, orig_ty_generics, _) = self.input.ast.generics.split_for_impl(); + let (_, orig_ty_generics, _) = self.ast.generics.split_for_impl(); let (impl_generics, _, where_clause) = generics.split_for_impl(); let mut where_clause = match where_clause { @@ -272,7 +319,7 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> { "{} {} for {} {} {{", quote!(impl#impl_generics), "::mendes::application::Responder", - self.input.ast.ident, + self.ast.ident, quote!(#orig_ty_generics #where_clause), ) .as_ref(), @@ -285,7 +332,7 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> { buf.writeln(&format!( "::askama_mendes::into_response(app, req, &self, {:?})", - self.input.extension() + self.extension ))?; buf.writeln("}")?; buf.writeln("}")?; @@ -307,7 +354,7 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> { "fn respond_to(self, _: &::askama_rocket::Request) \ -> ::askama_rocket::Result<'askama> {", )?; - let ext = self.input.extension().unwrap_or("txt"); + let ext = self.extension.unwrap_or("txt"); buf.writeln(&format!("::askama_rocket::respond(&self, {:?})", ext))?; buf.writeln("}")?; @@ -316,7 +363,7 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> { } fn impl_tide_integrations(&mut self, buf: &mut Buffer) -> Result<(), CompileError> { - let ext = self.input.extension().unwrap_or("txt"); + let ext = self.extension.unwrap_or("txt"); self.write_header( buf, @@ -344,7 +391,7 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> { self.write_header(buf, "::askama_warp::warp::reply::Reply", None)?; buf.writeln("#[inline]")?; buf.writeln("fn into_response(self) -> ::askama_warp::warp::reply::Response {")?; - let ext = self.input.extension().unwrap_or("txt"); + let ext = self.extension.unwrap_or("txt"); buf.writeln(&format!("::askama_warp::reply(&self, {:?})", ext))?; buf.writeln("}")?; buf.writeln("}") @@ -358,20 +405,20 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> { target: &str, params: Option>, ) -> Result<(), CompileError> { - let mut generics = self.input.ast.generics.clone(); + let mut generics = self.ast.generics.clone(); if let Some(params) = params { for param in params { generics.params.push(param); } } - let (_, orig_ty_generics, _) = self.input.ast.generics.split_for_impl(); + let (_, orig_ty_generics, _) = self.ast.generics.split_for_impl(); let (impl_generics, _, where_clause) = generics.split_for_impl(); buf.writeln( format!( "{} {} for {}{} {{", quote!(impl#impl_generics), target, - self.input.ast.ident, + self.ast.ident, quote!(#orig_ty_generics #where_clause), ) .as_ref(), @@ -761,12 +808,9 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> { ) -> Result { self.flush_ws(ws); self.write_buf_writable(buf)?; - let path = self - .input - .config - .find_template(path, Some(&self.input.path))?; + let path = self.config.find_template(path, Some(&self.current.path))?; let src = get_template_source(&path)?; - let nodes = parse(&src, self.input.syntax)?; + let nodes = parse(&src, self.current.syntax)?; // Make sure the compiler understands that the generated code depends on the template file. { @@ -967,7 +1011,7 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> { Wrapped => expr_buf.buf, Unwrapped => format!( "::askama::MarkupDisplay::new_unsafe(&({}), {})", - expr_buf.buf, self.input.escaper + expr_buf.buf, self.current.escaper ), }; @@ -1113,7 +1157,7 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> { if FILTERS.contains(&name) { buf.write(&format!( "::askama::filters::{}({}, ", - name, self.input.escaper + name, self.current.escaper )); } else if filters::BUILT_IN_FILTERS.contains(&name) { buf.write(&format!("::askama::filters::{}(", name)); @@ -1144,13 +1188,12 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> { }; let escaper = match opt_escaper { Some(name) => self - .input .config .escapers .iter() .find_map(|(escapers, escaper)| escapers.contains(name).then(|| escaper)) .ok_or(CompileError::Static("invalid escaper for escape filter"))?, - None => self.input.escaper, + None => self.current.escaper, }; buf.write("::askama::filters::escape("); buf.write(escaper); diff --git a/askama_shared/src/input.rs b/askama_shared/src/input.rs index 1d9823138..18c406cde 100644 --- a/askama_shared/src/input.rs +++ b/askama_shared/src/input.rs @@ -1,3 +1,4 @@ +use crate::parser::{one_expr, one_target}; use crate::{CompileError, Config, Syntax}; use std::path::{Path, PathBuf}; @@ -6,6 +7,15 @@ use std::str::FromStr; use mime::Mime; use quote::ToTokens; +#[derive(Clone)] +pub struct MultiTemplateInput<'a> { + pub syntax: &'a Syntax<'a>, + pub pattern: String, + pub path: PathBuf, + pub source: Source, + pub escaper: &'a str, +} + pub struct TemplateInput<'a> { pub ast: &'a syn::DeriveInput, pub config: &'a Config<'a>, @@ -15,8 +25,8 @@ pub struct TemplateInput<'a> { pub escaper: &'a str, pub ext: Option, pub mime_type: String, - pub parent: Option<&'a syn::Type>, pub path: PathBuf, + pub multi: Option<(String, Vec>)>, } impl TemplateInput<'_> { @@ -31,6 +41,7 @@ impl TemplateInput<'_> { // Check that an attribute called `template()` exists once and that it is // the proper type (list). let mut template_args = None; + let mut multi_args = vec![]; for attr in &ast.attrs { if attr.path.is_ident("template") { if template_args.is_some() { @@ -44,6 +55,18 @@ impl TemplateInput<'_> { Ok(_) => return Err("'template' attribute must be a list".into()), Err(e) => return Err(format!("unable to parse attribute: {}", e).into()), } + } else if attr.path.is_ident("multi_template") { + match attr.parse_meta() { + Ok(syn::Meta::List(inner)) => { + multi_args.push(inner.nested); + } + Ok(_) => return Err("attribute 'multi_template' has incorrect type".into()), + Err(e) => { + return Err( + format!("unable to parse 'multi_template' attribute: {}", e).into() + ) + } + } } } let template_args = @@ -53,10 +76,13 @@ impl TemplateInput<'_> { // understand. Raise panics if something is not right. // `source` contains an enum that can represent `path` or `source`. let mut source = None; + let mut path = None; let mut print = Print::None; let mut escaping = None; let mut ext = None; let mut syntax = None; + let mut multi = None; + for item in template_args { let pair = match item { syn::NestedMeta::Meta(syn::Meta::NameValue(ref pair)) => pair, @@ -71,19 +97,13 @@ impl TemplateInput<'_> { if pair.path.is_ident("path") { if let syn::Lit::Str(ref s) = pair.lit { - if source.is_some() { - return Err("must specify 'source' or 'path', not both".into()); - } - source = Some(Source::Path(s.value())); + path = Some(s.value()); } else { return Err("template path must be string literal".into()); } } else if pair.path.is_ident("source") { if let syn::Lit::Str(ref s) = pair.lit { - if source.is_some() { - return Err("must specify 'source' or 'path', not both".into()); - } - source = Some(Source::Source(s.value())); + source = Some(s.value()); } else { return Err("template source must be string literal".into()); } @@ -111,6 +131,16 @@ impl TemplateInput<'_> { } else { return Err("syntax value must be string literal".into()); } + } else if pair.path.is_ident("multi") { + if let syn::Lit::Str(ref s) = pair.lit { + let s = s.value(); + if one_expr(&s).is_err() { + return Err(CompileError::Static("multi value must be expression")); + } + multi = Some(s); + } else { + return Err("localizer value must be string literal".into()); + } } else { return Err(format!( "unsupported attribute key '{}' found", @@ -120,69 +150,26 @@ impl TemplateInput<'_> { } } - // Validate the `source` and `ext` value together, since they are - // related. In case `source` was used instead of `path`, the value - // of `ext` is merged into a synthetic `path` value here. - let source = source.expect("template path or source not found in attributes"); - let path = match (&source, &ext) { - (&Source::Path(ref path), _) => config.find_template(path, None)?, - (&Source::Source(_), Some(ext)) => PathBuf::from(format!("{}.{}", ast.ident, ext)), - (&Source::Source(_), None) => { - return Err("must include 'ext' attribute when using 'source' attribute".into()) + // 'multi' and 'multi_template' go together + let multi = match (multi, multi_args) { + (None, multi_args) if multi_args.is_empty() => None, + (Some(localizer), multi_args) if !multi_args.is_empty() => { + let multi = multi_args + .into_iter() + .map(|args| parse_multi(ast, config, args)) + .collect::>()?; + Some((localizer, multi)) } - }; - - // Check to see if a `_parent` field was defined on the context - // struct, and store the type for it for use in the code generator. - let parent = match ast.data { - syn::Data::Struct(syn::DataStruct { - fields: syn::Fields::Named(ref fields), - .. - }) => fields - .named - .iter() - .find(|f| f.ident.as_ref().filter(|name| *name == "_parent").is_some()) - .map(|f| &f.ty), - _ => None, - }; - - if parent.is_some() { - eprint!( - " --> in struct {}\n = use of deprecated field '_parent'\n", - ast.ident - ); - } - - // Validate syntax - let syntax = syntax.map_or_else( - || Ok(config.syntaxes.get(config.default_syntax).unwrap()), - |s| { - config.syntaxes.get(&s).ok_or_else(|| { - CompileError::String(format!("attribute syntax {} not exist", s)) - }) - }, - )?; - - // Match extension against defined output formats - - let escaping = escaping.unwrap_or_else(|| { - path.extension() - .map(|s| s.to_str().unwrap()) - .unwrap_or("") - .to_string() - }); - - let mut escaper = None; - for (extensions, path) in &config.escapers { - if extensions.contains(&escaping) { - escaper = Some(path); - break; + _ => { + return Err(CompileError::Static( + "#[template(multi)] and #[multi_template] have to be used together", + )); } - } + }; - let escaper = escaper.ok_or_else(|| { - CompileError::String(format!("no escaper defined for extension '{}'", escaping,)) - })?; + let (source, path) = select_source_and_path(ast, config, source, path, ext.as_deref())?; + let escaper = select_escaper(config, escaping.as_deref(), &path)?; + let syntax = select_syntax(config, syntax)?; let mime_type = extension_to_mime_type(ext_default_to_path(ext.as_deref(), &path).unwrap_or("txt")) @@ -197,8 +184,8 @@ impl TemplateInput<'_> { escaper, ext, mime_type, - parent, path, + multi, }) } @@ -208,6 +195,163 @@ impl TemplateInput<'_> { } } +fn parse_multi<'a>( + ast: &syn::DeriveInput, + config: &'a Config<'a>, + args: impl IntoIterator, +) -> Result, CompileError> { + let mut path = None; + let mut source = None; + let mut ext = None; + let mut syntax = None; + let mut escaping = None; + let mut pattern = None; + + for item in args { + let pair = match item { + syn::NestedMeta::Meta(syn::Meta::NameValue(ref pair)) => pair, + _ => { + return Err(format!( + "unsupported attribute argument {:?}", + item.to_token_stream() + ) + .into()) + } + }; + + if pair.path.is_ident("path") { + if let syn::Lit::Str(ref s) = pair.lit { + path = Some(s.value()); + } else { + return Err("path value must be string literal".into()); + } + } else if pair.path.is_ident("source") { + if let syn::Lit::Str(ref s) = pair.lit { + source = Some(s.value()); + } else { + return Err("source value must be string literal".into()); + } + } else if pair.path.is_ident("ext") { + if let syn::Lit::Str(ref s) = pair.lit { + ext = Some(s.value()); + } else { + return Err("ext value must be string literal".into()); + } + } else if pair.path.is_ident("syntax") { + if let syn::Lit::Str(ref s) = pair.lit { + syntax = Some(s.value()); + } else { + return Err("pattern value must be string literal".into()); + } + } else if pair.path.is_ident("escaping") { + if let syn::Lit::Str(ref s) = pair.lit { + escaping = Some(s.value()); + } else { + return Err("escaping value must be string literal".into()); + } + } else if pair.path.is_ident("pattern") { + if let syn::Lit::Str(ref s) = pair.lit { + pattern = Some(s.value()); + } else { + return Err("pattern value must be string literal".into()); + } + } else { + return Err(format!( + "unsupported attribute key '{}' found", + pair.path.to_token_stream() + ) + .into()); + } + } + + let (source, path) = select_source_and_path(ast, config, source, path, ext.as_deref())?; + let escaper = select_escaper(config, escaping.as_deref(), &path)?; + let syntax = select_syntax(config, syntax)?; + + let pattern = pattern.ok_or(CompileError::Static("multi-template pattern missing"))?; + if one_target(&pattern).is_err() { + return Err("the pattern attribute could not be parsed".into()); + } + + Ok(MultiTemplateInput { + pattern, + source, + path, + escaper, + syntax, + }) +} + +fn select_syntax<'a>( + config: &'a Config<'a>, + syntax: Option, +) -> Result<&'a Syntax<'a>, CompileError> { + syntax.map_or_else( + || Ok(config.syntaxes.get(config.default_syntax).unwrap()), + |s| { + config + .syntaxes + .get(&s) + .ok_or_else(|| CompileError::String(format!("attribute syntax {} not exist", s))) + }, + ) +} + +fn select_source_and_path( + ast: &syn::DeriveInput, + config: &Config<'_>, + source: Option, + path: Option, + ext: Option<&str>, +) -> Result<(Source, PathBuf), CompileError> { + // Validate the `source` and `ext` value together, since they are + // related. In case `source` was used instead of `path`, the value + // of `ext` is merged into a synthetic `path` value here. + + let source = match (source, path) { + (None, None) => return Err("template path or source not found in attributes".into()), + (Some(_), Some(_)) => return Err("must specify 'source' or 'path', not both".into()), + (None, Some(path)) => Source::Path(path), + (Some(source), None) => Source::Source(source), + }; + + let path = match (&source, ext) { + (Source::Path(path), _) => config.find_template(path, None)?, + (Source::Source(_), Some(ext)) => PathBuf::from(format!("{}.{}", ast.ident, ext)), + (Source::Source(_), None) => { + return Err("must include 'ext' attribute when using 'source' attribute".into()) + } + }; + + Ok((source, path)) +} + +fn select_escaper<'a>( + config: &'a Config<'_>, + escaping: Option<&str>, + path: &Path, +) -> Result<&'a str, CompileError> { + let escaping = escaping.unwrap_or_else(|| path.extension().map_or("", |s| s.to_str().unwrap())); + + let mut escaper = None; + for (extensions, path) in &config.escapers { + if extensions.contains(escaping) { + escaper = Some(path); + break; + } + } + + escaper.map_or_else( + || { + Err(CompileError::String(format!( + "no escaper defined for extension '{}'", + escaping + ))) + }, + |s| Ok(s.as_str()), + ) +} + #[inline] pub fn ext_default_to_path<'a>(ext: Option<&'a str>, path: &'a Path) -> Option<&'a str> { ext.or_else(|| extension(path)) @@ -227,6 +371,7 @@ fn extension(path: &Path) -> Option<&str> { } } +#[derive(Clone)] pub enum Source { Path(String), Source(String), diff --git a/askama_shared/src/parser.rs b/askama_shared/src/parser.rs index f5685b9fd..df8cb3946 100644 --- a/askama_shared/src/parser.rs +++ b/askama_shared/src/parser.rs @@ -640,6 +640,14 @@ expr_prec_layer!(expr_compare, expr_bor, "==", "!=", ">=", ">", "<=", "<"); expr_prec_layer!(expr_and, expr_compare, "&&"); expr_prec_layer!(expr_or, expr_and, "||"); +pub(crate) fn one_expr(s: &str) -> Result, nom::Err>> { + Ok(terminated(ws(expr_any), eof)(s)?.1) +} + +pub(crate) fn one_target(s: &str) -> Result, nom::Err>> { + Ok(terminated(ws(target), eof)(s)?.1) +} + fn expr_any(i: &str) -> IResult<&str, Expr<'_>> { let range_right = |i| pair(ws(alt((tag("..="), tag("..")))), opt(expr_or))(i); alt(( diff --git a/testing/templates/localization/base.html b/testing/templates/localization/base.html new file mode 100644 index 000000000..a10b86a68 --- /dev/null +++ b/testing/templates/localization/base.html @@ -0,0 +1,2 @@ +Localization test: +{% block body %}{% endblock body %} diff --git a/testing/templates/localization/index.de.html b/testing/templates/localization/index.de.html new file mode 100644 index 000000000..04b143718 --- /dev/null +++ b/testing/templates/localization/index.de.html @@ -0,0 +1 @@ +Hallo, {{user}}! diff --git a/testing/templates/localization/index.en.html b/testing/templates/localization/index.en.html new file mode 100644 index 000000000..6abbfb472 --- /dev/null +++ b/testing/templates/localization/index.en.html @@ -0,0 +1 @@ +Hello, {{user}}! diff --git a/testing/templates/localization/index.es.html b/testing/templates/localization/index.es.html new file mode 100644 index 000000000..2fa23730b --- /dev/null +++ b/testing/templates/localization/index.es.html @@ -0,0 +1 @@ +¡Hola, {{user}}! diff --git a/testing/templates/localization/index.fr.html b/testing/templates/localization/index.fr.html new file mode 100644 index 000000000..03b91857a --- /dev/null +++ b/testing/templates/localization/index.fr.html @@ -0,0 +1,4 @@ +{% extends "localization/base.html" %} +{% block body -%} + Bonjour, {{user}} ! +{%- endblock body %} diff --git a/testing/templates/localization/index.html b/testing/templates/localization/index.html new file mode 100644 index 000000000..4857bef7f --- /dev/null +++ b/testing/templates/localization/index.html @@ -0,0 +1 @@ +Not implemented: {{language}} diff --git a/testing/tests/inheritance.rs b/testing/tests/inheritance.rs deleted file mode 100644 index 2dac36280..000000000 --- a/testing/tests/inheritance.rs +++ /dev/null @@ -1,306 +0,0 @@ -use askama::Template; - -#[derive(Template)] -#[template(path = "base.html")] -struct BaseTemplate<'a> { - title: &'a str, -} - -#[derive(Template)] -#[template(path = "child.html")] -struct ChildTemplate<'a> { - _parent: BaseTemplate<'a>, -} - -#[test] -fn test_use_base_directly() { - let t = BaseTemplate { title: "Foo" }; - assert_eq!(t.render().unwrap(), "Foo\n\nFoo\nCopyright 2017"); -} - -#[test] -fn test_simple_extends() { - let t = ChildTemplate { - _parent: BaseTemplate { title: "Bar" }, - }; - assert_eq!( - t.render().unwrap(), - "Bar\n(Bar) Content goes here\nFoo\nCopyright 2017" - ); -} - -#[derive(Template)] -#[template(source = "{% extends \"base.html\" %}", ext = "html")] -struct EmptyChild<'a> { - title: &'a str, -} - -#[test] -fn test_empty_child() { - let t = EmptyChild { title: "baz" }; - assert_eq!(t.render().unwrap(), "baz\n\nFoo\nCopyright 2017"); -} - -pub mod parent { - use askama::Template; - #[derive(Template)] - #[template(path = "base.html")] - pub struct BaseTemplate<'a> { - pub title: &'a str, - } -} - -pub mod child { - use super::parent::*; - use askama::Template; - #[derive(Template)] - #[template(path = "child.html")] - pub struct ChildTemplate<'a> { - pub _parent: BaseTemplate<'a>, - } -} - -#[test] -fn test_different_module() { - let t = child::ChildTemplate { - _parent: parent::BaseTemplate { title: "a" }, - }; - assert_eq!( - t.render().unwrap(), - "a\n(a) Content goes here\nFoo\nCopyright 2017" - ); -} - -#[derive(Template)] -#[template(path = "nested-base.html")] -struct NestedBaseTemplate {} - -#[derive(Template)] -#[template(path = "nested-child.html")] -struct NestedChildTemplate { - _parent: NestedBaseTemplate, -} - -#[test] -fn test_nested_blocks() { - let t = NestedChildTemplate { - _parent: NestedBaseTemplate {}, - }; - assert_eq!(t.render().unwrap(), "\ndurpy\n"); -} - -#[derive(Template)] -#[template(path = "deep-base.html")] -struct DeepBaseTemplate { - year: u16, -} - -#[derive(Template)] -#[template(path = "deep-mid.html")] -struct DeepMidTemplate { - _parent: DeepBaseTemplate, - title: String, -} - -#[derive(Template)] -#[template(path = "deep-kid.html")] -struct DeepKidTemplate { - _parent: DeepMidTemplate, - item: String, -} - -#[test] -fn test_deep() { - let t = DeepKidTemplate { - _parent: DeepMidTemplate { - _parent: DeepBaseTemplate { year: 2018 }, - title: "Test".into(), - }, - item: "Foo".into(), - }; - - assert_eq!( - t.render().unwrap(), - " - - - - - - - - -
-
- - Foo Foo Foo - -
-
- nav nav nav -
-
- - -" - ); - assert_eq!( - t._parent.render().unwrap(), - " - - - - Test - - - - - - - -
-
- - No content found - -
-
- nav nav nav -
-
- - -" - ); - assert_eq!( - t._parent._parent.render().unwrap(), - " - - - - - - - - - nav nav nav - Copyright 2018 - - -" - ); -} - -#[derive(Template)] -#[template(path = "deep-base.html")] -struct FlatDeepBaseTemplate { - year: u16, -} - -#[derive(Template)] -#[template(path = "deep-mid.html")] -struct FlatDeepMidTemplate { - title: String, -} - -#[derive(Template)] -#[template(path = "deep-kid.html")] -struct FlatDeepKidTemplate { - item: String, -} - -#[test] -fn test_flat_deep() { - let t = FlatDeepKidTemplate { item: "Foo".into() }; - - assert_eq!( - t.render().unwrap(), - " - - - - - - - - -
-
- - Foo Foo Foo - -
-
- nav nav nav -
-
- - -" - ); - - let t = FlatDeepMidTemplate { - title: "Test".into(), - }; - assert_eq!( - t.render().unwrap(), - " - - - - Test - - - - - - - -
-
- - No content found - -
-
- nav nav nav -
-
- - -" - ); - - let t = FlatDeepBaseTemplate { year: 2018 }; - assert_eq!( - t.render().unwrap(), - " - - - - - - - - - nav nav nav - Copyright 2018 - - -" - ); -} - -#[derive(Template)] -#[template(path = "let-base.html")] -struct LetBase {} - -#[derive(Template)] -#[template(path = "let-child.html")] -struct LetChild {} - -#[test] -fn test_let_block() { - let t = LetChild {}; - assert_eq!(t.render().unwrap(), "1"); -} diff --git a/testing/tests/multi_template.rs b/testing/tests/multi_template.rs new file mode 100644 index 000000000..2a9ca7bc8 --- /dev/null +++ b/testing/tests/multi_template.rs @@ -0,0 +1,52 @@ +use askama::Template; + +#[derive(Template)] +#[template(path = "localization/index.html", multi = "language")] +#[multi_template( + pattern = r#""de""#, + path = "localization/index.de.html", + escaping = "txt" +)] +#[multi_template(pattern = r#""en""#, path = "localization/index.en.html")] +#[multi_template(pattern = r#""es""#, path = "localization/index.es.html")] +#[multi_template(pattern = r#""fr""#, path = "localization/index.fr.html")] +struct MultiTemplate<'a> { + language: &'a str, + user: &'a str, +} + +#[test] +fn test_localization() { + let template = MultiTemplate { + language: "de", + user: "", + }; + assert_eq!(template.render().unwrap(), "Hallo, !"); + + let template = MultiTemplate { + language: "en", + user: "", + }; + assert_eq!(template.render().unwrap(), "Hello, <you>!"); + + let template = MultiTemplate { + language: "es", + user: "", + }; + assert_eq!(template.render().unwrap(), "¡Hola, <you>!"); + + let template = MultiTemplate { + language: "fr", + user: "", + }; + assert_eq!( + template.render().unwrap(), + "Localization test:\nBonjour, <you> !" + ); + + let template = MultiTemplate { + language: "xx", + user: "", + }; + assert_eq!(template.render().unwrap(), "Not implemented: xx"); +}