Skip to content

Commit 506d88d

Browse files
committed
Add conditional expressions
Jinja, being based on Python, allows using conditional expressions: ```jinja {{ "then" if condition else "otherwise" }} ``` will print `"then"` if `condition` is truthy, otherwise `"otherwise"`. This PR adds the same syntax, with a few restrictions, to Askama: * The condition must evaluate to a `bool` (or a reference to it), same as in `{% if .. %}`. * The else-case can be absent in Jinja, then behaves like `else None`. For Jinja that makes sense. It renders `{{ None }}` like an empty string. Askama does not do that: `Option<T>` can not be rendered for any `T`; we don't unwrap automatically. So, without automatic unwrapping, I don't see a case when you would want to omit the else-case.
1 parent 2b76a1e commit 506d88d

File tree

7 files changed

+348
-5
lines changed

7 files changed

+348
-5
lines changed

askama_derive/src/generator.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,19 @@ enum DisplayWrap {
700700
Unwrapped,
701701
}
702702

703+
/// `Wrapped & Wrapped == Wrapped`, otherwise `Unwrapped`
704+
impl std::ops::BitAnd for DisplayWrap {
705+
type Output = Self;
706+
707+
#[inline]
708+
fn bitand(self, rhs: Self) -> Self::Output {
709+
match self {
710+
DisplayWrap::Wrapped => rhs,
711+
DisplayWrap::Unwrapped => self,
712+
}
713+
}
714+
}
715+
703716
#[derive(Default, Debug)]
704717
struct WritableBuffer<'a> {
705718
buf: Vec<Writable<'a>>,

askama_derive/src/generator/expr.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::borrow::Cow;
22

3+
use parser::expr::Conditional;
34
use parser::node::CondTest;
45
use parser::{
56
AssociatedItem, CharLit, CharPrefix, Expr, PathComponent, Span, StrLit, Target, TyGenerics,
@@ -12,6 +13,7 @@ use super::{
1213
normalize_identifier,
1314
};
1415
use crate::CompileError;
16+
use crate::generator::node::EvaluatedResult;
1517
use crate::heritage::Context;
1618
use crate::integration::Buffer;
1719

@@ -95,6 +97,7 @@ impl<'a> Generator<'a, '_> {
9597
Expr::Concat(ref exprs) => self.visit_concat(ctx, buf, exprs)?,
9698
Expr::LetCond(ref cond) => self.visit_let_cond(ctx, buf, cond)?,
9799
Expr::ArgumentPlaceholder => DisplayWrap::Unwrapped,
100+
Expr::Conditional(ref cond) => self.visit_conditional_expr(ctx, buf, cond)?,
98101
})
99102
}
100103

@@ -230,6 +233,35 @@ impl<'a> Generator<'a, '_> {
230233
}
231234
}
232235

236+
fn visit_conditional_expr(
237+
&mut self,
238+
ctx: &Context<'_>,
239+
buf: &mut Buffer,
240+
cond: &Conditional<'a>,
241+
) -> Result<DisplayWrap, CompileError> {
242+
let expr;
243+
let expr = if cond.test.contains_bool_lit_or_is_defined() {
244+
let result;
245+
(result, expr) = self.evaluate_condition(WithSpan::clone(&cond.test), &mut true);
246+
match result {
247+
EvaluatedResult::AlwaysTrue => return self.visit_expr(ctx, buf, &cond.then),
248+
EvaluatedResult::AlwaysFalse => return self.visit_expr(ctx, buf, &cond.otherwise),
249+
EvaluatedResult::Unknown => &expr,
250+
}
251+
} else {
252+
&cond.test
253+
};
254+
255+
buf.write("if ");
256+
self.visit_condition(ctx, buf, expr)?;
257+
buf.write('{');
258+
let then = self.visit_expr(ctx, buf, &cond.then)?;
259+
buf.write("} else {");
260+
let otherwise = self.visit_expr(ctx, buf, &cond.otherwise)?;
261+
buf.write('}');
262+
Ok(then & otherwise)
263+
}
264+
233265
fn visit_let_cond(
234266
&mut self,
235267
ctx: &Context<'_>,

askama_derive/src/generator/node.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ impl<'a> Generator<'a, '_> {
180180
Ok(size_hint)
181181
}
182182

