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

Add {% break %} and {% continue %} #519

Merged
merged 2 commits into from
Aug 30, 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
10 changes: 10 additions & 0 deletions askama_shared/src/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,16 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> {
// No whitespace handling: child template top-level is not used,
// except for the blocks defined in it.
}
Node::Break(ws) => {
self.handle_ws(ws);
self.write_buf_writable(buf)?;
buf.writeln("break;")?;
}
Node::Continue(ws) => {
self.handle_ws(ws);
self.write_buf_writable(buf)?;
buf.writeln("continue;")?;
}
}
}

Expand Down
122 changes: 84 additions & 38 deletions askama_shared/src/parser.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
use std::cell::Cell;
use std::str;

use nom::branch::alt;
use nom::bytes::complete::{escaped, is_not, tag, take_until};
use nom::character::complete::{anychar, char, digit1};
Expand All @@ -6,7 +9,6 @@ use nom::error::{Error, ParseError};
use nom::multi::{fold_many0, many0, many1, separated_list0, separated_list1};
use nom::sequence::{delimited, pair, preceded, terminated, tuple};
use nom::{self, error_position, Compare, IResult, InputLength, InputTake};
use std::str;

use crate::{CompileError, Syntax};

Expand All @@ -27,6 +29,8 @@ pub enum Node<'a> {
Import(Ws, &'a str, &'a str),
Macro(&'a str, Macro<'a>),
Raw(Ws, &'a str, Ws),
Break(Ws),
Continue(Ws),
}

#[derive(Debug, PartialEq)]
Expand Down Expand Up @@ -178,14 +182,19 @@ enum ContentState {
End(usize),
}

fn take_content<'a>(i: &'a [u8], s: &'a Syntax<'a>) -> ParserError<'a, Node<'a>> {
struct State<'a> {
syntax: &'a Syntax<'a>,
loop_depth: Cell<usize>,
}

fn take_content<'a>(i: &'a [u8], s: &State<'_>) -> ParserError<'a, Node<'a>> {
use crate::parser::ContentState::*;
let bs = s.block_start.as_bytes()[0];
let be = s.block_start.as_bytes()[1];
let cs = s.comment_start.as_bytes()[0];
let ce = s.comment_start.as_bytes()[1];
let es = s.expr_start.as_bytes()[0];
let ee = s.expr_start.as_bytes()[1];
let bs = s.syntax.block_start.as_bytes()[0];
let be = s.syntax.block_start.as_bytes()[1];
let cs = s.syntax.comment_start.as_bytes()[0];
let ce = s.syntax.comment_start.as_bytes()[1];
let es = s.syntax.expr_start.as_bytes()[0];
let ee = s.syntax.expr_start.as_bytes()[1];

let mut state = Start;
for (idx, c) in i.iter().enumerate() {
Expand Down Expand Up @@ -683,7 +692,7 @@ fn expr_any(i: &[u8]) -> IResult<&[u8], Expr<'_>> {
alt((range_right, compound, expr_or))(i)
}

