From fc047882c556aab7a3c6defaa7c8efd9f2ca813a Mon Sep 17 00:00:00 2001 From: YannikSc Date: Tue, 24 Oct 2023 10:53:55 +0200 Subject: [PATCH 1/4] feature: Add embed node --- askama_derive/src/generator.rs | 50 +++++++++++++++++++++++++- askama_parser/src/node.rs | 44 +++++++++++++++++++++++ testing/templates/embed_base.html | 7 ++++ testing/templates/embed_base_base.html | 7 ++++ testing/templates/embed_parent.html | 7 ++++ testing/tests/embed.rs | 43 ++++++++++++++++++++++ 6 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 testing/templates/embed_base.html create mode 100644 testing/templates/embed_base_base.html create mode 100644 testing/templates/embed_parent.html create mode 100644 testing/tests/embed.rs diff --git a/askama_derive/src/generator.rs b/askama_derive/src/generator.rs index 6f561f83b..ee356c800 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,33 @@ 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 mut generator = Generator::new( + self.input, + self.contexts, + Some(&heritage), + MapChain::new(), + self.whitespace, + ); + self.prepare_ws(embed.ws2); + generator.handle(heritage.root, &heritage.root.nodes, buf, AstLevel::Top) + } + 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/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); +} From b44d0649b0c01064982ef13987e896184b18f9e0 Mon Sep 17 00:00:00 2001 From: Yannik_Sc Date: Tue, 24 Oct 2023 22:16:06 +0200 Subject: [PATCH 2/4] Add documentation for the `embed` node --- book/src/template_syntax.md | 50 +++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) 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 From 693ad68ae4cd1cca54a7e61e29c76f917443d4e5 Mon Sep 17 00:00:00 2001 From: Yannik_Sc Date: Tue, 24 Oct 2023 23:09:19 +0200 Subject: [PATCH 3/4] Fix clippy annotations --- askama_derive/src/generator.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/askama_derive/src/generator.rs b/askama_derive/src/generator.rs index ee356c800..e430e8843 100644 --- a/askama_derive/src/generator.rs +++ b/askama_derive/src/generator.rs @@ -1088,7 +1088,7 @@ impl<'a> Generator<'a> { 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 heritage = Heritage::new(&embedded_context, self.contexts); let mut generator = Generator::new( self.input, @@ -1098,7 +1098,7 @@ impl<'a> Generator<'a> { self.whitespace, ); self.prepare_ws(embed.ws2); - generator.handle(heritage.root, &heritage.root.nodes, buf, AstLevel::Top) + generator.handle(heritage.root, heritage.root.nodes, buf, AstLevel::Top) } fn is_shadowing_variable(&self, var: &Target<'a>) -> Result { From 0ad1c0d326821a16ab51b179f20afef12f37bcdc Mon Sep 17 00:00:00 2001 From: YannikSc Date: Tue, 7 Nov 2023 14:42:15 +0100 Subject: [PATCH 4/4] Update askama_derive/src/generator.rs Co-authored-by: Matthew Taylor --- askama_derive/src/generator.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/askama_derive/src/generator.rs b/askama_derive/src/generator.rs index e430e8843..ecf5344f1 100644 --- a/askama_derive/src/generator.rs +++ b/askama_derive/src/generator.rs @@ -1090,15 +1090,17 @@ impl<'a> Generator<'a> { embedded_context.extends = Some(embed_path); let heritage = Heritage::new(&embedded_context, self.contexts); - let mut generator = Generator::new( + let locals = MapChain::with_parent(&self.locals); + let mut generator = Self::new( self.input, self.contexts, Some(&heritage), - MapChain::new(), + locals, self.whitespace, ); + let size_hint = generator.handle(heritage.root, heritage.root.nodes, buf, AstLevel::Top); self.prepare_ws(embed.ws2); - generator.handle(heritage.root, heritage.root.nodes, buf, AstLevel::Top) + size_hint } fn is_shadowing_variable(&self, var: &Target<'a>) -> Result {