183-
fn evaluate_condition(
183+
pub(super) fn evaluate_condition(
184184
&self,
185185
expr: WithSpan<'a, Expr<'a>>,
186186
only_contains_is_defined: &mut bool,
@@ -340,6 +340,16 @@ impl<'a> Generator<'a, '_> {
340340
)
341341
}
342342
}
343+
Expr::Conditional(cond) => {
344+
// If we can tell the outcome of the condition, recurse.
345+
let (result, expr) = self.evaluate_condition(*cond.test, only_contains_is_defined);
346+
let expr = match result {
347+
EvaluatedResult::AlwaysTrue => cond.then,
348+
EvaluatedResult::AlwaysFalse => cond.otherwise,
349+
EvaluatedResult::Unknown => return (EvaluatedResult::Unknown, expr),
350+
};
351+
self.evaluate_condition(*expr, only_contains_is_defined)
352+
}
343353
}
344354
}
345355

@@ -1370,7 +1380,7 @@ struct Conds<'a> {
13701380
}
13711381

13721382
#[derive(Clone, Copy, PartialEq, Debug)]
1373-
enum EvaluatedResult {
1383+
pub(super) enum EvaluatedResult {
13741384
AlwaysTrue,
13751385
AlwaysFalse,
13761386
Unknown,
@@ -1527,6 +1537,9 @@ fn is_cacheable(expr: &WithSpan<'_, Expr<'_>>) -> bool {
15271537
Expr::As(expr, _) => is_cacheable(expr),
15281538
Expr::Try(expr) => is_cacheable(expr),
15291539
Expr::Concat(args) => args.iter().all(is_cacheable),
1540+
Expr::Conditional(cond) => {
1541+
is_cacheable(&cond.test) && is_cacheable(&cond.then) && is_cacheable(&cond.otherwise)
1542+
}
15301543
// Doesn't make sense in this context.
15311544
Expr::LetCond(_) => false,
15321545
// We have too little information to tell if the expression is pure:

askama_derive/src/tests.rs

Lines changed: 120 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
//! Files containing tests for generated code.
22
3-
use std::fmt;
3+
use std::io::Write;
44
use std::path::Path;
5+
use std::process::exit;
6+
use std::{fmt, io};
57

68
use console::style;
79
use prettyplease::unparse;
@@ -121,7 +123,8 @@ fn compare_ex(
121123
}
122124
}
123125

124-
panic!(
126+
let _: io::Result<()> = writeln!(
127+
io::stderr().lock(),
125128
"\n\
126129
=== Expected ===\n\
127130
\n\
@@ -140,6 +143,7 @@ fn compare_ex(
140143
generated = style(&generated).green(),
141144
diff = Diff(&expected, &generated),
142145
);
146+
exit(1);
143147
}
144148
}
145149

@@ -1410,3 +1414,117 @@ fn test_bare_cr_doc_comment() -> Result<(), syn::Error> {
14101414

14111415
Ok(())
14121416
}
1417+
1418+
#[test]
1419+
fn test_conditional_expr() {
1420+
compare(
1421+
r"{{ a if b else c }}",
1422+
r#"
1423+
match (
1424+
&((&&askama::filters::AutoEscaper::new(
1425+
&(if askama::helpers::as_bool(&(self.b)) { self.a } else { self.c }),
1426+
askama::filters::Text,
1427+
))
1428+
.askama_auto_escape()?),
1429+
) {
1430+
(expr0,) => {
1431+
(&&&askama::filters::Writable(expr0))
1432+
.askama_write(__askama_writer, __askama_values)?;
1433+
}
1434+
}"#,
1435+
&[("a", "i8"), ("b", "i8"), ("c", "i8")],
1436+
3,
1437+
);
1438+
1439+
compare(
1440+
r"{{ a if b is defined else c }}",
1441+
r#"
1442+
match (
1443+
&((&&askama::filters::AutoEscaper::new(&(self.a), askama::filters::Text))
1444+
.askama_auto_escape()?),
1445+
) {
1446+
(expr0,) => {
1447+
(&&&askama::filters::Writable(expr0))
1448+
.askama_write(__askama_writer, __askama_values)?;
1449+
}
1450+
}"#,
1451+
&[("a", "i8"), ("b", "i8"), ("c", "i8")],
1452+
3,
1453+
);
1454+
1455+
compare(
1456+
r"{{ a if b is not defined else c }}",
1457+
r#"
1458+
match (
1459+
&((&&askama::filters::AutoEscaper::new(&(self.c), askama::filters::Text))
1460+
.askama_auto_escape()?),
1461+
) {
1462+
(expr0,) => {
1463+
(&&&askama::filters::Writable(expr0))
1464+
.askama_write(__askama_writer, __askama_values)?;
1465+
}
1466+
}"#,
1467+
&[("a", "i8"), ("b", "i8"), ("c", "i8")],
1468+
3,
1469+
);
1470+
1471+
compare(
1472+
r"{{ a if b if c else d else e }}",
1473+
r#"
1474+
match (
1475+
&((&&askama::filters::AutoEscaper::new(
1476+
&(if askama::helpers::as_bool(
1477+
&(if askama::helpers::as_bool(&(self.c)) { self.b } else { self.d })
1478+
) {
1479+
self.a
1480+
} else {
1481+
self.e
1482+
}),
1483+
askama::filters::Text,
1484+
))
1485+
.askama_auto_escape()?),
1486+
) {
1487+
(expr0,) => {
1488+
(&&&askama::filters::Writable(expr0))
1489+
.askama_write(__askama_writer, __askama_values)?;
1490+
}
1491+
}"#,
1492+
&[
1493+
("a", "i8"),
1494+
("b", "i8"),
1495+
("c", "i8"),
1496+
("d", "i8"),
1497+
("e", "i8"),
1498+
],
1499+
3,
1500+
);
1501+
1502+
compare(
1503+
r"{{ a if b else c if d else e }}",
1504+
r#"
1505+
match (
1506+
&((&&askama::filters::AutoEscaper::new(
1507+
&(if askama::helpers::as_bool(&(self.b)) {
1508+
self.a
1509+
} else {
1510+
if askama::helpers::as_bool(&(self.d)) { self.c } else { self.e }
1511+
}),
1512+
askama::filters::Text,
1513+
))
1514+
.askama_auto_escape()?),
1515+
) {
1516+
(expr0,) => {
1517+
(&&&askama::filters::Writable(expr0))
1518+
.askama_write(__askama_writer, __askama_values)?;
1519+
}
1520+
}"#,
1521+
&[
1522+
("a", "i8"),
1523+
("b", "i8"),
1524+
("c", "i8"),
1525+
("d", "i8"),
1526+
("e", "i8"),
1527+
],
1528+
3,
1529+
);
1530+
}

askama_parser/src/expr.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,11 @@ fn check_expr<'a>(expr: &WithSpan<'a, Expr<'a>>, allowed: Allowed) -> ParseResul
140140
}
141141
Expr::LetCond(cond) => check_expr(&cond.expr, Allowed::default()),
142142
Expr::ArgumentPlaceholder => cut_error!("unreachable", expr.span),
143+
Expr::Conditional(cond) => {
144+
check_expr(&cond.then, Allowed::default())?;
145+
check_expr(&cond.test, Allowed::default())?;
146+
check_expr(&cond.otherwise, Allowed::default())
147+
}
143148
Expr::BoolLit(_)
144149
| Expr::NumLit(_, _)
145150
| Expr::StrLit(_)
@@ -225,6 +230,14 @@ pub enum Expr<'a> {
225230
/// This variant should never be used directly.
226231
/// It is used for the handling of named arguments in the generator, esp. with filters.
227232
ArgumentPlaceholder,
233+
Conditional(Conditional<'a>),
234+
}
235+
236+
#[derive(Clone, Debug, PartialEq)]
237+
pub struct Conditional<'a> {
238+
pub then: Box<WithSpan<'a, Expr<'a>>>,
239+
pub test: Box<WithSpan<'a, Expr<'a>>>,
240+
pub otherwise: Box<WithSpan<'a, Expr<'a>>>,
228241
}
229242