fn expr_node<'a>(i: &'a [u8], s: &'a Syntax<'a>) -> IResult<&'a [u8], Node<'a>> {
fn expr_node<'a>(i: &'a [u8], s: &State<'_>) -> IResult<&'a [u8], Node<'a>> {
let mut p = tuple((
|i| tag_expr_start(i, s),
cut(tuple((opt(tag("-")), ws(expr_any), opt(tag("-")), |i| {
Expand Down Expand Up @@ -729,7 +738,7 @@ fn cond_if(i: &[u8]) -> IResult<&[u8], CondTest<'_>> {
Ok((i, CondTest { target, expr }))
}

fn cond_block<'a>(i: &'a [u8], s: &'a Syntax<'a>) -> IResult<&'a [u8], Cond<'a>> {
fn cond_block<'a>(i: &'a [u8], s: &State<'_>) -> IResult<&'a [u8], Cond<'a>> {
let mut p = tuple((
|i| tag_block_start(i, s),
opt(tag("-")),
Expand All @@ -745,7 +754,7 @@ fn cond_block<'a>(i: &'a [u8], s: &'a Syntax<'a>) -> IResult<&'a [u8], Cond<'a>>
Ok((i, (Ws(pws.is_some(), nws.is_some()), cond, block)))
}

fn block_if<'a>(i: &'a [u8], s: &'a Syntax<'a>) -> IResult<&'a [u8], Node<'a>> {
fn block_if<'a>(i: &'a [u8], s: &State<'_>) -> IResult<&'a [u8], Node<'a>> {
let mut p = tuple((
opt(tag("-")),
cond_if,
Expand All @@ -771,7 +780,7 @@ fn block_if<'a>(i: &'a [u8], s: &'a Syntax<'a>) -> IResult<&'a [u8], Node<'a>> {
Ok((i, Node::Cond(res, Ws(pws2.is_some(), nws2.is_some()))))
}

fn match_else_block<'a>(i: &'a [u8], s: &'a Syntax<'a>) -> IResult<&'a [u8], When<'a>> {
fn match_else_block<'a>(i: &'a [u8], s: &State<'_>) -> IResult<&'a [u8], When<'a>> {
let mut p = tuple((
|i| tag_block_start(i, s),
opt(tag("-")),
Expand All @@ -789,7 +798,7 @@ fn match_else_block<'a>(i: &'a [u8], s: &'a Syntax<'a>) -> IResult<&'a [u8], Whe
))
}

fn when_block<'a>(i: &'a [u8], s: &'a Syntax<'a>) -> IResult<&'a [u8], When<'a>> {
fn when_block<'a>(i: &'a [u8], s: &State<'_>) -> IResult<&'a [u8], When<'a>> {
let mut p = tuple((
|i| tag_block_start(i, s),
opt(tag("-")),
Expand All @@ -805,7 +814,7 @@ fn when_block<'a>(i: &'a [u8], s: &'a Syntax<'a>) -> IResult<&'a [u8], When<'a>>
Ok((i, (Ws(pws.is_some(), nws.is_some()), target, block)))
}

fn block_match<'a>(i: &'a [u8], s: &'a Syntax<'a>) -> IResult<&'a [u8], Node<'a>> {
fn block_match<'a>(i: &'a [u8], s: &State<'_>) -> IResult<&'a [u8], Node<'a>> {
let mut p = tuple((
opt(tag("-")),
ws(tag("match")),
Expand Down Expand Up @@ -885,7 +894,14 @@ fn block_let(i: &[u8]) -> IResult<&[u8], Node<'_>> {
))
}

fn block_for<'a>(i: &'a [u8], s: &'a Syntax<'a>) -> IResult<&'a [u8], Node<'a>> {
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)?;
s.loop_depth.set(s.loop_depth.get() - 1);
Ok((i, node))
}

fn block_for<'a>(i: &'a [u8], s: &State<'_>) -> IResult<&'a [u8], Node<'a>> {
let mut p = tuple((
opt(tag("-")),
ws(tag("for")),
Expand All @@ -897,7 +913,7 @@ fn block_for<'a>(i: &'a [u8], s: &'a Syntax<'a>) -> IResult<&'a [u8], Node<'a>>
opt(tag("-")),
|i| tag_block_end(i, s),
cut(tuple((
|i| parse_template(i, s),
|i| parse_loop_content(i, s),
cut(tuple((
|i| tag_block_start(i, s),
opt(tag("-")),
Expand Down Expand Up @@ -926,7 +942,7 @@ fn block_extends(i: &[u8]) -> IResult<&[u8], Node<'_>> {
Ok((i, Node::Extends(name)))
}

fn block_block<'a>(i: &'a [u8], s: &'a Syntax<'a>) -> IResult<&'a [u8], Node<'a>> {
fn block_block<'a>(i: &'a [u8], s: &State<'_>) -> IResult<&'a [u8], Node<'a>> {
let mut start = tuple((
opt(tag("-")),
ws(tag("block")),
Expand Down Expand Up @@ -1001,7 +1017,7 @@ fn block_import(i: &[u8]) -> IResult<&[u8], Node<'_>> {
))
}

fn block_macro<'a>(i: &'a [u8], s: &'a Syntax<'a>) -> IResult<&'a [u8], Node<'a>> {
fn block_macro<'a>(i: &'a [u8], s: &State<'_>) -> IResult<&'a [u8], Node<'a>> {
let mut p = tuple((
opt(tag("-")),
ws(tag("macro")),
Expand Down Expand Up @@ -1041,7 +1057,7 @@ fn block_macro<'a>(i: &'a [u8], s: &'a Syntax<'a>) -> IResult<&'a [u8], Node<'a>
))
}

fn block_raw<'a>(i: &'a [u8], s: &'a Syntax<'a>) -> IResult<&'a [u8], Node<'a>> {
fn block_raw<'a>(i: &'a [u8], s: &State<'_>) -> IResult<&'a [u8], Node<'a>> {
let mut p = tuple((
opt(tag("-")),
ws(tag("raw")),
Expand All @@ -1068,7 +1084,31 @@ fn block_raw<'a>(i: &'a [u8], s: &'a Syntax<'a>) -> IResult<&'a [u8], Node<'a>>
))
}

fn block_node<'a>(i: &'a [u8], s: &'a Syntax<'a>) -> IResult<&'a [u8], Node<'a>> {
fn break_statement<'a>(i: &'a [u8], s: &State<'_>) -> IResult<&'a [u8], Node<'a>> {
let mut p = tuple((opt(tag("-")), ws(tag("break")), opt(tag("-"))));
let (j, (pws, _, nws)) = p(i)?;
if s.loop_depth.get() == 0 {
return Err(nom::Err::Failure(error_position!(
i,
nom::error::ErrorKind::Tag
)));
}
Ok((j, Node::Break(Ws(pws.is_some(), nws.is_some()))))
}

fn continue_statement<'a>(i: &'a [u8], s: &State<'_>) -> IResult<&'a [u8], Node<'a>> {
let mut p = tuple((opt(tag("-")), ws(tag("continue")), opt(tag("-"))));
let (j, (pws, _, nws)) = p(i)?;
if s.loop_depth.get() == 0 {
return Err(nom::Err::Failure(error_position!(
i,
nom::error::ErrorKind::Tag
)));
}
Ok((j, Node::Continue(Ws(pws.is_some(), nws.is_some()))))
}

fn block_node<'a>(i: &'a [u8], s: &State<'_>) -> IResult<&'a [u8], Node<'a>> {
let mut p = tuple((
|i| tag_block_start(i, s),
alt((
Expand All @@ -1083,18 +1123,20 @@ fn block_node<'a>(i: &'a [u8], s: &'a Syntax<'a>) -> IResult<&'a [u8], Node<'a>>
|i| block_block(i, s),
|i| block_macro(i, s),
|i| block_raw(i, s),
|i| break_statement(i, s),
|i| continue_statement(i, s),
)),
cut(|i| tag_block_end(i, s)),
));
let (i, (_, contents, _)) = p(i)?;
Ok((i, contents))
}

