From 5cad82f38e800a42717284f20e7e0923add1e32f Mon Sep 17 00:00:00 2001 From: max Date: Mon, 11 Dec 2023 16:43:16 +0200 Subject: Allow included templates to `extend`, `import`, and `macro` Signed-off-by: max --- askama_derive/src/generator.rs | 50 +++++++----- askama_derive/src/heritage.rs | 1 + askama_derive/src/input.rs | 102 +++++++++++++++--------- testing/templates/include-extends-base.html | 6 ++ testing/templates/include-extends-included.html | 2 + testing/templates/include-extends.html | 4 + testing/templates/include-macro.html | 4 + testing/templates/included-macro.html | 6 ++ testing/tests/include.rs | 41 ++++++++++ 9 files changed, 158 insertions(+), 58 deletions(-) create mode 100644 testing/templates/include-extends-base.html create mode 100644 testing/templates/include-extends-included.html create mode 100644 testing/templates/include-extends.html create mode 100644 testing/templates/include-macro.html create mode 100644 testing/templates/included-macro.html diff --git a/askama_derive/src/generator.rs b/askama_derive/src/generator.rs index c1a8ebe..22f996f 100644 --- a/askama_derive/src/generator.rs +++ b/askama_derive/src/generator.rs @@ -1,8 +1,8 @@ use std::collections::hash_map::{Entry, HashMap}; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::{cmp, hash, mem, str}; -use crate::config::{get_template_source, WhitespaceHandling}; +use crate::config::WhitespaceHandling; use crate::heritage::{Context, Heritage}; use crate::input::{Source, TemplateInput}; use crate::CompileError; @@ -10,7 +10,7 @@ use crate::CompileError; use parser::node::{ Call, Comment, CondTest, If, Include, Let, Lit, Loop, Match, Target, Whitespace, Ws, }; -use parser::{Expr, Node, Parsed}; +use parser::{Expr, Node}; use quote::quote; pub(crate) struct Generator<'a> { @@ -20,8 +20,6 @@ pub(crate) struct Generator<'a> { 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 @@ -50,7 +48,6 @@ impl<'a> Generator<'a> { input, contexts, heritage, - includes: HashMap::default(), locals, next_ws: None, skip_ws: WhitespaceHandling::Preserve, @@ -846,24 +843,35 @@ impl<'a> Generator<'a> { )?; } - // Since nodes must not outlive the Generator, we instantiate a nested `Generator` here to - // handle the include's nodes. Unfortunately we can't easily share the `includes` cache. + // We clone the context of the child in order to preserve their macros and imports. + // But also add all the imports and macros from this template that don't override the + // child's ones to preserve this template's context. + let child_ctx = &mut self.contexts[path.as_path()].clone(); + for (name, mac) in &ctx.macros { + child_ctx.macros.entry(name).or_insert(mac); + } + for (name, import) in &ctx.imports { + child_ctx + .imports + .entry(name) + .or_insert_with(|| import.clone()); + } - let locals = MapChain::with_parent(&self.locals); - let mut child = Self::new(self.input, self.contexts, self.heritage, locals); - - let nodes = match self.contexts.get(path.as_path()) { - Some(ctx) => ctx.nodes, - None => match self.includes.entry(path) { - Entry::Occupied(entry) => entry.into_mut().nodes(), - Entry::Vacant(entry) => { - let src = get_template_source(entry.key())?; - entry.insert(Parsed::new(src, self.input.syntax)?).nodes() - } - }, + // Create a new generator for the child, and call it like in `impl_template` as if it were + // a full template, while preserving the context. + let heritage = if !child_ctx.blocks.is_empty() || child_ctx.extends.is_some() { + Some(Heritage::new(child_ctx, self.contexts)) + } else { + None }; - let mut size_hint = child.handle(ctx, nodes, buf, AstLevel::Nested)?; + let handle_ctx = match &heritage { + Some(heritage) => heritage.root, + None => child_ctx, + }; + let locals = MapChain::with_parent(&self.locals); + let mut child = Self::new(self.input, self.contexts, heritage.as_ref(), locals); + let mut size_hint = child.handle(handle_ctx, handle_ctx.nodes, buf, AstLevel::Top)?; size_hint += child.write_buf_writable(buf)?; self.prepare_ws(i.ws); diff --git a/askama_derive/src/heritage.rs b/askama_derive/src/heritage.rs index d75d0a5..bded66b 100644 --- a/askama_derive/src/heritage.rs +++ b/askama_derive/src/heritage.rs @@ -35,6 +35,7 @@ impl Heritage<'_> { type BlockAncestry<'a> = HashMap<&'a str, Vec<(&'a Context<'a>, &'a BlockDef<'a>)>>; +#[derive(Clone)] pub(crate) struct Context<'a> { pub(crate) nodes: &'a [Node<'a>], pub(crate) extends: Option, diff --git a/askama_derive/src/input.rs b/askama_derive/src/input.rs index 57fbc04..54facdc 100644 --- a/askama_derive/src/input.rs +++ b/askama_derive/src/input.rs @@ -113,46 +113,74 @@ impl TemplateInput<'_> { let mut check = vec![(self.path.clone(), source)]; while let Some((path, source)) = check.pop() { let parsed = Parsed::new(source, self.syntax)?; - for n in parsed.nodes() { - use Node::*; - match n { - Extends(extends) => { - let extends = self.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()); + + let mut top = true; + let mut nested = vec![parsed.nodes()]; + while let Some(nodes) = nested.pop() { + for n in nodes { + use Node::*; + match n { + Extends(extends) if top => { + let extends = self.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)); } - dependency_graph.push(dependency_path); - let source = get_template_source(&extends)?; - check.push((extends, source)); - } - Import(import) => { - let import = self.config.find_template(import.path, Some(&path))?; - let source = get_template_source(&import)?; - check.push((import, source)); + Macro(m) if top => { + nested.push(&m.nodes); + } + Import(import) if top => { + let import = self.config.find_template(import.path, Some(&path))?; + let source = get_template_source(&import)?; + check.push((import, source)); + } + Include(include) => { + let include = self.config.find_template(include.path, Some(&path))?; + let source = get_template_source(&include)?; + check.push((include, source)); + } + BlockDef(b) => { + nested.push(&b.nodes); + } + If(i) => { + for cond in &i.branches { + nested.push(&cond.nodes); + } + } + Loop(l) => { + nested.push(&l.body); + nested.push(&l.else_nodes); + } + Match(m) => { + for arm in &m.arms { + nested.push(&arm.nodes); + } + } + Lit(_) + | Comment(_) + | Expr(_, _) + | Call(_) + | Extends(_) + | Let(_) + | Import(_) + | Macro(_) + | Raw(_) + | Continue(_) + | Break(_) => {} } - If(_) - | Loop(_) - | Match(_) - | BlockDef(_) - | Include(_) - | Lit(_) - | Comment(_) - | Expr(_, _) - | Call(_) - | Let(_) - | Macro(_) - | Raw(_) - | Continue(_) - | Break(_) => {} } + top = false; } map.insert(path, parsed); } diff --git a/testing/templates/include-extends-base.html b/testing/templates/include-extends-base.html new file mode 100644 index 0000000..7a54ca0 --- /dev/null +++ b/testing/templates/include-extends-base.html @@ -0,0 +1,6 @@ +
+

Below me is the header

+ {% block header %}{% endblock %} +

Above me is the header

+
+Hello, {{ name }}! diff --git a/testing/templates/include-extends-included.html b/testing/templates/include-extends-included.html new file mode 100644 index 0000000..03b7553 --- /dev/null +++ b/testing/templates/include-extends-included.html @@ -0,0 +1,2 @@ +{% extends "include-extends-base.html" %} +{% block header %}foo{% endblock %} diff --git a/testing/templates/include-extends.html b/testing/templates/include-extends.html new file mode 100644 index 0000000..371c133 --- /dev/null +++ b/testing/templates/include-extends.html @@ -0,0 +1,4 @@ +
+

Welcome

+ {% include "include-extends-included.html" %} +
diff --git a/testing/templates/include-macro.html b/testing/templates/include-macro.html new file mode 100644 index 0000000..e29789d --- /dev/null +++ b/testing/templates/include-macro.html @@ -0,0 +1,4 @@ +{% macro m(name) -%} + Hello, {{ name }}! +{%- endmacro -%} +{% include "included-macro.html" %} diff --git a/testing/templates/included-macro.html b/testing/templates/included-macro.html new file mode 100644 index 0000000..efbae18 --- /dev/null +++ b/testing/templates/included-macro.html @@ -0,0 +1,6 @@ +{% macro m2(name) -%} + Howdy, {{ name }}! +{%- endmacro -%} + +{% call m(name) %} +{% call m2(name2) %} diff --git a/testing/tests/include.rs b/testing/tests/include.rs index f461a7b..c11d96f 100644 --- a/testing/tests/include.rs +++ b/testing/tests/include.rs @@ -12,3 +12,44 @@ fn test_include() { let s = IncludeTemplate { strs: &strs }; assert_eq!(s.render().unwrap(), "\n INCLUDED: foo\n INCLUDED: bar") } + +#[derive(Template)] +#[template(path = "include-extends.html")] +struct IncludeExtendsTemplate<'a> { + name: &'a str, +} + +#[test] +fn test_include_extends() { + let template = IncludeExtendsTemplate { name: "Alice" }; + + assert_eq!( + template.render().unwrap(), + "
\n \ +

Welcome

\n \ +
\n \ +

Below me is the header

\n \ + foo\n \ +

Above me is the header

\n\ +
\n\ + Hello, Alice!\n\ +
" + ); +} + +#[derive(Template)] +#[template(path = "include-macro.html")] +struct IncludeMacroTemplate<'a> { + name: &'a str, + name2: &'a str, +} + +#[test] +fn test_include_macro() { + let template = IncludeMacroTemplate { + name: "Alice", + name2: "Bob", + }; + + assert_eq!(template.render().unwrap(), "Hello, Alice!\nHowdy, Bob!"); +} -- cgit