Skip to content

Commit f278964

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 b8e11e8 commit f278964

File tree

10 files changed

+753
-6
lines changed

10 files changed

+753
-6
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: 31 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,34 @@ 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 result;
243+
let expr = if cond.test.contains_bool_lit_or_is_defined() {
244+
result = self.evaluate_condition(WithSpan::clone(&cond.test), &mut true);
245+
match &result {
246+
EvaluatedResult::AlwaysTrue => return self.visit_expr(ctx, buf, &cond.then),
247+
EvaluatedResult::AlwaysFalse => return self.visit_expr(ctx, buf, &cond.otherwise),
248+
EvaluatedResult::Unknown(expr) => expr,
249+
}
250+
} else {
251+
&cond.test
252+
};
253+
254+
buf.write("if ");
255+
self.visit_condition(ctx, buf, expr)?;
256+
buf.write('{');
257+
let then = self.visit_expr(ctx, buf, &cond.then)?;
258+
buf.write("} else {");
259+
let otherwise = self.visit_expr(ctx, buf, &cond.otherwise)?;
260+
buf.write('}');
261+
Ok(then & otherwise)
262+
}
263+
233264
fn visit_let_cond(
234265
&mut self,
235266
ctx: &Context<'_>,

askama_derive/src/generator/node.rs

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

184-
fn evaluate_condition(
184+
pub(super) fn evaluate_condition(
185185
&self,
186186
expr: WithSpan<'a, Box<Expr<'a>>>,
187187
only_contains_is_defined: &mut bool,
@@ -304,6 +304,15 @@ impl<'a> Generator<'a, '_> {
304304
EvaluatedResult::AlwaysTrue
305305
}
306306
}
307+
Expr::Conditional(cond) => {
308+
// If we can tell the outcome of the condition, recurse.
309+
let expr = match self.evaluate_condition(cond.test, only_contains_is_defined) {
310+
EvaluatedResult::AlwaysTrue => cond.then,
311+
EvaluatedResult::AlwaysFalse => cond.otherwise,
312+
EvaluatedResult::Unknown(expr) => return EvaluatedResult::Unknown(expr),
313+
};
314+
self.evaluate_condition(expr, only_contains_is_defined)
315+
}
307316
}
308317
}
309318

@@ -1363,7 +1372,7 @@ struct Conds<'a> {
13631372
}
13641373

13651374
#[derive(Debug, Clone, PartialEq)]
1366-
enum EvaluatedResult<'a> {
1375+
pub(super) enum EvaluatedResult<'a> {
13671376
AlwaysTrue,
13681377
AlwaysFalse,
13691378
Unknown(WithSpan<'a, Box<Expr<'a>>>),
@@ -1534,6 +1543,9 @@ fn is_cacheable(expr: &WithSpan<'_, Box<Expr<'_>>>) -> bool {
15341543
Expr::As(expr, _) => is_cacheable(expr),
15351544
Expr::Try(expr) => is_cacheable(expr),
15361545
Expr::Concat(args) => args.iter().all(is_cacheable),
1546+
Expr::Conditional(cond) => {
1547+
is_cacheable(&cond.test) && is_cacheable(&cond.then) && is_cacheable(&cond.otherwise)
1548+
}
15371549
// Doesn't make sense in this context.
15381550
Expr::LetCond(_) => false,
15391551
// We have too little information to tell if the expression is pure:

askama_derive/src/tests.rs

Lines changed: 124 additions & 3 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\
@@ -135,11 +138,15 @@ fn compare_ex(
135138
\n\
136139
{diff}\n\
137140
\n\
138-
=== FAILURE ===",
141+
=== FAILURE ===\n\
142+
\n\
143+
{location}\n",
139144
expected = style(&expected).red(),
140145
generated = style(&generated).green(),
141146
diff = Diff(&expected, &generated),
147+
location = std::panic::Location::caller(),
142148
);
149+
exit(1);
143150
}
144151
}
145152

