diff --git a/askama_derive/src/generator.rs b/askama_derive/src/generator.rs index 6f561f83b..ecf5344f1 100644 --- a/askama_derive/src/generator.rs +++ b/askama_derive/src/generator.rs @@ -4,7 +4,7 @@ use crate::input::{Print, Source, TemplateInput}; use crate::CompileError; use parser::node::{ - Call, Comment, CondTest, If, Include, Let, Lit, Loop, Match, Target, Whitespace, Ws, + Call, Comment, CondTest, Embed, If, Include, Let, Lit, Loop, Match, Target, Whitespace, Ws, }; use parser::{Expr, Node, Parsed}; use proc_macro::TokenStream; @@ -236,6 +236,24 @@ fn find_used_templates( let source = get_template_source(&import)?; check.push((import, source)); } + Node::Embed(embed) => { + let embed = input.config.find_template(embed.path, Some(&path))?; + let dependency_path = (path.clone(), embed.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(&embed)?; + check.push((embed, source)); + } _ => {} } } @@ -684,6 +702,9 @@ impl<'a> Generator<'a> { // No whitespace handling: child template top-level is not used, // except for the blocks defined in it. } + Node::Embed(ref embed) => { + size_hint += self.handle_embed(buf, embed)?; + } Node::Break(ws) => { self.handle_ws(ws); self.write_buf_writable(buf)?; @@ -1053,6 +1074,35 @@ impl<'a> Generator<'a> { Ok(size_hint) } + fn handle_embed( + &mut self, + buf: &mut Buffer, + embed: &'a Embed<'_>, + ) -> Result { + self.flush_ws(embed.ws1); + self.write_buf_writable(buf)?; + let embed_path = self + .input + .config + .find_template(embed.path, Some(&self.input.path))?; + let mut embedded_context = + Context::new(self.input.config, &self.input.path, embed.nodes.as_slice())?; + embedded_context.extends = Some(embed_path); + let heritage = Heritage::new(&embedded_context, self.contexts); + + let locals = MapChain::with_parent(&self.locals); + let mut generator = Self::new( + self.input, + self.contexts, + Some(&heritage), + locals, + self.whitespace, + ); + let size_hint = generator.handle(heritage.root, heritage.root.nodes, buf, AstLevel::Top); + self.prepare_ws(embed.ws2); + size_hint + } + fn is_shadowing_variable(&self, var: &Target<'a>) -> Result { match var { Target::Name(name) => { diff --git a/askama_parser/src/node.rs b/askama_parser/src/node.rs index ba4d09edd..19757bbdd 100644 --- a/askama_parser/src/node.rs +++ b/askama_parser/src/node.rs @@ -27,6 +27,7 @@ pub enum Node<'a> { Match(Match<'a>), Loop(Box>), Extends(Extends<'a>), + Embed(Embed<'a>), BlockDef(BlockDef<'a>), Include(Include<'a>), Import(Import<'a>), @@ -56,6 +57,7 @@ impl<'a> Node<'a> { map(|i| Loop::parse(i, s), |l| Self::Loop(Box::new(l))), map(|i| Match::parse(i, s), Self::Match), map(Extends::parse, Self::Extends), + map(|i| Embed::parse(i, s), Self::Embed), map(Include::parse, Self::Include), map(Import::parse, Self::Import), map(|i| BlockDef::parse(i, s), Self::BlockDef), @@ -846,6 +848,48 @@ impl<'a> Extends<'a> { } } +#[derive(Debug, PartialEq)] +pub struct Embed<'a> { + pub ws1: Ws, + pub path: &'a str, + pub nodes: Vec>, + pub ws2: Ws, +} + +impl<'a> Embed<'a> { + fn parse(i: &'a str, s: &State<'_>) -> IResult<&'a str, Self> { + let mut start = tuple(( + opt(Whitespace::parse), + ws(keyword("embed")), + cut(tuple((ws(str_lit), opt(Whitespace::parse), |i| { + s.tag_block_end(i) + }))), + )); + let (i, (pws1, _, (path, nws1, _))) = start(i)?; + + let mut end = cut(tuple(( + |i| Node::many(i, s), + cut(tuple(( + |i| s.tag_block_start(i), + opt(Whitespace::parse), + ws(keyword("endembed")), + cut(opt(Whitespace::parse)), + ))), + ))); + let (i, (nodes, (_, pws2, _, nws2))) = end(i)?; + + Ok(( + i, + Self { + ws1: Ws(pws1, nws1), + path, + nodes, + ws2: Ws(pws2, nws2), + }, + )) + } +} + #[derive(Debug, PartialEq)] pub struct Comment<'a> { pub ws: Ws, diff --git a/book/src/template_syntax.md b/book/src/template_syntax.md index 0b76b22b2..7cfec922d 100644 --- a/book/src/template_syntax.md +++ b/book/src/template_syntax.md @@ -212,6 +212,56 @@ blocks from the base template with those from the child template. Inside a block in a child template, the `super()` macro can be called to render the parent block's contents. +### Embedded templates + +Using the `embed` tag you can *extend* multiple templates at once or differently in the same template + +#### Base template + +```html +
+
{% block title %}{% endblock %}
+
{% block content %}{% endblock %}
+
{% block author %}Yannik{% endblock %}
+
+``` + +#### Page template + +```html +{% extends "base.html" %} + +{% block title %}Index{% endblock %} + +{% block head %} + +{% endblock %} + +{% block content %} + +{% embed "base_section.html" %} + +{% block title %}Example Section{% endblock %} +{% block content %}lorem ipsum ...{% endblock %} + +{% endembed %} + +{% embed "base_section.html" %} + +{% block title %}Another Section{% endblock %} +{% block content %}ipsum lorem ...{% endblock %} + +{% endembed %} + +{% endblock content %} +``` + +This allows you to create reusable component-like templates and `embed` +them wherever and how often you need them. It will work similar to +combining an `include` (as it includes the template) and `extend` +as you are able to override blocks/content from the included template. + ## HTML escaping Askama by default escapes variables if it thinks it is rendering HTML diff --git a/testing/templates/embed_base.html b/testing/templates/embed_base.html new file mode 100644 index 000000000..330822b8b --- /dev/null +++ b/testing/templates/embed_base.html @@ -0,0 +1,7 @@ +{# The embedded template, which again extends another template and overrides a block (just to test the complexity) #} + +{% extends "embed_base_base.html" %} + +{% block title %} +

Hello {{ user.name }}

+{% endblock %} diff --git a/testing/templates/embed_base_base.html b/testing/templates/embed_base_base.html new file mode 100644 index 000000000..309b7658a --- /dev/null +++ b/testing/templates/embed_base_base.html @@ -0,0 +1,7 @@ +{# The base template which is going to be embedded. Could be a responsive container for example #} +
+ {% block title %} +

Hello anonymous

+ {% endblock %} + {% block content %}{% endblock %} +
diff --git a/testing/templates/embed_parent.html b/testing/templates/embed_parent.html new file mode 100644 index 000000000..e212b8d4c --- /dev/null +++ b/testing/templates/embed_parent.html @@ -0,0 +1,7 @@ +{# Base template which gets rendered used for the struct, embedding another template #} + + +{% embed "embed_base.html" %} +{% block content %}

Welcome to this example!

{% endblock %} +{% endembed %} + diff --git a/testing/tests/embed.rs b/testing/tests/embed.rs new file mode 100644 index 000000000..51c64066a --- /dev/null +++ b/testing/tests/embed.rs @@ -0,0 +1,43 @@ +use askama::Template; + +struct FakeUser { + name: String, +} + +#[derive(Template)] +#[template(path = "embed_parent.html")] +struct EmbedTemplate { + user: FakeUser, +} + +fn strip_whitespaces(string: &str) -> String { + string + .split_whitespace() + .filter(|char| !char.is_empty()) + .collect::>() + .join(" ") + .trim_end() + .trim_start() + .to_string() +} + +#[test] +fn test_embed() { + let expected = strip_whitespaces( + r#" + +
+

Hello Yannik

+

Welcome to this example!

+
+ "#, + ); + let template = EmbedTemplate { + user: FakeUser { + name: String::from("Yannik"), + }, + }; + let rendered = strip_whitespaces(&template.render().unwrap()); + + assert_eq!(rendered, expected); +}