Skip to content
Draft
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
13 changes: 13 additions & 0 deletions askama_derive/src/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,19 @@ enum DisplayWrap {
Unwrapped,
}

/// `Wrapped & Wrapped == Wrapped`, otherwise `Unwrapped`
impl std::ops::BitAnd for DisplayWrap {
type Output = Self;

#[inline]
fn bitand(self, rhs: Self) -> Self::Output {
match self {
DisplayWrap::Wrapped => rhs,
DisplayWrap::Unwrapped => self,
}
}
}

#[derive(Default, Debug)]
struct WritableBuffer<'a> {
buf: Vec<Writable<'a>>,
Expand Down
34 changes: 34 additions & 0 deletions askama_derive/src/generator/expr.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::borrow::Cow;
use std::str::FromStr;

use parser::expr::Conditional;
use parser::node::CondTest;
use parser::{
AssociatedItem, CharLit, CharPrefix, Expr, PathComponent, Span, StrLit, StrPrefix, Target,
Expand All @@ -14,6 +15,7 @@ use super::{
DisplayWrap, Generator, LocalMeta, Writable, binary_op, compile_time_escape, is_copyable,
logic_op, range_op, unary_op,
};
use crate::generator::node::EvaluatedResult;
use crate::heritage::Context;
use crate::integration::Buffer;
use crate::{CompileError, field_new, quote_into};
Expand Down Expand Up @@ -103,6 +105,7 @@ impl<'a> Generator<'a, '_> {
Expr::Concat(ref exprs) => self.visit_concat(ctx, buf, exprs)?,
Expr::LetCond(ref cond) => self.visit_let_cond(ctx, buf, cond)?,
Expr::ArgumentPlaceholder => DisplayWrap::Unwrapped,
Expr::Conditional(ref cond) => self.visit_conditional_expr(ctx, buf, cond)?,
})
}

Expand Down Expand Up @@ -251,6 +254,37 @@ impl<'a> Generator<'a, '_> {
}
}

fn visit_conditional_expr(
&mut self,
ctx: &Context<'_>,
buf: &mut Buffer,
cond: &Conditional<'a>,
) -> Result<DisplayWrap, CompileError> {
let result;
let expr = if cond.test.contains_bool_lit_or_is_defined() {
result = self.evaluate_condition(WithSpan::clone(&cond.test), &mut true);
match &result {
EvaluatedResult::AlwaysTrue => return self.visit_expr(ctx, buf, &cond.then),
EvaluatedResult::AlwaysFalse => return self.visit_expr(ctx, buf, &cond.otherwise),
EvaluatedResult::Unknown(expr) => expr,
}
} else {
&cond.test
};

let mut then_buf = Buffer::new();
let then = self.visit_expr(ctx, &mut then_buf, &cond.then)?;
let mut otherwise_buf = Buffer::new();
let otherwise = self.visit_expr(ctx, &mut otherwise_buf, &cond.otherwise)?;

let span = ctx.span_for_node(expr.span());
buf.write_token(Token![if], span);
self.visit_condition(ctx, buf, expr)?;
quote_into!(buf, span, { { #then_buf } else { #otherwise_buf } });

Ok(then & otherwise)
}

fn visit_let_cond(
&mut self,
ctx: &Context<'_>,
Expand Down
16 changes: 14 additions & 2 deletions askama_derive/src/generator/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ impl<'a> Generator<'a, '_> {
Ok(size_hint)
}

fn evaluate_condition(
pub(super) fn evaluate_condition(
&self,
expr: WithSpan<'a, Box<Expr<'a>>>,
only_contains_is_defined: &mut bool,
Expand Down Expand Up @@ -310,6 +310,15 @@ impl<'a> Generator<'a, '_> {
EvaluatedResult::AlwaysTrue
}
}
Expr::Conditional(cond) => {
// If we can tell the outcome of the condition, recurse.
let expr = match self.evaluate_condition(cond.test, only_contains_is_defined) {
EvaluatedResult::AlwaysTrue => cond.then,
EvaluatedResult::AlwaysFalse => cond.otherwise,
EvaluatedResult::Unknown(expr) => return EvaluatedResult::Unknown(expr),
};
self.evaluate_condition(expr, only_contains_is_defined)
}
}
}

Expand Down Expand Up @@ -1500,7 +1509,7 @@ struct Conds<'a> {
}

