Skip to content

Commit 313f2ca

Browse files
Merge pull request #697 from GuillaumeGomez/let-blocks
Add support for `let blocks`
2 parents ba23980 + 7e04b5b commit 313f2ca

File tree

17 files changed

+348
-87
lines changed

17 files changed

+348
-87
lines changed

askama_derive/src/generator.rs

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ pub(crate) fn template_to_string(
3636
heritage,
3737
MapChain::default(),
3838
input.block.is_some(),
39-
0,
39+
BlockInfo::new(),
4040
);
4141
let size_hint = match generator.impl_template(buf, tmpl_kind) {
4242
Err(mut err) if err.span.is_none() => {
@@ -72,6 +72,34 @@ enum RenderFor {
7272
Extends,
7373
}
7474

75+
#[derive(Clone, Copy)]
76+
struct BlockInfo {
77+
block_name: &'static str,
78+
level: usize,
79+
}
80+
81+
impl BlockInfo {
82+
fn new() -> Self {
83+
Self {
84+
block_name: "",
85+
level: 0,
86+
}
87+
}
88+
89+
// FIXME: Instead of this error-prone API, we should use something relying on `Drop` to
90+
// decrement, or use a callback which would decrement on exit.
91+
fn increase(&mut self, block_name: &'static str) {
92+
if self.level == 0 {
93+
self.block_name = block_name;
94+
}
95+
self.level += 1;
96+
}
97+
98+
fn decrease(&mut self) {
99+
self.level -= 1;
100+
}
101+
}
102+
75103
struct Generator<'a, 'h> {
76104
/// The template input state: original struct AST and attributes
77105
input: &'a TemplateInput<'a>,
@@ -92,8 +120,8 @@ struct Generator<'a, 'h> {
92120
super_block: Option<(&'a str, usize)>,
93121
/// Buffer for writable
94122
buf_writable: WritableBuffer<'a>,
95-
/// Used in blocks to check if we are inside a filter block.
96-
is_in_filter_block: usize,
123+
/// Used in blocks to check if we are inside a filter/let block.
124+
is_in_block: BlockInfo,
97125
/// Set of called macros we are currently in. Used to prevent (indirect) recursions.
98126
seen_callers: Vec<(&'a Macro<'a>, Option<FileInfo<'a>>)>,
99127
/// The directory path of the calling file.
@@ -113,7 +141,7 @@ impl<'a, 'h> Generator<'a, 'h> {
113141
heritage: Option<&'h Heritage<'a, 'h>>,
114142
locals: MapChain<'a>,
115143
buf_writable_discard: bool,
116-
is_in_filter_block: usize,
144+
is_in_block: BlockInfo,
117145
) -> Self {
118146
Self {
119147
input,
@@ -127,7 +155,7 @@ impl<'a, 'h> Generator<'a, 'h> {
127155
discard: buf_writable_discard,
128156
..Default::default()
129157
},
130-
is_in_filter_block,
158+
is_in_block,
131159
seen_callers: Vec::new(),
132160
caller_dir: CallerDir::Unresolved,
133161
}

askama_derive/src/generator/node.rs

Lines changed: 122 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use parser::node::{
1010
Call, Comment, Compound, Cond, CondTest, Declare, FilterBlock, If, Include, Let, Lit, Loop,
1111
Match, Whitespace, Ws,
1212
};
13-
use parser::{Expr, Node, Span, Target, WithSpan};
13+
use parser::{Expr, LetValueOrBlock, Node, Span, Target, WithSpan};
1414
use proc_macro2::TokenStream;
1515
use quote::quote_spanned;
1616
use rustc_hash::FxBuildHasher;
@@ -82,7 +82,7 @@ impl<'a> Generator<'a, '_> {
8282
heritage,
8383
locals,
8484
self.buf_writable.discard,
85-
self.is_in_filter_block,
85+
self.is_in_block,
8686
);
8787
child.buf_writable = buf_writable;
8888
let res = callback(&mut child);
@@ -727,7 +727,7 @@ impl<'a> Generator<'a, '_> {
727727

728728
let mut size_hint = self.write_buf_writable(ctx, buf)?;
729729
self.flush_ws(filter.ws1);
730-
self.is_in_filter_block += 1;
730+
self.is_in_block.increase("filter");
731731
size_hint += self.write_buf_writable(ctx, buf)?;
732732
let span = ctx.span_for_node(filter.span());
733733

@@ -785,7 +785,7 @@ impl<'a> Generator<'a, '_> {
785785
}
786786
} });
787787

788-
self.is_in_filter_block -= 1;
788+
self.is_in_block.decrease();
789789
self.prepare_ws(filter.ws2);
790790
Ok(size_hint)
791791
}
@@ -926,47 +926,25 @@ impl<'a> Generator<'a, '_> {
926926

927927
fn write_let(
928928
&mut self,
929-
ctx: &Context<'_>,
929+
ctx: &Context<'a>,
930930
buf: &mut Buffer,
931931
l: &'a WithSpan<Let<'_>>,
932932
) -> Result<SizeHint, CompileError> {
933933
self.handle_ws(l.ws);
934-
let span = ctx.span_for_node(l.span());
935-
936-
let Some(val) = &l.val else {
937-
let file_info = ctx
938-
.file_info_of(l.span())
939-
.map(|info| format!(" {info}:"))
940-
.unwrap_or_default();
941-
eprintln!(
942-
"⚠️{file_info} `let` tag will stop supporting declaring variables without value. \
943-
Use `create` instead for this case",
944-
);
945-
let size_hint = self.write_buf_writable(ctx, buf)?;
946-
buf.write_token(Token![let], span);
947-
if l.is_mutable {
948-
buf.write_token(Token![mut], span);
949-
}
950-
self.visit_target(ctx, buf, false, true, &l.var, span);
951-
buf.write_token(Token![;], span);
952-
return Ok(size_hint);
953-
};
954934

955-
// Handle when this statement creates a new alias of a caller variable (or of another alias),
956-
if let Target::Name(dstvar) = l.var
957-
&& let Expr::Var(srcvar) = ***val
958-
&& let Some(caller_alias) = self.locals.get_caller(srcvar)
959-
{
960-
self.locals.insert(
961-
Cow::Borrowed(*dstvar),
962-
LocalMeta::CallerAlias(caller_alias.clone()),
963-
);
964-
return Ok(SizeHint::EMPTY);
935+
match &l.val {
936+
LetValueOrBlock::Value(val) => self.write_let_value(ctx, buf, l, val),
937+
LetValueOrBlock::Block { nodes, ws } => self.write_let_block(ctx, buf, l, nodes, *ws),
965938
}
939+
}
966940

967-
let mut expr_buf = Buffer::new();
968-
self.visit_expr(ctx, &mut expr_buf, val)?;
969-
941+
fn write_let_target(
942+
&mut self,
943+
ctx: &Context<'_>,
944+
buf: &mut Buffer,
945+
l: &'a WithSpan<Let<'_>>,
946+
span: proc_macro2::Span,
947+
) -> Result<SizeHint, CompileError> {
970948
let shadowed = self.is_shadowing_variable(ctx, &l.var, l.span())?;
971949
let size_hint = if shadowed {
972950
// Need to flush the buffer if the variable is being shadowed,
@@ -986,6 +964,104 @@ impl<'a> Generator<'a, '_> {
986964
}
987965

988966
self.visit_target(ctx, buf, true, true, &l.var, span);
967+
Ok(size_hint)
968+
}
969+
970+
fn write_let_block(
971+
&mut self,
972+
ctx: &Context<'a>,
973+
buf: &mut Buffer,
974+
l: &'a WithSpan<Let<'_>>,
975+
nodes: &'a [Box<Node<'a>>],
976+
ws: Ws,
977+
) -> Result<SizeHint, CompileError> {
978+
let var_let_source = crate::var_let_source();
979+
980+
let mut size_hint = self.write_buf_writable(ctx, buf)?;
981+
self.flush_ws(l.ws);
982+
self.is_in_block.increase("let/set");
983+
size_hint += self.write_buf_writable(ctx, buf)?;
984+
let span = ctx.span_for_node(l.span());
985+
986+
// build `FmtCell` that contains the inner block
987+
let mut filter_def_buf = Buffer::new();
988+
size_hint += self.push_locals(|this| {
989+
this.prepare_ws(l.ws);
990+
let mut size_hint = this.handle(
991+
ctx,
992+
nodes,
993+
&mut filter_def_buf,
994+
AstLevel::Nested,
995+
RenderFor::Template,
996+
)?;
997+
this.flush_ws(ws);
998+
size_hint += this.write_buf_writable(ctx, &mut filter_def_buf)?;
999+
Ok(size_hint)
1000+
})?;
1001+
let filter_def_buf = filter_def_buf.into_token_stream();
1002+
1003+
size_hint += self.write_let_target(ctx, buf, l, span)?;
1004+
buf.write_token(Token![=], span);
1005+
1006+
let var_writer = crate::var_writer();
1007+
let filter_def_buf = quote_spanned!(span=>
1008+
let #var_let_source = askama::helpers::FmtCell::new(
1009+
|#var_writer: &mut askama::helpers::core::fmt::Formatter<'_>| -> askama::Result<()> {
1010+
#filter_def_buf
1011+
askama::Result::Ok(())
1012+
}
1013+
);
1014+
);
1015+
1016+
// display the `FmtCell`
1017+
let mut filter_buf = Buffer::new();
1018+
quote_into!(&mut filter_buf, span, { askama::filters::Safe(&#var_let_source) });
1019+
let filter_buf = filter_buf.into_token_stream();
1020+
let escaper = TokenStream::from_str(self.input.escaper).unwrap();
1021+
let filter_buf = quote_spanned!(span=>
1022+
(&&askama::filters::AutoEscaper::new(
1023+
&(#filter_buf), #escaper
1024+
)).askama_auto_escape()?
1025+
);
1026+
quote_into!(buf, span, { {
1027+
#filter_def_buf
1028+
let mut __askama_tmp_write = String::new();
1029+
if askama::helpers::core::write!(&mut __askama_tmp_write, "{}", #filter_buf).is_err() {
1030+
return #var_let_source.take_err();
1031+
}
1032+
__askama_tmp_write
1033+
}; });
1034+
1035+
self.is_in_block.decrease();
1036+
self.prepare_ws(ws);
1037+
Ok(size_hint)
1038+
}
1039+
1040+
fn write_let_value(
1041+
&mut self,
1042+
ctx: &Context<'_>,
1043+
buf: &mut Buffer,
1044+
l: &'a WithSpan<Let<'_>>,
1045+
val: &WithSpan<Box<Expr<'a>>>,
1046+
) -> Result<SizeHint, CompileError> {
1047+
let span = ctx.span_for_node(l.span());
1048+
// Handle when this statement creates a new alias of a caller variable (or of another alias),
1049+
if let Target::Name(dstvar) = l.var
1050+
&& let Expr::Var(srcvar) = ***val
1051+
&& let Some(caller_alias) = self.locals.get_caller(srcvar)
1052+
{
1053+
self.locals.insert(
1054+
Cow::Borrowed(*dstvar),
1055+
LocalMeta::CallerAlias(caller_alias.clone()),
1056+
);
1057+
return Ok(SizeHint::EMPTY);
1058+
}
1059+
1060+
let mut expr_buf = Buffer::new();
1061+
self.visit_expr(ctx, &mut expr_buf, val)?;
1062+
1063+
let size_hint = self.write_let_target(ctx, buf, l, span)?;
1064+
9891065
// If it's not taking the ownership of a local variable or copyable, then we need to add
9901066
// a reference.
9911067
let borrow = !matches!(***val, Expr::Try(..))
@@ -1036,8 +1112,14 @@ impl<'a> Generator<'a, '_> {
10361112
outer: Ws,
10371113
node: Span,
10381114
) -> Result<SizeHint, CompileError> {
1039-
if self.is_in_filter_block > 0 {
1040-
return Err(ctx.generate_error("cannot have a block inside a filter block", node));
1115+
if self.is_in_block.level > 0 {
1116+
return Err(ctx.generate_error(
1117+
format!(
1118+
"cannot have a block inside a {} block",
1119+
self.is_in_block.block_name
1120+
),
1121+
node,
1122+
));
10411123
}
10421124
// Flush preceding whitespace according to the outer WS spec
10431125
self.flush_ws(outer);

askama_derive/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,10 @@ fn var_filter_source() -> Ident {
753753
syn::Ident::new("__askama_filter_block", proc_macro2::Span::call_site())
754754
}
755755

756+
fn var_let_source() -> Ident {
757+
syn::Ident::new("__askama_let_block", proc_macro2::Span::call_site())
758+
}
759+
756760
fn var_values() -> Ident {
757761
syn::Ident::new("__askama_values", proc_macro2::Span::call_site())
758762
}

askama_parser/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ use winnow::{LocatingSlice, ModalParser, ModalResult, Parser, Stateful};
3131

3232
use crate::ascii_str::{AsciiChar, AsciiStr};
3333
pub use crate::expr::{AssociatedItem, Expr, Filter, PathComponent, TyGenerics};
34-
pub use crate::node::Node;
34+
pub use crate::node::{LetValueOrBlock, Node};
3535
pub use crate::target::{NamedTarget, Target};
3636

3737
mod _parsed {

askama_parser/src/node.rs

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1239,11 +1239,17 @@ impl<'a: 'l, 'l> Declare<'a> {
12391239
}
12401240
}
12411241

1242+
#[derive(Debug, PartialEq)]
1243+
pub enum LetValueOrBlock<'a> {
1244+
Value(WithSpan<Box<Expr<'a>>>),
1245+
Block { nodes: Vec<Box<Node<'a>>>, ws: Ws },
1246+
}
1247+
12421248
#[derive(Debug, PartialEq)]
12431249
pub struct Let<'a> {
12441250
pub ws: Ws,
12451251
pub var: Target<'a>,
1246-
pub val: Option<WithSpan<Box<Expr<'a>>>>,
1252+
pub val: LetValueOrBlock<'a>,
12471253
pub is_mutable: bool,
12481254
}
12491255

@@ -1322,11 +1328,57 @@ impl<'a: 'l, 'l> Let<'a> {
13221328
);
13231329
}
13241330

1331+
if let Some(val) = val {
1332+
return Ok(Box::new(Node::Let(WithSpan::new(
1333+
Let {
1334+
ws: Ws(pws, nws),
1335+
var,
1336+
val: LetValueOrBlock::Value(val),
1337+
is_mutable: is_mut.is_some(),
1338+
},
1339+
span,
1340+
))));
1341+
}
1342+
1343+
// We do this operation
1344+
if block_end.parse_next(i).is_err() {
1345+
return Err(
1346+
ErrorContext::unclosed("block", i.state.syntax.block_end, Span::new(span)).cut(),
1347+
);
1348+
}
1349+
1350+
let (keyword, end_keyword) = if tag == "let" {
1351+
("let", "endlet")
1352+
} else {
1353+
("set", "endset")
1354+
};
1355+
1356+
let keyword_span = Span::new(span.clone());
1357+
let mut end = cut_node(
1358+
Some(keyword),
1359+
(
1360+
Node::many,
1361+
cut_node(
1362+
Some(keyword),
1363+
(
1364+
|i: &mut _| check_block_start(i, keyword_span, keyword, end_keyword),
1365+
opt(Whitespace::parse),
1366+
end_node(keyword, end_keyword),
1367+
opt(Whitespace::parse),
1368+
),
1369+
),
1370+
),
1371+
);
1372+
let (nodes, (_, pws2, _, nws2)) = end.parse_next(i)?;
1373+
13251374
Ok(Box::new(Node::Let(WithSpan::new(
13261375
Let {
13271376
ws: Ws(pws, nws),
13281377
var,
1329-
val,
1378+
val: LetValueOrBlock::Block {
1379+
nodes,
1380+
ws: Ws(pws2, nws2),
1381+
},
13301382
is_mutable: is_mut.is_some(),
13311383
},
13321384
span,

0 commit comments

Comments
 (0)