fn block_comment_body<'a>(mut i: &'a [u8], s: &'a Syntax<'a>) -> IResult<&'a [u8], &'a [u8]> {
fn block_comment_body<'a>(mut i: &'a [u8], s: &State<'_>) -> IResult<&'a [u8], &'a [u8]> {
let mut level = 0;
loop {
let (end, tail) = take_until(s.comment_end)(i)?;
match take_until::<_, _, Error<_>>(s.comment_start)(i) {
let (end, tail) = take_until(s.syntax.comment_end)(i)?;
match take_until::<_, _, Error<_>>(s.syntax.comment_start)(i) {
Ok((start, _)) if start.as_ptr() < end.as_ptr() => {
level += 1;
i = &start[2..];
Expand All @@ -1108,7 +1150,7 @@ fn block_comment_body<'a>(mut i: &'a [u8], s: &'a Syntax<'a>) -> IResult<&'a [u8
}
}

fn block_comment<'a>(i: &'a [u8], s: &'a Syntax<'a>) -> IResult<&'a [u8], Node<'a>> {
fn block_comment<'a>(i: &'a [u8], s: &State<'_>) -> IResult<&'a [u8], Node<'a>> {
let mut p = tuple((
|i| tag_comment_start(i, s),
cut(tuple((
Expand All @@ -1121,7 +1163,7 @@ fn block_comment<'a>(i: &'a [u8], s: &'a Syntax<'a>) -> IResult<&'a [u8], Node<'
Ok((i, Node::Comment(Ws(pws.is_some(), tail.ends_with(b"-")))))
}

fn parse_template<'a>(i: &'a [u8], s: &'a Syntax<'a>) -> IResult<&'a [u8], Vec<Node<'a>>> {
fn parse_template<'a>(i: &'a [u8], s: &State<'_>) -> IResult<&'a [u8], Vec<Node<'a>>> {
many0(alt((
complete(|i| take_content(i, s)),
complete(|i| block_comment(i, s)),
Expand All @@ -1130,27 +1172,31 @@ fn parse_template<'a>(i: &'a [u8], s: &'a Syntax<'a>) -> IResult<&'a [u8], Vec<N
)))(i)
}

fn tag_block_start<'a>(i: &'a [u8], s: &'a Syntax<'a>) -> IResult<&'a [u8], &'a [u8]> {
tag(s.block_start)(i)
fn tag_block_start<'a>(i: &'a [u8], s: &State<'_>) -> IResult<&'a [u8], &'a [u8]> {
tag(s.syntax.block_start)(i)
}
fn tag_block_end<'a>(i: &'a [u8], s: &'a Syntax<'a>) -> IResult<&'a [u8], &'a [u8]> {
tag(s.block_end)(i)
fn tag_block_end<'a>(i: &'a [u8], s: &State<'_>) -> IResult<&'a [u8], &'a [u8]> {
tag(s.syntax.block_end)(i)
}
fn tag_comment_start<'a>(i: &'a [u8], s: &'a Syntax<'a>) -> IResult<&'a [u8], &'a [u8]> {
tag(s.comment_start)(i)
fn tag_comment_start<'a>(i: &'a [u8], s: &State<'_>) -> IResult<&'a [u8], &'a [u8]> {
tag(s.syntax.comment_start)(i)
}
fn tag_comment_end<'a>(i: &'a [u8], s: &'a Syntax<'a>) -> IResult<&'a [u8], &'a [u8]> {
tag(s.comment_end)(i)
fn tag_comment_end<'a>(i: &'a [u8], s: &State<'_>) -> IResult<&'a [u8], &'a [u8]> {
tag(s.syntax.comment_end)(i)
}
fn tag_expr_start<'a>(i: &'a [u8], s: &'a Syntax<'a>) -> IResult<&'a [u8], &'a [u8]> {
tag(s.expr_start)(i)
fn tag_expr_start<'a>(i: &'a [u8], s: &State<'_>) -> IResult<&'a [u8], &'a [u8]> {
tag(s.syntax.expr_start)(i)
}
fn tag_expr_end<'a>(i: &'a [u8], s: &'a Syntax<'a>) -> IResult<&'a [u8], &'a [u8]> {
tag(s.expr_end)(i)
fn tag_expr_end<'a>(i: &'a [u8], s: &State<'_>) -> IResult<&'a [u8], &'a [u8]> {
tag(s.syntax.expr_end)(i)
}

