Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

feature: Add embed node #888

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 51 additions & 1 deletion askama_derive/src/generator.rs
Original file line number Diff line number Diff line change
@@ -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::<Vec<_>>()
)
.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<usize, CompileError> {
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())?;
Copy link
Contributor

@wrapperup wrapperup Nov 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's missing errors for doing anything outside block tags. Some of these don't actually do anything (extends for example gets overwritten), but you're allowed to write macro and include nodes here, which seems weird (this is similar behavior to how normal extended templates work, so maybe not a real issue?)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I see why this is problematic, but it's consistent. I think this is something, that should be fixed/improved in general.

Another real problem, that I mentioned in the issue (#488) is, that the compiler is not including the file when the embed is inside a block in an extended file. This problem however also exists for includes and I'm not sure how to approach this.

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<bool, CompileError> {
match var {
Target::Name(name) => {
44 changes: 44 additions & 0 deletions askama_parser/src/node.rs
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@ pub enum Node<'a> {
Match(Match<'a>),
Loop(Box<Loop<'a>>),
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<Node<'a>>,
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,
50 changes: 50 additions & 0 deletions book/src/template_syntax.md
Original file line number Diff line number Diff line change
@@ -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
<section>
<div>{% block title %}{% endblock %}</div>
<div>{% block content %}{% endblock %}</div>
<div>{% block author %}Yannik{% endblock %}</div>
</section>
```

#### Page template

```html
{% extends "base.html" %}

{% block title %}Index{% endblock %}

{% block head %}
<style>
</style>
{% 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
7 changes: 7 additions & 0 deletions testing/templates/embed_base.html
Original file line number Diff line number Diff line change
@@ -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 %}
<h1>Hello {{ user.name }}</h1>
{% endblock %}
7 changes: 7 additions & 0 deletions testing/templates/embed_base_base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{# The base template which is going to be embedded. Could be a responsive container for example #}
<div>
{% block title %}
<h1>Hello anonymous</h1>
{% endblock %}
{% block content %}{% endblock %}
</div>
7 changes: 7 additions & 0 deletions testing/templates/embed_parent.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{# Base template which gets rendered used for the struct, embedding another template #}

<body>
{% embed "embed_base.html" %}
{% block content %}<p>Welcome to this example!</p>{% endblock %}
{% endembed %}
</body>
43 changes: 43 additions & 0 deletions testing/tests/embed.rs
Original file line number Diff line number Diff line change
@@ -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::<Vec<_>>()
.join(" ")
.trim_end()
.trim_start()
.to_string()
}

#[test]
fn test_embed() {
let expected = strip_whitespaces(
r#"
<body>
<div>
<h1>Hello Yannik</h1>
<p>Welcome to this example!</p>
</div>
</body>"#,
);
let template = EmbedTemplate {
user: FakeUser {
name: String::from("Yannik"),
},
};
let rendered = strip_whitespaces(&template.render().unwrap());

assert_eq!(rendered, expected);
}