#[derive(Debug, Clone, PartialEq)]
enum EvaluatedResult<'a> {
pub(super) enum EvaluatedResult<'a> {
AlwaysTrue,
AlwaysFalse,
Unknown(WithSpan<'a, Box<Expr<'a>>>),
Expand Down Expand Up @@ -1660,6 +1669,9 @@ fn is_cacheable(expr: &WithSpan<'_, Box<Expr<'_>>>) -> bool {
Expr::As(expr, _) => is_cacheable(expr),
Expr::Try(expr) => is_cacheable(expr),
Expr::Concat(args) => args.iter().all(is_cacheable),
Expr::Conditional(cond) => {
is_cacheable(&cond.test) && is_cacheable(&cond.then) && is_cacheable(&cond.otherwise)
}
// Doesn't make sense in this context.
Expr::LetCond(_) => false,
// We have too little information to tell if the expression is pure:
Expand Down
127 changes: 124 additions & 3 deletions askama_derive/src/tests.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
//! Files containing tests for generated code.

use std::fmt;
use std::io::Write;
use std::path::Path;
use std::process::exit;
use std::{fmt, io};

use console::style;
use prettyplease::unparse;
Expand Down Expand Up @@ -124,7 +126,8 @@ fn compare_ex(
}
}

panic!(
let _: io::Result<()> = writeln!(
io::stderr().lock(),
"\n\
=== Expected ===\n\
\n\
Expand All @@ -138,11 +141,15 @@ fn compare_ex(
\n\
{diff}\n\
\n\
=== FAILURE ===",
=== FAILURE ===\n\
\n\
{location}\n",
expected = style(&expected).red(),
generated = style(&generated).green(),
diff = Diff(&expected, &generated),
location = std::panic::Location::caller(),
);
exit(1);
}
}

Expand Down Expand Up @@ -1443,3 +1450,117 @@ fn check_expr_ungrouping() {
11,
);
}

#[test]
fn test_conditional_expr() {
compare(
r"{{ a if b else c }}",
r#"
match (
&((&&askama::filters::AutoEscaper::new(
&(if askama::helpers::as_bool(&(self.b)) { self.a } else { self.c }),
askama::filters::Text,
))
.askama_auto_escape()?),
) {
(__askama_expr0,) => {
(&&&askama::filters::Writable(__askama_expr0))
.askama_write(__askama_writer, __askama_values)?;
}
}"#,
&[("a", "i8"), ("b", "i8"), ("c", "i8")],
3,
);

compare(
r"{{ a if b is defined else c }}",
r#"
match (
&((&&askama::filters::AutoEscaper::new(&(self.a), askama::filters::Text))
.askama_auto_escape()?),
) {
(__askama_expr0,) => {
(&&&askama::filters::Writable(__askama_expr0))
.askama_write(__askama_writer, __askama_values)?;
}
}"#,
&[("a", "i8"), ("b", "i8"), ("c", "i8")],
3,
);

compare(
r"{{ a if b is not defined else c }}",
r#"
match (
&((&&askama::filters::AutoEscaper::new(&(self.c), askama::filters::Text))
.askama_auto_escape()?),
) {
(__askama_expr0,) => {
(&&&askama::filters::Writable(__askama_expr0))
.askama_write(__askama_writer, __askama_values)?;
}
}"#,
&[("a", "i8"), ("b", "i8"), ("c", "i8")],
3,
);

compare(
r"{{ a if b if c else d else e }}",
r#"
match (
&((&&askama::filters::AutoEscaper::new(
&(if askama::helpers::as_bool(
&(if askama::helpers::as_bool(&(self.c)) { self.b } else { self.d })
) {
self.a
} else {
self.e
}),
askama::filters::Text,
))
.askama_auto_escape()?),
) {
(__askama_expr0,) => {
(&&&askama::filters::Writable(__askama_expr0))
.askama_write(__askama_writer, __askama_values)?;
}
}"#,
&[
("a", "i8"),
("b", "i8"),
("c", "i8"),
("d", "i8"),
("e", "i8"),
],
3,
);

compare(
r"{{ a if b else c if d else e }}",
Copy link
Collaborator

@GuillaumeGomez GuillaumeGomez Jul 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is absolutely horrifying for my brain to parse. 🤣

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about this formatting? :)

{{
   a if b else
   c if d else
   e
}}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That doesn't help much, I'm too used to blocks. But it's fine, don't worry. One question though, does this work:

{{ a if b else (c if d else e) }}

?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that does work.

r#"
match (
&((&&askama::filters::AutoEscaper::new(
&(if askama::helpers::as_bool(&(self.b)) {
self.a
} else {
if askama::helpers::as_bool(&(self.d)) { self.c } else { self.e }
}),
askama::filters::Text,
))
.askama_auto_escape()?),
) {
(__askama_expr0,) => {
(&&&askama::filters::Writable(__askama_expr0))
.askama_write(__askama_writer, __askama_values)?;
}
}"#,
&[
("a", "i8"),
("b", "i8"),
("c", "i8"),
("d", "i8"),
("e", "i8"),
],
3,
);
}
Loading
Loading