pub fn parse<'a>(src: &'a str, syntax: &'a Syntax<'a>) -> Result<Vec<Node<'a>>, CompileError> {
match parse_template(src.as_bytes(), syntax) {
let state = State {
syntax,
loop_depth: Cell::new(0),
};
match parse_template(src.as_bytes(), &state) {
Ok((left, res)) => {
if !left.is_empty() {
let s = str::from_utf8(left).unwrap();
Expand Down
11 changes: 11 additions & 0 deletions testing/templates/for-break-continue.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{%- for v in values -%}
x {{- v -}}
{%- if matches!(v, x if *x > 9) -%}
{%- if matches!(v, x if *x % 2 == 0) -%}
{%- break -%}
{%- else -%}
{%- continue -%}
{%- endif -%}
{%- endif -%}
y
{%- endfor -%}
68 changes: 68 additions & 0 deletions testing/tests/loops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,3 +304,71 @@ fn test_for_enumerate() {
};
assert_eq!(t.render().unwrap(), "0=hello-1=world-2=!-");
}

#[derive(Template)]
#[template(
source = "{% for v in values.iter() %}x{{v}}{% if matches!(v, x if *x==3) %}{% break %}{% endif %}y{% endfor %}",
ext = "txt"
)]
struct Break<'a> {
values: &'a [i32],
}

#[test]
fn test_loop_break() {
let t = Break {
values: &[1, 2, 3, 4, 5],
};
assert_eq!(t.render().unwrap(), "x1yx2yx3");

let t = Break {
values: &[1, 2, 4, 5],
};
assert_eq!(t.render().unwrap(), "x1yx2yx4yx5y");
}

#[derive(Template)]
#[template(
source = "{% for v in values %}x{{v}}{% if matches!(v, x if *x==3) %}{% continue %}{% endif %}y{% endfor %}",
ext = "txt"
)]
struct Continue<'a> {
values: &'a [i32],
}

#[test]
fn test_loop_continue() {
let t = Continue {
values: &[1, 2, 3, 4, 5],
};
assert_eq!(t.render().unwrap(), "x1yx2yx3x4yx5y");

let t = Continue {
values: &[1, 2, 4, 5],
};
assert_eq!(t.render().unwrap(), "x1yx2yx4yx5y");
}

#[derive(Template)]
#[template(path = "for-break-continue.html")]
struct BreakContinue<'a> {
values: &'a [i32],
}

#[test]
fn test_loop_break_continue() {
let t = BreakContinue {
values: &[1, 2, 3, 4, 5],
};
assert_eq!(t.render().unwrap(), "x1yx2yx3yx4yx5y");

let t = BreakContinue {
values: &[1, 2, 3, 10, 4, 5],
};
assert_eq!(t.render().unwrap(), "x1yx2yx3yx10");

let t = BreakContinue {
values: &[1, 2, 3, 11, 4, 5],
};
assert_eq!(t.render().unwrap(), "x1yx2yx3yx11x4yx5y");
}
11 changes: 11 additions & 0 deletions testing/tests/ui/break_outside_of_loop.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use askama::Template;

#[derive(Template)]
#[template(
source = "Have a {%break%}, have a parsing error!",
ext = "txt"
)]
struct MyTemplate;

fn main() {
}
8 changes: 8 additions & 0 deletions testing/tests/ui/break_outside_of_loop.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
error: problems parsing template source at row 1, column 9 near:
"break%}, have a parsing error!"
--> $DIR/break_outside_of_loop.rs:3:10
|
3 | #[derive(Template)]
| ^^^^^^^^
|
= note: this error originates in the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info)