Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Add embed node #888

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
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
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
}
_ => {}
}
}
Expand Down Expand Up @@ -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)?;
Expand Down Expand Up @@ -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) => {
Expand Down
44 changes: 44 additions & 0 deletions askama_parser/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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,
Expand Down
50 changes: 50 additions & 0 deletions book/src/template_syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
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);
}