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

Implement for-else #518

Merged
merged 3 commits into from
Nov 11, 2021
Merged
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
89 changes: 48 additions & 41 deletions askama_shared/src/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use super::{get_template_source, CompileError, Integrations};
use crate::filters;
use crate::heritage::{Context, Heritage};
use crate::input::{Source, TemplateInput};
use crate::parser::{parse, Cond, CondTest, Expr, Node, Target, When, Ws};
use crate::parser::{parse, Cond, CondTest, Expr, Loop, Node, Target, When, Ws};

use proc_macro2::Span;

Expand Down Expand Up @@ -420,8 +420,8 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> {
Node::Match(ws1, ref expr, ref arms, ws2) => {
self.write_match(ctx, buf, ws1, expr, arms, ws2)?;
}
Node::Loop(ws1, ref var, ref iter, ref body, ws2) => {
self.write_loop(ctx, buf, ws1, var, iter, body, ws2)?;
Node::Loop(ref loop_block) => {
self.write_loop(ctx, buf, loop_block)?;
}
Node::BlockDef(ws1, name, _, ws2) => {
self.write_block(buf, Some(name), Ws(ws1.0, ws2.1))?;
Expand Down Expand Up @@ -596,62 +596,69 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> {
&mut self,
ctx: &'a Context<'_>,
buf: &mut Buffer,
ws1: Ws,
var: &'a Target<'_>,
iter: &Expr<'_>,
body: &'a [Node<'_>],
ws2: Ws,
loop_block: &'a Loop<'_>,
) -> Result<usize, CompileError> {
self.handle_ws(ws1);
self.handle_ws(loop_block.ws1);
self.locals.push();

let expr_code = self.visit_expr_root(iter)?;
let expr_code = self.visit_expr_root(&loop_block.iter)?;

let flushed = self.write_buf_writable(buf)?;
buf.write("for (");
self.visit_target(buf, true, true, var);
match iter {
Expr::Range(_, _, _) => buf.writeln(&format!(
", _loop_item) in ::askama::helpers::TemplateLoop::new({}) {{",
expr_code
)),
Expr::Array(..) => buf.writeln(&format!(
", _loop_item) in ::askama::helpers::TemplateLoop::new({}.iter()) {{",
expr_code
)),
buf.writeln("{")?;
buf.writeln("let mut _did_loop = false;")?;
match loop_block.iter {
Expr::Range(_, _, _) => buf.writeln(&format!("let _iter = {};", expr_code)),
Expr::Array(..) => buf.writeln(&format!("let _iter = {}.iter();", expr_code)),
// If `iter` is a call then we assume it's something that returns
// an iterator. If not then the user can explicitly add the needed
// call without issues.
Expr::MethodCall(..) | Expr::PathCall(..) | Expr::Index(..) => buf.writeln(&format!(
", _loop_item) in ::askama::helpers::TemplateLoop::new(({}).into_iter()) {{",
expr_code
)),
Expr::MethodCall(..) | Expr::PathCall(..) | Expr::Index(..) => {
buf.writeln(&format!("let _iter = ({}).into_iter();", expr_code))
}
// If accessing `self` then it most likely needs to be
// borrowed, to prevent an attempt of moving.
_ if expr_code.starts_with("self.") => buf.writeln(&format!(
", _loop_item) in ::askama::helpers::TemplateLoop::new(((&{}).into_iter())) {{",
expr_code
)),
_ if expr_code.starts_with("self.") => {
buf.writeln(&format!("let _iter = (&{}).into_iter();", expr_code))
}
// If accessing a field then it most likely needs to be
// borrowed, to prevent an attempt of moving.
Expr::Attr(..) => buf.writeln(&format!(
", _loop_item) in ::askama::helpers::TemplateLoop::new(((&{}).into_iter())) {{",
expr_code
)),
Expr::Attr(..) => buf.writeln(&format!("let _iter = (&{}).into_iter();", expr_code)),
// Otherwise, we borrow `iter` assuming that it implements `IntoIterator`.
_ => buf.writeln(&format!(
", _loop_item) in ::askama::helpers::TemplateLoop::new(({}).into_iter()) {{",
expr_code
)),
_ => buf.writeln(&format!("let _iter = ({}).into_iter();", expr_code)),
}?;
djc marked this conversation as resolved.
Show resolved Hide resolved
if let Some(cond) = &loop_block.cond {
self.locals.push();
buf.write("let _iter = _iter.filter(|");
self.visit_target(buf, true, true, &loop_block.var);
buf.write("| -> bool {");
self.visit_expr(buf, cond)?;
buf.writeln("});")?;
self.locals.pop();
}

let mut size_hint = self.handle(ctx, body, buf, AstLevel::Nested)?;
self.handle_ws(ws2);
self.locals.push();
buf.write("for (");
self.visit_target(buf, true, true, &loop_block.var);
buf.writeln(", _loop_item) in ::askama::helpers::TemplateLoop::new(_iter) {")?;

size_hint += self.write_buf_writable(buf)?;
buf.writeln("_did_loop = true;")?;
let mut size_hint1 = self.handle(ctx, &loop_block.body, buf, AstLevel::Nested)?;
self.handle_ws(loop_block.ws2);
size_hint1 += self.write_buf_writable(buf)?;
self.locals.pop();
buf.writeln("}")?;

buf.writeln("if !_did_loop {")?;
self.locals.push();
let mut size_hint2 = self.handle(ctx, &loop_block.else_block, buf, AstLevel::Nested)?;
self.handle_ws(loop_block.ws3);
size_hint2 += self.write_buf_writable(buf)?;
self.locals.pop();
Ok(flushed + (size_hint * 3))
buf.writeln("}")?;

buf.writeln("}")?;

Ok(flushed + ((size_hint1 * 3) + size_hint2) / 2)
}

fn write_call(
Expand Down
9 changes: 6 additions & 3 deletions askama_shared/src/heritage.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::collections::HashMap;
use std::path::{Path, PathBuf};

use crate::parser::{Expr, Macro, Node};
use crate::parser::{Expr, Loop, Macro, Node};
use crate::{CompileError, Config};

pub struct Heritage<'a> {
Expand Down Expand Up @@ -86,8 +86,11 @@ impl<'a> Context<'a> {
nested.push(nodes);
}
}
Node::Loop(_, _, _, nodes, _) => {
nested.push(nodes);
Node::Loop(Loop {
body, else_block, ..
}) => {
nested.push(body);
nested.push(else_block);
}
Node::Match(_, _, arms, _) => {
for (_, _, arm) in arms {
Expand Down
54 changes: 45 additions & 9 deletions askama_shared/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub enum Node<'a> {
Let(Ws, Target<'a>, Expr<'a>),
Cond(Vec<Cond<'a>>, Ws),
Match(Ws, Expr<'a>, Vec<When<'a>>, Ws),
Loop(Ws, Target<'a>, Expr<'a>, Vec<Node<'a>>, Ws),
Loop(Loop<'a>),
Extends(Expr<'a>),
BlockDef(Ws, &'a str, Vec<Node<'a>>, Ws),
Include(Ws, &'a str),
Expand All @@ -33,6 +33,18 @@ pub enum Node<'a> {
Continue(Ws),
}

#[derive(Debug, PartialEq)]
pub struct Loop<'a> {
djc marked this conversation as resolved.
Show resolved Hide resolved
pub ws1: Ws,
pub var: Target<'a>,
pub iter: Expr<'a>,
pub cond: Option<Expr<'a>>,
pub body: Vec<Node<'a>>,
pub ws2: Ws,
pub else_block: Vec<Node<'a>>,
pub ws3: Ws,
}

#[derive(Debug, PartialEq)]
pub enum Expr<'a> {
BoolLit(&'a str),
Expand Down Expand Up @@ -897,12 +909,29 @@ fn block_let(i: &[u8]) -> IResult<&[u8], Node<'_>> {

fn parse_loop_content<'a>(i: &'a [u8], s: &State<'_>) -> IResult<&'a [u8], Vec<Node<'a>>> {
s.loop_depth.set(s.loop_depth.get() + 1);
let (i, node) = parse_template(i, s)?;
let result = parse_template(i, s);
s.loop_depth.set(s.loop_depth.get() - 1);
Ok((i, node))
result
}

fn block_for<'a>(i: &'a [u8], s: &State<'_>) -> IResult<&'a [u8], Node<'a>> {
let if_cond = preceded(ws(tag("if")), cut(ws(expr_any)));
let else_block = |i| {
let mut p = preceded(
ws(tag("else")),
cut(tuple((
opt(tag("-")),
delimited(
|i| tag_block_end(i, s),
|i| parse_template(i, s),
|i| tag_block_start(i, s),
),
opt(tag("-")),
))),
);
let (i, (pws, nodes, nws)) = p(i)?;
Ok((i, (pws.is_some(), nodes, nws.is_some())))
};
let mut p = tuple((
opt(char('-')),
ws(tag("for")),
Expand All @@ -911,30 +940,37 @@ fn block_for<'a>(i: &'a [u8], s: &State<'_>) -> IResult<&'a [u8], Node<'a>> {
ws(tag("in")),
cut(tuple((
ws(expr_any),
opt(if_cond),
opt(char('-')),
|i| tag_block_end(i, s),
cut(tuple((
|i| parse_loop_content(i, s),
cut(tuple((
|i| tag_block_start(i, s),
opt(char('-')),
opt(else_block),
ws(tag("endfor")),
opt(char('-')),
))),
))),
))),
))),
));
let (i, (pws1, _, (var, _, (iter, nws1, _, (block, (_, pws2, _, nws2)))))) = p(i)?;
let (i, (pws1, _, (var, _, (iter, cond, nws1, _, (body, (_, pws2, else_block, _, nws2)))))) =
p(i)?;
let (nws3, else_block, pws3) = else_block.unwrap_or_default();
Ok((
i,
Node::Loop(
Ws(pws1.is_some(), nws1.is_some()),
Node::Loop(Loop {
ws1: Ws(pws1.is_some(), nws1.is_some()),
var,
iter,
block,
Ws(pws2.is_some(), nws2.is_some()),
),
cond,
body,
ws2: Ws(pws2.is_some(), nws3),
else_block,
ws3: Ws(pws3, nws2.is_some()),
}),
))
}

Expand Down
58 changes: 58 additions & 0 deletions testing/tests/gen_loop_else.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/usr/bin/env python3

print(r'''use askama::Template;

#[derive(Template)]
#[template(
source = "{% for v in values %}{{v}}{% else %}empty{% endfor %}",
ext = "txt"
)]
struct ForElse<'a> {
values: &'a [i32],
}

#[test]
fn test_for_else() {
let t = ForElse { values: &[1, 2, 3] };
assert_eq!(t.render().unwrap(), "123");

let t = ForElse { values: &[] };
assert_eq!(t.render().unwrap(), "empty");
}
''')

for i in range(2**6):
a = '-' if i & 2**0 else ' '
b = '-' if i & 2**1 else ' '
c = '-' if i & 2**2 else ' '
d = '-' if i & 2**3 else ' '
e = '-' if i & 2**4 else ' '
f = '-' if i & 2**5 else ' '
source = fr'a {{%{a}for v in values{b}%}}\t{{{{v}}}}\t{{%{c}else{d}%}}\nX\n{{%{e}endfor{f}%}} b'

a = '' if i & 2**0 else r' '
b = '' if i & 2**1 else r'\t'
c = '' if i & 2**2 else r'\t'
d = '' if i & 2**3 else r'\n'
e = '' if i & 2**4 else r'\n'
f = '' if i & 2**5 else r' '
some = f'a{a}{b}1{c}{f}b'
none = f'a{a}{d}X{e}{f}b'

print(f'''#[derive(Template)]
#[template(
source = "{source}",
ext = "txt"
)]
struct LoopElseTrim{i:02}<'a> {{
values: &'a [i32],
}}

#[test]
fn test_loop_else_trim{i:02}() {{
let t = LoopElseTrim{i:02} {{ values: &[1] }};
assert_eq!(t.render().unwrap(), "{some}");

let t = LoopElseTrim{i:02} {{ values: &[] }};
assert_eq!(t.render().unwrap(), "{none}");
}}''')
Loading