use std::borrow::Cow; use std::collections::hash_map::{Entry, HashMap}; use std::fs::read_to_string; use std::iter::FusedIterator; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::{Arc, OnceLock}; use parser::node::Whitespace; use parser::{Node, Parsed}; use proc_macro2::Span; use rustc_hash::FxBuildHasher; use syn::punctuated::Punctuated; use syn::spanned::Spanned; use syn::{Attribute, Expr, ExprLit, ExprPath, Ident, Lit, LitBool, LitStr, Meta, Token}; use crate::config::{Config, SyntaxAndCache}; use crate::{CompileError, FileInfo, MsgValidEscapers, OnceMap}; #[derive(Clone)] pub(crate) struct TemplateInput<'a> { pub(crate) ast: &'a syn::DeriveInput, pub(crate) enum_ast: Option<&'a syn::DeriveInput>, pub(crate) config: &'a Config, pub(crate) syntax: &'a SyntaxAndCache<'a>, pub(crate) source: &'a Source, pub(crate) source_span: Option, pub(crate) block: Option<(&'a str, Span)>, #[cfg(feature = "blocks")] pub(crate) blocks: &'a [Block], pub(crate) print: Print, pub(crate) escaper: &'a str, pub(crate) path: Arc, pub(crate) fields: Arc<[String]>, } impl TemplateInput<'_> { /// Extract the template metadata from the `DeriveInput` structure. This /// mostly recovers the data for the `TemplateInput` fields from the /// `template()` attribute list fields. pub(crate) fn new<'n>( ast: &'n syn::DeriveInput, enum_ast: Option<&'n syn::DeriveInput>, config: &'n Config, args: &'n TemplateArgs, ) -> Result, CompileError> { let TemplateArgs { source: (source, source_span), block, #[cfg(feature = "blocks")] blocks, print, escaping, ext, ext_span, syntax, .. } = args; // 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 path = match (&source, &ext) { (Source::Path(path), _) => config.find_template(path, None, None)?, (&Source::Source(_), Some(ext)) => { PathBuf::from(format!("{}.{}", ast.ident, ext)).into() } (&Source::Source(_), None) => { return Err(CompileError::no_file_info( #[cfg(not(feature = "code-in-doc"))] "must include `ext` attribute when using `source` attribute", #[cfg(feature = "code-in-doc")] "must include `ext` attribute when using `source` or `in_doc` attribute", None, )); } }; // Validate syntax let syntax = syntax.as_deref().map_or_else( || Ok(config.syntaxes.get(config.default_syntax).unwrap()), |s| { config.syntaxes.get(s).ok_or_else(|| { CompileError::no_file_info(format_args!("syntax `{s}` is undefined"), None) }) }, )?; // Match extension against defined output formats let escaping = escaping .as_deref() .or_else(|| path.extension().and_then(|s| s.to_str())) .unwrap_or_default(); let escaper = config .escapers .iter() .find_map(|(extensions, path)| { extensions .contains(&Cow::Borrowed(escaping)) .then_some(path.as_ref()) }) .ok_or_else(|| { CompileError::no_file_info( format_args!( "no escaper defined for extension '{escaping}'. You can define an escaper \ in the config file (named `askama.toml` by default). {}", MsgValidEscapers(&config.escapers), ), *ext_span, ) })?; let empty_punctuated = Punctuated::new(); let fields = match ast.data { syn::Data::Struct(ref struct_) => { if let syn::Fields::Named(ref fields) = &struct_.fields { &fields.named } else { &empty_punctuated } } syn::Data::Union(ref union_) => &union_.fields.named, syn::Data::Enum(_) => &empty_punctuated, } .iter() .map(|f| match &f.ident { Some(ident) => ident.to_string(), None => unreachable!("we checked that we are using a struct"), }) .collect::>(); Ok(TemplateInput { ast, enum_ast, config, syntax, source, source_span: *source_span, block: block.as_ref().map(|(block, span)| (block.as_str(), *span)), #[cfg(feature = "blocks")] blocks: blocks.as_slice(), print: *print, escaper, path, fields: fields.into(), }) } pub(crate) fn find_used_templates( &self, map: &mut HashMap, Arc, FxBuildHasher>, ) -> Result<(), CompileError> { let (source, source_path) = match &self.source { Source::Source(s) => (s.clone(), None), Source::Path(_) => ( get_template_source(&self.path, None)?, Some(Arc::clone(&self.path)), ), }; let mut dependency_graph = Vec::new(); let mut check = vec![(Arc::clone(&self.path), source, source_path)]; while let Some((path, source, source_path)) = check.pop() { let parsed = match self.syntax.parse(Arc::clone(&source), source_path) { Ok(parsed) => parsed, Err(err) => { let msg = err .message .unwrap_or_else(|| "failed to parse template source".into()); let file_path = err .file_path .as_deref() .unwrap_or(Path::new("")); let file_info = FileInfo::new(file_path, Some(&source), Some(&source[err.offset..])); return Err(CompileError::new(msg, Some(file_info))); } }; let mut top = true; let mut nested = vec![parsed.nodes()]; while let Some(nodes) = nested.pop() { for n in nodes { let mut add_to_check = |new_path: Arc| -> Result<(), CompileError> { if let Entry::Vacant(e) = map.entry(new_path) { // Add a dummy entry to `map` in order to prevent adding `path` // multiple times to `check`. let new_path = e.key(); let source = parsed.source(); let source = get_template_source( new_path, Some(( &path, source, n.span().as_suffix_of(source).unwrap_or_default(), )), )?; check.push((new_path.clone(), source, Some(new_path.clone()))); e.insert(Arc::default()); } Ok(()) }; match n { Node::Extends(extends) if top => { let extends = self.config.find_template( extends.path, Some(&path), Some(FileInfo::of(extends.span(), &path, &parsed)), )?; let dependency_path = (path.clone(), extends.clone()); if path == extends { // We add the path into the graph to have a better looking error. dependency_graph.push(dependency_path); return cyclic_graph_error(&dependency_graph); } else if dependency_graph.contains(&dependency_path) { return cyclic_graph_error(&dependency_graph); } dependency_graph.push(dependency_path); add_to_check(extends)?; } Node::Macro(m) if top => { nested.push(&m.nodes); } Node::Import(import) if top => { let import = self.config.find_template( import.path, Some(&path), Some(FileInfo::of(import.span(), &path, &parsed)), )?; add_to_check(import)?; } Node::FilterBlock(f) => { nested.push(&f.nodes); } Node::Include(include) => { let include = self.config.find_template( include.path, Some(&path), Some(FileInfo::of(include.span(), &path, &parsed)), )?; add_to_check(include)?; } Node::BlockDef(b) => { nested.push(&b.nodes); } Node::If(i) => { for cond in &i.branches { nested.push(&cond.nodes); } } Node::Loop(l) => { nested.push(&l.body); nested.push(&l.else_nodes); } Node::Match(m) => { for arm in &m.arms { nested.push(&arm.nodes); } } Node::Lit(_) | Node::Comment(_) | Node::Expr(_, _) | Node::Call(_) | Node::Extends(_) | Node::Let(_) | Node::Import(_) | Node::Macro(_) | Node::Raw(_) | Node::Continue(_) | Node::Break(_) => {} } } top = false; } map.insert(path, parsed); } Ok(()) } } pub(crate) enum AnyTemplateArgs { Struct(TemplateArgs), Enum { enum_args: Option, vars_args: Vec>, has_default_impl: bool, }, } impl AnyTemplateArgs { pub(crate) fn new(ast: &syn::DeriveInput) -> Result { let syn::Data::Enum(enum_data) = &ast.data else { return Ok(Self::Struct(TemplateArgs::new(ast)?)); }; let enum_args = PartialTemplateArgs::new(ast, &ast.attrs, false)?; let vars_args = enum_data .variants .iter() .map(|variant| PartialTemplateArgs::new(ast, &variant.attrs, true)) .collect::, _>>()?; if vars_args.is_empty() { return Ok(Self::Struct(TemplateArgs::from_partial(ast, enum_args)?)); } let mut needs_default_impl = vars_args.len(); let enum_source = enum_args.as_ref().and_then(|v| v.source.as_ref()); for (variant, var_args) in enum_data.variants.iter().zip(&vars_args) { if var_args .as_ref() .and_then(|v| v.source.as_ref()) .or(enum_source) .is_none() { return Err(CompileError::new_with_span( #[cfg(not(feature = "code-in-doc"))] "either all `enum` variants need a `path` or `source` argument, \ or the `enum` itself needs a default implementation", #[cfg(feature = "code-in-doc")] "either all `enum` variants need a `path`, `source` or `in_doc` argument, \ or the `enum` itself needs a default implementation", None, Some(variant.ident.span()), )); } else if !var_args.is_none() { needs_default_impl -= 1; } } Ok(Self::Enum { enum_args, vars_args, has_default_impl: needs_default_impl > 0, }) } pub(crate) fn take_crate_name(&mut self) -> Option { match self { AnyTemplateArgs::Struct(template_args) => template_args.crate_name.take(), AnyTemplateArgs::Enum { enum_args, .. } => { if let Some(PartialTemplateArgs { crate_name, .. }) = enum_args { crate_name.take() } else { None } } } } } #[cfg(feature = "blocks")] pub(crate) struct Block { pub(crate) name: String, pub(crate) span: Span, } pub(crate) struct TemplateArgs { pub(crate) source: (Source, Option), block: Option<(String, Span)>, #[cfg(feature = "blocks")] blocks: Vec, print: Print, escaping: Option, ext: Option, ext_span: Option, syntax: Option, config: Option, crate_name: Option, pub(crate) whitespace: Option, pub(crate) template_span: Option, pub(crate) config_span: Option, } impl TemplateArgs { pub(crate) fn new(ast: &syn::DeriveInput) -> Result { Self::from_partial(ast, PartialTemplateArgs::new(ast, &ast.attrs, false)?) } pub(crate) fn from_partial( ast: &syn::DeriveInput, args: Option, ) -> Result { let Some(args) = args else { return Err(CompileError::new_with_span( "no attribute `template` found", None, Some(ast.ident.span()), )); }; Ok(Self { source: match args.source { Some(PartialTemplateArgsSource::Path(s)) => { (Source::Path(s.value().into()), Some(s.span())) } Some(PartialTemplateArgsSource::Source(s)) => { (Source::Source(s.value().into()), Some(s.span())) } #[cfg(feature = "code-in-doc")] Some(PartialTemplateArgsSource::InDoc(span, source)) => (source, Some(span)), None => { return Err(CompileError::no_file_info( #[cfg(not(feature = "code-in-doc"))] "specify one template argument `path` or `source`", #[cfg(feature = "code-in-doc")] "specify one template argument `path`, `source` or `in_doc`", Some(args.template.span()), )); } }, block: args.block.map(|value| (value.value(), value.span())), #[cfg(feature = "blocks")] blocks: args .blocks .unwrap_or_default() .into_iter() .map(|value| Block { name: value.value(), span: value.span(), }) .collect(), print: args.print.unwrap_or_default(), escaping: args.escape.map(|value| value.value()), ext: args.ext.as_ref().map(|value| value.value()), ext_span: args.ext.as_ref().map(|value| value.span()), syntax: args.syntax.map(|value| value.value()), config: args.config.as_ref().map(|value| value.value()), crate_name: args.crate_name, whitespace: args.whitespace, template_span: Some(args.template.span()), config_span: args.config.as_ref().map(|value| value.span()), }) } pub(crate) fn fallback() -> Self { Self { source: (Source::Source("".into()), None), block: None, #[cfg(feature = "blocks")] blocks: vec![], print: Print::default(), escaping: None, ext: Some("txt".to_string()), ext_span: None, syntax: None, config: None, crate_name: None, whitespace: None, template_span: None, config_span: None, } } pub(crate) fn config_path(&self) -> Option<&str> { self.config.as_deref() } } /// Try to find the source in the comment, in a `askama` code block. /// /// This is only done if no path or source was given in the `#[template]` attribute. #[cfg(feature = "code-in-doc")] fn source_from_docs( span: Span, docs: &[&Attribute], ast: &syn::DeriveInput, ) -> Result<(Source, Option), CompileError> { let (source_span, source) = collect_comment_blocks(span, docs, ast)?; let source = strip_common_ws_prefix(source); let source = collect_askama_code_blocks(span, ast, source)?; Ok((source, source_span)) } #[cfg(feature = "code-in-doc")] fn collect_comment_blocks( span: Span, docs: &[&Attribute], ast: &syn::DeriveInput, ) -> Result<(Option, String), CompileError> { let mut source_span: Option = None; let mut assign_span = |kv: &syn::MetaNameValue| { // FIXME: uncomment once is stable // let new_span = kv.path.span(); // source_span = Some(match source_span { // Some(cur_span) => cur_span.join(new_span).unwrap_or(cur_span), // None => new_span, // }); if source_span.is_none() { source_span = Some(kv.path.span()); } }; let mut source = String::new(); for a in docs { // is a comment? let Meta::NameValue(kv) = &a.meta else { continue; }; if !kv.path.is_ident("doc") { continue; } // is an understood comment, e.g. not `#[doc = inline_str(…)]` let mut value = &kv.value; let value = loop { match value { Expr::Lit(lit) => break lit, Expr::Group(group) => value = &group.expr, _ => continue, } }; let Lit::Str(value) = &value.lit else { continue; }; assign_span(kv); source.push_str(value.value().as_str()); source.push('\n'); } if source.is_empty() { return Err(no_askama_code_block(span, ast)); } Ok((source_span, source)) } #[cfg(feature = "code-in-doc")] fn no_askama_code_block(span: Span, ast: &syn::DeriveInput) -> CompileError { let kind = match &ast.data { syn::Data::Struct(_) => "struct", syn::Data::Enum(_) => "enum", // actually unreachable: `union`s are rejected by `TemplateArgs::new()` syn::Data::Union(_) => "union", }; CompileError::no_file_info( format_args!( "when using `in_doc` with the value `true`, the {kind}'s documentation needs a \ `askama` code block" ), Some(span), ) } #[cfg(feature = "code-in-doc")] fn strip_common_ws_prefix(source: String) -> String { let mut common_prefix_iter = source .lines() .filter_map(|s| Some(&s[..s.find(|c: char| !c.is_ascii_whitespace())?])); let mut common_prefix = common_prefix_iter.next().unwrap_or_default(); for p in common_prefix_iter { if common_prefix.is_empty() { break; } let ((pos, _), _) = common_prefix .char_indices() .zip(p.char_indices()) .take_while(|(l, r)| l == r) .last() .unwrap_or_default(); common_prefix = &common_prefix[..pos]; } if common_prefix.is_empty() { return source; } source .lines() .flat_map(|s| [s.get(common_prefix.len()..).unwrap_or_default(), "\n"]) .collect() } #[cfg(feature = "code-in-doc")] fn collect_askama_code_blocks( span: Span, ast: &syn::DeriveInput, source: String, ) -> Result { use pulldown_cmark::{CodeBlockKind, Event, Parser, Tag, TagEnd}; let mut tmpl_source = String::new(); let mut in_askama_code = false; let mut had_askama_code = false; for e in Parser::new(&source) { match (in_askama_code, e) { (false, Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(s)))) => { if s.split(",") .any(|s| JINJA_EXTENSIONS.contains(&s.trim_ascii())) { in_askama_code = true; had_askama_code = true; } } (true, Event::End(TagEnd::CodeBlock)) => in_askama_code = false, (true, Event::Text(text)) => tmpl_source.push_str(&text), _ => {} } } if !had_askama_code { return Err(no_askama_code_block(span, ast)); } if tmpl_source.ends_with('\n') { tmpl_source.pop(); } Ok(Source::Source(tmpl_source.into())) } struct ResultIter(Result>); impl From> for ResultIter { fn from(value: Result) -> Self { Self(match value { Ok(i) => Ok(i.into_iter()), Err(e) => Err(Some(e)), }) } } impl Iterator for ResultIter { type Item = Result; fn next(&mut self) -> Option { match &mut self.0 { Ok(iter) => Some(Ok(iter.next()?)), Err(err) => Some(Err(err.take()?)), } } } impl FusedIterator for ResultIter {} #[derive(Debug, Clone, Hash, PartialEq)] pub(crate) enum Source { Path(Arc), Source(Arc), } #[derive(Clone, Copy, Debug, PartialEq, Hash)] pub(crate) enum Print { All, Ast, Code, None, } impl Default for Print { fn default() -> Self { Self::None } } impl FromStr for Print { type Err = String; fn from_str(s: &str) -> Result { match s { "all" => Ok(Self::All), "ast" => Ok(Self::Ast), "code" => Ok(Self::Code), "none" => Ok(Self::None), _ => Err(format!("invalid value for `print` option: {s}")), } } } fn cyclic_graph_error(dependency_graph: &[(Arc, Arc)]) -> Result<(), CompileError> { Err(CompileError::no_file_info( format_args!( "cyclic dependency in graph {:#?}", dependency_graph .iter() .map(|e| format!("{:#?} --> {:#?}", e.0, e.1)) .collect::>() ), None, )) } pub(crate) fn get_template_source( tpl_path: &Arc, import_from: Option<(&Arc, &str, &str)>, ) -> Result, CompileError> { static CACHE: OnceLock, Arc>> = OnceLock::new(); CACHE.get_or_init(OnceMap::default).get_or_try_insert( tpl_path, |tpl_path| match read_to_string(tpl_path) { Ok(mut source) => { if source.ends_with('\n') { let _ = source.pop(); } Ok((Arc::clone(tpl_path), Arc::from(source))) } Err(err) => Err(CompileError::new( format_args!( "unable to open template file '{}': {err}", tpl_path.to_str().unwrap(), ), import_from.map(|(node_file, file_source, node_source)| { FileInfo::new(node_file, Some(file_source), Some(node_source)) }), )), }, Arc::clone, ) } pub(crate) struct PartialTemplateArgs { pub(crate) template: Ident, pub(crate) source: Option, pub(crate) block: Option, pub(crate) print: Option, pub(crate) escape: Option, pub(crate) ext: Option, pub(crate) syntax: Option, pub(crate) config: Option, pub(crate) whitespace: Option, pub(crate) crate_name: Option, #[cfg(feature = "blocks")] pub(crate) blocks: Option>, } #[derive(Clone)] pub(crate) enum PartialTemplateArgsSource { Path(LitStr), Source(LitStr), #[cfg(feature = "code-in-doc")] InDoc(Span, Source), } impl PartialTemplateArgsSource { pub(crate) fn span(&self) -> Span { match self { Self::Path(s) => s.span(), Self::Source(s) => s.span(), #[cfg(feature = "code-in-doc")] Self::InDoc(s, _) => s.span(), } } } // implement PartialTemplateArgs::new() const _: () = { impl PartialTemplateArgs { pub(crate) fn new( ast: &syn::DeriveInput, attrs: &[Attribute], is_enum_variant: bool, ) -> Result, CompileError> { new(ast, attrs, is_enum_variant) } } #[inline] fn new( ast: &syn::DeriveInput, attrs: &[Attribute], is_enum_variant: bool, ) -> Result, CompileError> { // FIXME: implement once is stable if let syn::Data::Union(data) = &ast.data { return Err(CompileError::new_with_span( "askama templates are not supported for `union` types, only `struct` and `enum`", None, Some(data.union_token.span), )); } #[cfg(feature = "code-in-doc")] let mut meta_docs = vec![]; let mut this = PartialTemplateArgs { template: Ident::new("template", Span::call_site()), source: None, block: None, print: None, escape: None, ext: None, syntax: None, config: None, whitespace: None, crate_name: None, #[cfg(feature = "blocks")] blocks: None, }; let mut has_data = false; for attr in attrs { let Some(ident) = attr.path().get_ident() else { continue; }; if ident == "template" { this.template = ident.clone(); has_data = true; } else { #[cfg(feature = "code-in-doc")] if ident == "doc" { meta_docs.push(attr); } continue; } let args = attr .parse_args_with(>::parse_terminated) .map_err(|e| { CompileError::no_file_info( format_args!("unable to parse template arguments: {e}"), Some(attr.path().span()), ) })?; for arg in args { let pair = match arg { Meta::NameValue(pair) => pair, v => { return Err(CompileError::no_file_info( "unsupported attribute argument", Some(v.span()), )); } }; let ident = match pair.path.get_ident() { Some(ident) => ident, None => unreachable!("not possible in syn::Meta::NameValue(…)"), }; if ident == "askama" { if is_enum_variant { return Err(CompileError::no_file_info( "template attribute `askama` can only be used on the `enum`, \ not its variants", Some(ident.span()), )); } ensure_only_once(ident, &mut this.crate_name)?; this.crate_name = Some(get_exprpath(ident, pair.value)?); continue; } else if ident == "blocks" { if !cfg!(feature = "blocks") { return Err(CompileError::no_file_info( "enable feature `blocks` to use `blocks` argument", Some(ident.span()), )); } else if is_enum_variant { return Err(CompileError::no_file_info( "template attribute `blocks` can only be used on the `enum`, \ not its variants", Some(ident.span()), )); } #[cfg(feature = "blocks")] { ensure_only_once(ident, &mut this.blocks)?; this.blocks = Some( get_exprarray(ident, pair.value)? .elems .into_iter() .map(|value| get_strlit(ident, get_lit(ident, value)?)) .collect::>()?, ); continue; } } let value = get_lit(ident, pair.value)?; if ident == "path" { ensure_source_only_once(ident, &this.source)?; this.source = Some(PartialTemplateArgsSource::Path(get_strlit(ident, value)?)); } else if ident == "source" { ensure_source_only_once(ident, &this.source)?; this.source = Some(PartialTemplateArgsSource::Source(get_strlit(ident, value)?)); } else if ident == "in_doc" { let value = get_boollit(ident, value)?; if !value.value() { continue; } ensure_source_only_once(ident, &this.source)?; #[cfg(not(feature = "code-in-doc"))] { return Err(CompileError::no_file_info( "enable feature `code-in-doc` to use `in_doc` argument", Some(ident.span()), )); } #[cfg(feature = "code-in-doc")] { this.source = Some(PartialTemplateArgsSource::InDoc( value.span(), Source::Path("".into()), )); } } else if ident == "block" { set_strlit_pair(ident, value, &mut this.block)?; } else if ident == "print" { set_parseable_string(ident, value, &mut this.print)?; } else if ident == "escape" { set_strlit_pair(ident, value, &mut this.escape)?; } else if ident == "ext" { set_strlit_pair(ident, value, &mut this.ext)?; } else if ident == "syntax" { set_strlit_pair(ident, value, &mut this.syntax)?; } else if ident == "config" { set_strlit_pair(ident, value, &mut this.config)?; } else if ident == "whitespace" { set_parseable_string(ident, value, &mut this.whitespace)?; } else { return Err(CompileError::no_file_info( format_args!("unsupported template attribute `{ident}` found"), Some(ident.span()), )); } } } if !has_data { return Ok(None); } #[cfg(feature = "code-in-doc")] if let Some(PartialTemplateArgsSource::InDoc(lit_span, _)) = this.source { let (source, doc_span) = source_from_docs(lit_span, &meta_docs, ast)?; this.source = Some(PartialTemplateArgsSource::InDoc( doc_span.unwrap_or(lit_span), source, )); } Ok(Some(this)) } fn set_strlit_pair( name: &Ident, value: ExprLit, dest: &mut Option, ) -> Result<(), CompileError> { ensure_only_once(name, dest)?; *dest = Some(get_strlit(name, value)?); Ok(()) } fn set_parseable_string>( name: &Ident, value: ExprLit, dest: &mut Option, ) -> Result<(), CompileError> { ensure_only_once(name, dest)?; let str_value = get_strlit(name, value)?; *dest = Some( str_value .value() .parse() .map_err(|msg| CompileError::no_file_info(msg, Some(str_value.span())))?, ); Ok(()) } fn ensure_only_once(name: &Ident, dest: &mut Option) -> Result<(), CompileError> { if dest.is_none() { Ok(()) } else { Err(CompileError::no_file_info( format_args!("template attribute `{name}` already set"), Some(name.span()), )) } } fn get_lit(name: &Ident, mut expr: Expr) -> Result { loop { match expr { Expr::Lit(lit) => return Ok(lit), Expr::Group(group) => expr = *group.expr, v => { return Err(CompileError::no_file_info( format_args!("template attribute `{name}` expects a literal"), Some(v.span()), )); } } } } fn get_strlit(name: &Ident, value: ExprLit) -> Result { if let Lit::Str(s) = value.lit { Ok(s) } else { Err(CompileError::no_file_info( format_args!("template attribute `{name}` expects a string literal"), Some(value.lit.span()), )) } } fn get_boollit(name: &Ident, value: ExprLit) -> Result { if let Lit::Bool(s) = value.lit { Ok(s) } else { Err(CompileError::no_file_info( format_args!("template attribute `{name}` expects a boolean value"), Some(value.lit.span()), )) } } fn get_exprpath(name: &Ident, mut expr: Expr) -> Result { loop { match expr { Expr::Path(path) => return Ok(path), Expr::Group(group) => expr = *group.expr, v => { return Err(CompileError::no_file_info( format_args!("template attribute `{name}` expects a path or identifier"), Some(v.span()), )); } } } } #[cfg(feature = "blocks")] fn get_exprarray(name: &Ident, mut expr: Expr) -> Result { loop { match expr { Expr::Array(array) => return Ok(array), Expr::Group(group) => expr = *group.expr, v => { return Err(CompileError::no_file_info( format_args!("template attribute `{name}` expects an array"), Some(v.span()), )); } } } } fn ensure_source_only_once( name: &Ident, source: &Option, ) -> Result<(), CompileError> { if source.is_some() { return Err(CompileError::no_file_info( #[cfg(feature = "code-in-doc")] "must specify `source`, `path` or `is_doc` exactly once", #[cfg(not(feature = "code-in-doc"))] "must specify `source` or `path` exactly once", Some(name.span()), )); } Ok(()) } }; #[cfg(feature = "code-in-doc")] const JINJA_EXTENSIONS: &[&str] = &["askama", "j2", "jinja", "jinja2", "rinja"]; #[test] fn get_source() { let path = Config::new("", None, None, None, None) .and_then(|config| config.find_template("b.html", None, None)) .unwrap(); assert_eq!(get_template_source(&path, None).unwrap(), "bar".into()); }