Skip to content

Commit cff0acc

Browse files
Merge pull request #613 from GuillaumeGomez/create
Add new tag to declare variable without value
2 parents a9f534e + ed08f85 commit cff0acc

File tree

13 files changed

+229
-44
lines changed

13 files changed

+229
-44
lines changed

askama_derive/src/generator/expr.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -982,7 +982,6 @@ impl<'a> Generator<'a, '_> {
982982
DisplayWrap::Unwrapped
983983
}
984984

985-
// FIXME: This function should have a `Span`, but `cond.target` isn't `WithSpan`.
986985
pub(super) fn visit_target(
987986
&mut self,
988987
ctx: &Context<'_>,

askama_derive/src/generator/node.rs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ use std::str::FromStr;
77

88
use parser::expr::BinOp;
99
use parser::node::{
10-
Call, Comment, Cond, CondTest, FilterBlock, If, Include, Let, Lit, Loop, Match, Whitespace, Ws,
10+
Call, Comment, Cond, CondTest, Declare, FilterBlock, If, Include, Let, Lit, Loop, Match,
11+
Whitespace, Ws,
1112
};
1213
use parser::{Expr, Node, Span, Target, WithSpan};
1314
use proc_macro2::TokenStream;
@@ -103,6 +104,9 @@ impl<'a> Generator<'a, '_> {
103104
Node::Let(ref l) => {
104105
self.write_let(ctx, buf, l)?;
105106
}
107+
Node::Declare(ref c) => {
108+
self.write_decl(ctx, buf, c)?;
109+
}
106110
Node::If(ref i) => {
107111
size_hint += self.write_if(ctx, buf, i)?;
108112
}
@@ -859,6 +863,14 @@ impl<'a> Generator<'a, '_> {
859863
let span = ctx.span_for_node(l.span());
860864

861865
let Some(val) = &l.val else {
866+
let file_info = ctx
867+
.file_info_of(l.span())
868+
.map(|info| format!(" {info}:"))
869+
.unwrap_or_default();
870+
eprintln!(
871+
"⚠️{file_info} `let` tag will stop supporting declaring variables without value. \
872+
Use `create` instead for this case",
873+
);
862874
self.write_buf_writable(ctx, buf)?;
863875
buf.write_token(Token![let], span);
864876
if l.is_mutable {
@@ -914,6 +926,32 @@ impl<'a> Generator<'a, '_> {
914926
Ok(())
915927
}
916928

929+
fn write_decl(
930+
&mut self,
931+
ctx: &Context<'_>,
932+
buf: &mut Buffer,
933+
c: &'a WithSpan<Declare<'_>>,
934+
) -> Result<(), CompileError> {
935+
let span = ctx.span_for_node(c.span());
936+
if *c.var_name == "_" {
937+
return Err(ctx.generate_error(
938+
"`_` cannot be used when there is no value assigned, use `let` instead",
939+
c.var_name.span(),
940+
));
941+
}
942+
self.handle_ws(c.ws);
943+
944+
self.write_buf_writable(ctx, buf)?;
945+
buf.write_token(Token![let], span);
946+
if c.is_mutable {
947+
buf.write_token(Token![mut], span);
948+
}
949+
self.visit_target(ctx, buf, false, true, &Target::Name(c.var_name), span);
950+
buf.write_token(Token![;], span);
951+
952+
Ok(())
953+
}
954+
917955
// If `name` is `Some`, this is a call to a block definition, and we have to find
918956
// the first block for that name from the ancestry chain. If name is `None`, this
919957
// is from a `super()` call, and we can get the name from `self.super_block`.

askama_derive/src/input.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,7 @@ impl TemplateInput<'_> {
310310
| Node::Expr(_, _)
311311
| Node::Extends(_)
312312
| Node::Let(_)
313+
| Node::Declare(_)
313314
| Node::Import(_)
314315
| Node::Macro(_)
315316
| Node::Raw(_)

askama_parser/src/node.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ pub enum Node<'a> {
2222
Expr(Ws, WithSpan<Box<Expr<'a>>>),
2323
Call(WithSpan<Call<'a>>),
2424
Let(WithSpan<Let<'a>>),
25+
Declare(WithSpan<Declare<'a>>),
2526
If(WithSpan<If<'a>>),
2627
Match(WithSpan<Match<'a>>),
2728
Loop(WithSpan<Loop<'a>>),
@@ -93,6 +94,7 @@ impl<'a: 'l, 'l> Node<'a> {
9394

9495
let func = match tag {
9596
"call" => Call::parse,
97+
"decl" | "declare" => Declare::parse,
9698
"let" | "set" => Let::parse,
9799
"if" => If::parse,
98100
"for" => Loop::parse,
@@ -190,6 +192,7 @@ impl<'a: 'l, 'l> Node<'a> {
190192
Self::Expr(_, span) => span.span,
191193
Self::Call(span) => span.span,
192194
Self::Let(span) => span.span,
195+
Self::Declare(span) => span.span,
193196
Self::If(span) => span.span,
194197
Self::Match(span) => span.span,
195198
Self::Loop(span) => span.span,
@@ -1198,6 +1201,35 @@ impl<'a: 'l, 'l> Raw<'a> {
11981201
}
11991202
}
12001203

1204+
#[derive(Debug, PartialEq)]
1205+
pub struct Declare<'a> {
1206+
pub ws: Ws,
1207+
pub var_name: WithSpan<&'a str>,
1208+
pub is_mutable: bool,
1209+
}
1210+
1211+
impl<'a: 'l, 'l> Declare<'a> {
1212+
fn parse(i: &mut InputStream<'a, 'l>) -> ParseResult<'a, Box<Node<'a>>> {
1213+
let mut p = (
1214+
opt(Whitespace::parse),
1215+
ws(alt((keyword("decl"), keyword("declare"))).span()),
1216+
ws(opt(keyword("mut").span())),
1217+
ws(identifier.with_span()),
1218+
opt(Whitespace::parse),
1219+
);
1220+
let (pws, span, is_mut, (var_name, var_name_span), nws) = p.parse_next(i)?;
1221+
1222+
Ok(Box::new(Node::Declare(WithSpan::new(
1223+
Declare {
1224+
ws: Ws(pws, nws),
1225+
var_name: WithSpan::new(var_name, var_name_span),
1226+
is_mutable: is_mut.is_some(),
1227+
},
1228+
span,
1229+
))))
1230+
}
1231+
}
1232+
12011233
#[derive(Debug, PartialEq)]
12021234
pub struct Let<'a> {
12031235
pub ws: Ws,

book/src/template_syntax.md

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
| `{% filter ... %} ... {% endfilter %}` | [Filter block](#filter-blocks) |
1010
| `{# ... #}` | [Comment](#comments) |
1111
| `{% let ... = ... %}` or `{% set ... = ... %}` | [Variable assignment](#assignments) |
12+
| `{% decl ... %}` or `{% declare ... = ... %}` | [Set variable values later](#set-variable-values-later) |
1213
| `{% if ... %} ... {% else if ... %} ... {% else %} ... {% endif %}` | [If-Else conditional block](#if) |
1314
| `{% match ... %} {% when ... %} ... {% else %} ... {% endmatch %}` | [Match block](#match) |
1415
| `{% for ... in ... %} ... {% else %} ... {% endfor %}` | [For loop block](#for) |
@@ -67,14 +68,6 @@ Assignments use the `let` tag:
6768
```jinja
6869
{% let name = user.name %}
6970
{% let len = name.len() %}
70-
71-
{% let val -%}
72-
{% if len == 0 -%}
73-
{% let val = "foo" -%}
74-
{% else -%}
75-
{% let val = name -%}
76-
{% endif -%}
77-
{{ val }}
7871
```
7972

8073
Like Rust, Askama also supports shadowing variables.
@@ -98,6 +91,21 @@ you need it to be mutable #}
9891

9992
For compatibility with Jinja, `set` can be used in place of `let`.
10093

94+
### Set variable values later
95+
96+
If you want to create a variable but set its value based on a condition, you can
97+
declare it without a value by using the `decl` (or `declare`) keyword:
98+
99+
```jinja
100+
{% decl val -%}
101+
{% if len == 0 -%}
102+
{% let val = "foo" -%}
103+
{% else -%}
104+
{% let val = name -%}
105+
{% endif -%}
106+
{{ val }}
107+
```
108+
101109
### Borrow rules
102110

103111
In some cases, the value of a variable initialization will be put behind a reference

book/update-theme.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def update_theme(target: PathLike) -> None:
7777
state = "before-sidebar"
7878
for line in input_f:
7979
match state:
80-
case "before-sidebar" if '<nav id="mdbook-sidebar"' in line:
80+
case "before-sidebar" if '<nav ' in line and 'class="sidebar"' in line:
8181
line = SIDEBAR
8282
state = "in-sidebar"
8383
case "in-sidebar":

testing/tests/declare.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
use askama::Template;
2+
3+
// This test ensures that the `decl` keyword works as expected.
4+
#[test]
5+
fn decl() {
6+
#[derive(Template)]
7+
#[template(
8+
source = r#"{%- decl x -%}
9+
{%- if y -%}
10+
{%- let x = String::new() %}
11+
{%- else -%}
12+
{%- let x = format!("blob") %}
13+
{%- endif -%}
14+
{{ x }}"#,
15+
ext = "html"
16+
)]
17+
struct A {
18+
y: bool,
19+
}
20+
21+
assert_eq!(A { y: false }.render().unwrap(), "blob");
22+
assert_eq!(A { y: true }.render().unwrap(), "");
23+
}
24+
25+
// Same as `decl` except with the `declare` keyword.
26+
#[test]
27+
fn declare() {
28+
#[derive(Template)]
29+
#[template(
30+
source = r#"{%- declare x -%}
31+
{%- if y -%}
32+
{%- let x = String::new() %}
33+
{%- else -%}
34+
{%- let x = format!("blob") %}
35+
{%- endif -%}
36+
{{ x }}"#,
37+
ext = "html"
38+
)]
39+
struct A {
40+
y: bool,
41+
}
42+
43+
assert_eq!(A { y: false }.render().unwrap(), "blob");
44+
assert_eq!(A { y: true }.render().unwrap(), "");
45+
}

testing/tests/let.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ fn let_macro() {
1818
y: bool,
1919
}
2020

21-
let template = A { y: false };
22-
assert_eq!(template.render().unwrap(), "blob")
21+
assert_eq!(A { y: false }.render().unwrap(), "blob");
22+
assert_eq!(A { y: true }.render().unwrap(), "");
2323
}
2424

2525
// Ensures that variables name can start with `_`.

testing/tests/ui/declare.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
use askama::Template;
2+
3+
#[derive(Template)]
4+
#[template(source = "{% decl x = 'a' %}", ext = "html")]
5+
struct Declare1;
6+
7+
#[derive(Template)]
8+
#[template(source = "{% declare x = 'a' %}", ext = "html")]
9+
struct Declare2;
10+
11+
fn main() {}

testing/tests/ui/declare.stderr

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
error: failed to parse template source
2+
--> <source attribute>:1:10
3+
"= 'a' %}"
4+
--> tests/ui/declare.rs:4:21
5+
|
6+
4 | #[template(source = "{% decl x = 'a' %}", ext = "html")]
7+
| ^^^^^^^^^^^^^^^^^^^^
8+
9+
error: failed to parse template source
10+
--> <source attribute>:1:13
11+
"= 'a' %}"
12+
--> tests/ui/declare.rs:8:21
13+
|
14+
8 | #[template(source = "{% declare x = 'a' %}", ext = "html")]
15+
| ^^^^^^^^^^^^^^^^^^^^^^^

0 commit comments

Comments
 (0)