230243
#[derive(Clone, Debug, PartialEq)]
@@ -316,6 +329,84 @@ impl<'a> Expr<'a> {
316329
allow_underscore: bool,
317330
) -> ParseResult<'a, WithSpan<'a, Self>> {
318331
let _level_guard = level.nest(i)?;
332+
Self::if_else(i, level, allow_underscore)
333+
}
334+
335+
/// Like [`Expr::parse()`], but does not parse conditional expressions,
336+
/// i.e. the token `if` is not consumed.
337+
pub(super) fn parse_no_if_else(
338+
i: &mut &'a str,
339+
level: Level<'_>,
340+
allow_underscore: bool,
341+
) -> ParseResult<'a, WithSpan<'a, Self>> {
342+
let _level_guard = level.nest(i)?;
343+
Self::range(i, level, allow_underscore)
344+
}
345+
346+
fn if_else(
347+
i: &mut &'a str,
348+
level: Level<'_>,
349+
allow_underscore: bool,
350+
) -> ParseResult<'a, WithSpan<'a, Self>> {
351+
#[inline(never)] // very unlikely case
352+
fn actually_cond<'a>(
353+
i: &mut &'a str,
354+
level: Level<'_>,
355+
allow_underscore: bool,
356+
then: Box<WithSpan<'a, Expr<'a>>>,
357+
start: &'a str,
358+
if_span: &'a str,
359+
) -> ParseResult<'a, WithSpan<'a, Expr<'a>>> {
360+
let Some(test) =
361+
opt(|i: &mut _| Expr::parse(i, level, allow_underscore)).parse_next(i)?
362+
else {
363+
return cut_error!(
364+
"conditional expression (`then if cond else otherwise`) expects an expression \
365+
after the keyword `if`",
366+
if_span,
367+
);
368+
};
369+
370+
let Some(else_span) = opt(ws(keyword("else")).take()).parse_next(i)? else {
371+
return cut_error!(
372+
"conditional expression (`then if cond else otherwise`) expects the keyword \
373+
`else` after its condition",
374+
test.span(),
375+
);
376+
};
377+
378+
let Some(otherwise) =
379+
opt(|i: &mut _| Expr::if_else(i, level, allow_underscore)).parse_next(i)?
380+
else {
381+
return cut_error!(
382+
"conditional expression (`then if cond else otherwise`) expects an expression \
383+
after the keyword `else`",
384+
else_span,
385+
);
386+
};
387+
388+
let expr = Expr::Conditional(Conditional {
389+
test: Box::new(test),
390+
then,
391+
otherwise: Box::new(otherwise),
392+
});
393+
Ok(WithSpan::new(expr, start, i))
394+
}
395+
396+
let start = *i;
397+
let expr = Self::range(i, level, allow_underscore)?;
398+
if let Some(if_span) = opt(ws(keyword("if")).take()).parse_next(i)? {
399+
actually_cond(i, level, allow_underscore, expr.into(), start, if_span)
400+
} else {
401+
Ok(expr)
402+
}
403+
}
404+
405+
fn range(
406+
i: &mut &'a str,
407+
level: Level<'_>,
408+
allow_underscore: bool,
409+
) -> ParseResult<'a, WithSpan<'a, Self>> {
319410
let range_right = move |i: &mut _| {
320411
(
321412
ws(alt(("..=", ".."))),
@@ -634,6 +725,7 @@ impl<'a> Expr<'a> {
634725
Self::BinOp(v) if matches!(v.op, "&&" | "||") => {
635726
v.lhs.contains_bool_lit_or_is_defined() || v.rhs.contains_bool_lit_or_is_defined()
636727
}
728+
Self::Conditional(cond) => cond.test.contains_bool_lit_or_is_defined(),
637729
Self::NumLit(_, _)
638730
| Self::StrLit(_)
639731
| Self::CharLit(_)

askama_parser/src/node.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -575,7 +575,7 @@ impl<'a> Loop<'a> {
575575
cut_node(
576576
Some("for"),
577577
(
578-
ws(|i: &mut _| Expr::parse(i, s.level, true)),
578+
ws(|i: &mut _| Expr::parse_no_if_else(i, s.level, true)),
579579
opt(if_cond),
580580
opt(Whitespace::parse),
581581
|i: &mut _| s.tag_block_end(i),

0 commit comments

Comments
 (0)