@@ -1441,3 +1448,117 @@ fn check_expr_ungrouping() {
14411448
11,
14421449
);
14431450
}
1451+
1452+
#[test]
1453+
fn test_conditional_expr() {
1454+
compare(
1455+
r"{{ a if b else c }}",
1456+
r#"
1457+
match (
1458+
&((&&askama::filters::AutoEscaper::new(
1459+
&(if askama::helpers::as_bool(&(self.b)) { self.a } else { self.c }),
1460+
askama::filters::Text,
1461+
))
1462+
.askama_auto_escape()?),
1463+
) {
1464+
(expr0,) => {
1465+
(&&&askama::filters::Writable(expr0))
1466+
.askama_write(__askama_writer, __askama_values)?;
1467+
}
1468+
}"#,
1469+
&[("a", "i8"), ("b", "i8"), ("c", "i8")],
1470+
3,
1471+
);
1472+
1473+
compare(
1474+
r"{{ a if b is defined else c }}",
1475+
r#"
1476+
match (
1477+
&((&&askama::filters::AutoEscaper::new(&(self.a), askama::filters::Text))
1478+
.askama_auto_escape()?),
1479+
) {
1480+
(expr0,) => {
1481+
(&&&askama::filters::Writable(expr0))
1482+
.askama_write(__askama_writer, __askama_values)?;
1483+
}
1484+
}"#,
1485+
&[("a", "i8"), ("b", "i8"), ("c", "i8")],
1486+
3,
1487+
);
1488+
1489+
compare(
1490+
r"{{ a if b is not defined else c }}",
1491+
r#"
1492+
match (
1493+
&((&&askama::filters::AutoEscaper::new(&(self.c), askama::filters::Text))
1494+
.askama_auto_escape()?),
1495+
) {
1496+
(expr0,) => {
1497+
(&&&askama::filters::Writable(expr0))
1498+
.askama_write(__askama_writer, __askama_values)?;
1499+
}
1500+
}"#,
1501+
&[("a", "i8"), ("b", "i8"), ("c", "i8")],
1502+
3,
1503+
);
1504+
1505+
compare(
1506+
r"{{ a if b if c else d else e }}",
1507+
r#"
1508+
match (
1509+
&((&&askama::filters::AutoEscaper::new(
1510+
&(if askama::helpers::as_bool(
1511+
&(if askama::helpers::as_bool(&(self.c)) { self.b } else { self.d })
1512+
) {
1513+
self.a
1514+
} else {
1515+
self.e
1516+
}),
1517+
askama::filters::Text,
1518+
))
1519+
.askama_auto_escape()?),
1520+
) {
1521+
(expr0,) => {
1522+
(&&&askama::filters::Writable(expr0))
1523+
.askama_write(__askama_writer, __askama_values)?;
1524+
}
1525+
}"#,
1526+
&[
1527+
("a", "i8"),
1528+
("b", "i8"),
1529+
("c", "i8"),
1530+
("d", "i8"),
1531+
("e", "i8"),
1532+
],
1533+
3,
1534+
);
1535+
1536+
compare(
1537+
r"{{ a if b else c if d else e }}",
1538+
r#"
1539+
match (
1540+
&((&&askama::filters::AutoEscaper::new(
1541+
&(if askama::helpers::as_bool(&(self.b)) {
1542+
self.a
1543+
} else {
1544+
if askama::helpers::as_bool(&(self.d)) { self.c } else { self.e }
1545+
}),
1546+
askama::filters::Text,
1547+
))
1548+
.askama_auto_escape()?),
1549+
) {
1550+
(expr0,) => {
1551+
(&&&askama::filters::Writable(expr0))
1552+
.askama_write(__askama_writer, __askama_values)?;
1553+
}
1554+
}"#,
1555+
&[
1556+
("a", "i8"),
1557+
("b", "i8"),
1558+
("c", "i8"),
1559+
("d", "i8"),
1560+
("e", "i8"),
1561+
],
1562+
3,
1563+
);
1564+
}

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, Box<Expr<'a>>>, allowed: Allowed) -> Parse
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: WithSpan<'a, Box<Expr<'a>>>,
239+
pub test: WithSpan<'a, Box<Expr<'a>>>,
240+
pub otherwise: WithSpan<'a, Box<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, Box<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, Box<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, Box<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: WithSpan<'a, Box<Expr<'a>>>,
357+
start: &'a str,
358+
if_span: &'a str,
359+
) -> ParseResult<'a, WithSpan<'a, Box<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 (`.. if .. else ..`) 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+
"in Askama, the else-case of a conditional expression (`.. if .. else ..`) \
373+
is not optional",
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 (`.. if .. else ..`) expects an expression \
383+
after the keyword `else`",
384+
else_span,
385+
);
386+
};
387+
388+
let expr = Box::new(Expr::Conditional(Conditional {
389+
test,
390+
then,
391+
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, 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, Box<Self>>> {
319410
let range_right = move |i: &mut _| {
320411
(
321412
ws(alt(("..=", ".."))),
@@ -637,6 +728,7 @@ impl<'a> Expr<'a> {
637728
Self::BinOp(v) if matches!(v.op, "&&" | "||") => {
638729
v.lhs.contains_bool_lit_or_is_defined() || v.rhs.contains_bool_lit_or_is_defined()
639730
}
731+
Self::Conditional(cond) => cond.test.contains_bool_lit_or_is_defined(),
640732
Self::NumLit(_, _)
641733
| Self::StrLit(_)
642734
| Self::CharLit(_)

0 commit comments

Comments
 (0)