mirror of
https://github.com/askama-rs/askama.git
synced 2026-03-13 18:08:23 +00:00
Implement compound assignments (e.g. {% mut a += 1 %})
This commit is contained in:
parent
92fd0be6e3
commit
4b0ee18fab
@ -849,5 +849,9 @@ fn range_op(op: &str, span: proc_macro2::Span) -> TokenStream {
|
||||
#[inline]
|
||||
#[track_caller]
|
||||
fn binary_op(op: &str, span: proc_macro2::Span) -> TokenStream {
|
||||
make_token_match!(op @ span => * / % + - << >> & ^ | == != < > <= >= && || .. ..=)
|
||||
make_token_match!(
|
||||
op @ span =>
|
||||
* / % + - << >> & ^ | == != < > <= >= && || .. ..=
|
||||
= += -= *= /= %= &= |= ^= <<= >>=
|
||||
)
|
||||
}
|
||||
|
||||
@ -1539,3 +1539,40 @@ fn regression_tests_span_change() {
|
||||
struct Foo;
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compound_assignment() {
|
||||
for op in [
|
||||
"=", "+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=", "<<=", ">>=",
|
||||
] {
|
||||
let jinja = r#"
|
||||
{%- let mut prefixsum = 0 -%}
|
||||
{%- for i in 0..limit -%}
|
||||
{%- mut prefixsum @= i -%}
|
||||
{{ prefixsum }}.
|
||||
{%- endfor -%}
|
||||
"#
|
||||
.replace("@=", op);
|
||||
|
||||
let expected = r#"
|
||||
let mut prefixsum = 0;
|
||||
let __askama_iter = 0..self.limit;
|
||||
for (i, __askama_item) in askama::helpers::TemplateLoop::new(__askama_iter) {
|
||||
let _ = prefixsum @= i;
|
||||
match (
|
||||
&((&&askama::filters::AutoEscaper::new(&(prefixsum), askama::filters::Text))
|
||||
.askama_auto_escape()?),
|
||||
) {
|
||||
(__askama_expr0,) => {
|
||||
(&&&askama::filters::Writable(__askama_expr0))
|
||||
.askama_write(__askama_writer, __askama_values)?;
|
||||
}
|
||||
}
|
||||
__askama_writer.write_str(".")?;
|
||||
}
|
||||
"#
|
||||
.replace("@=", op);
|
||||
|
||||
compare(&jinja, &expected, &[("limit", "u32")], 6);
|
||||
}
|
||||
}
|
||||
|
||||
@ -790,24 +790,22 @@ impl<'a: 'l, 'l> Expr<'a> {
|
||||
}
|
||||
|
||||
fn token_xor<'a: 'l, 'l>(i: &mut InputStream<'a, 'l>) -> ParseResult<'a> {
|
||||
let (good, span) = alt((keyword("xor").value(true), '^'.value(false)))
|
||||
.with_span()
|
||||
.parse_next(i)?;
|
||||
if good {
|
||||
Ok("^")
|
||||
} else {
|
||||
let good = keyword("xor").value(None);
|
||||
let bad = ('^', not('=')).span().map(Some);
|
||||
if let Some(span) = alt((good, bad)).parse_next(i)? {
|
||||
cut_error!("the binary XOR operator is called `xor` in askama", span)
|
||||
} else {
|
||||
Ok("^")
|
||||
}
|
||||
}
|
||||
|
||||
fn token_bitand<'a: 'l, 'l>(i: &mut InputStream<'a, 'l>) -> ParseResult<'a> {
|
||||
let (good, span) = alt((keyword("bitand").value(true), ('&', not('&')).value(false)))
|
||||
.with_span()
|
||||
.parse_next(i)?;
|
||||
if good {
|
||||
Ok("&")
|
||||
} else {
|
||||
let good = keyword("bitand").value(None);
|
||||
let bad = ('&', not(one_of(['&', '=']))).span().map(Some);
|
||||
if let Some(span) = alt((good, bad)).parse_next(i)? {
|
||||
cut_error!("the binary AND operator is called `bitand` in askama", span)
|
||||
} else {
|
||||
Ok("&")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1358,7 +1358,7 @@ impl LevelGuard<'_> {
|
||||
}
|
||||
|
||||
fn filter<'a: 'l, 'l>(i: &mut InputStream<'a, 'l>) -> ParseResult<'a, Filter<'a>> {
|
||||
preceded(('|', not('|')), cut_err(Filter::parse)).parse_next(i)
|
||||
preceded(('|', not(one_of(['|', '=']))), cut_err(Filter::parse)).parse_next(i)
|
||||
}
|
||||
|
||||
/// Returns the common parts of two paths.
|
||||
|
||||
@ -9,6 +9,7 @@ use winnow::stream::{Location, Stream};
|
||||
use winnow::token::{any, literal, rest, take, take_until};
|
||||
use winnow::{ModalParser, Parser};
|
||||
|
||||
use crate::expr::BinOp;
|
||||
use crate::{
|
||||
ErrorContext, Expr, Filter, HashSet, InputStream, ParseErr, ParseResult, Span, Target,
|
||||
WithSpan, block_end, block_start, cut_error, expr_end, expr_start, filter, identifier,
|
||||
@ -93,21 +94,22 @@ impl<'a: 'l, 'l> Node<'a> {
|
||||
.parse_next(i)?;
|
||||
|
||||
let func = match tag {
|
||||
"call" => Call::parse,
|
||||
"decl" | "declare" => Declare::parse,
|
||||
"let" | "set" => Let::parse,
|
||||
"if" => If::parse,
|
||||
"for" => Loop::parse,
|
||||
"match" => Match::parse,
|
||||
"extends" => Extends::parse,
|
||||
"include" => Include::parse,
|
||||
"import" => Import::parse,
|
||||
"block" => BlockDef::parse,
|
||||
"macro" => Macro::parse,
|
||||
"raw" => Raw::parse,
|
||||
"break" => Self::r#break,
|
||||
"call" => Call::parse,
|
||||
"continue" => Self::r#continue,
|
||||
"decl" | "declare" => Declare::parse,
|
||||
"extends" => Extends::parse,
|
||||
"filter" => FilterBlock::parse,
|
||||
"for" => Loop::parse,
|
||||
"if" => If::parse,
|
||||
"import" => Import::parse,
|
||||
"include" => Include::parse,
|
||||
"let" | "set" => Let::parse,
|
||||
"macro" => Macro::parse,
|
||||
"match" => Match::parse,
|
||||
"mut" => Let::compound,
|
||||
"raw" => Raw::parse,
|
||||
_ => {
|
||||
i.reset(&start);
|
||||
return fail.parse_next(i);
|
||||
@ -1299,6 +1301,43 @@ impl<'a: 'l, 'l> Let<'a> {
|
||||
span,
|
||||
))))
|
||||
}
|
||||
|
||||
fn compound(i: &mut InputStream<'a, 'l>) -> ParseResult<'a, Box<Node<'a>>> {
|
||||
let (pws, span, (lhs, op, rhs, nws)) = (
|
||||
opt(Whitespace::parse),
|
||||
ws(keyword("mut").span()),
|
||||
cut_node(
|
||||
Some("mut"),
|
||||
(
|
||||
|i: &mut _| Expr::parse(i, false),
|
||||
ws(alt((
|
||||
// <https://doc.rust-lang.org/reference/expressions/operator-expr.html#compound-assignment-expressions>
|
||||
"=", "+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=", "<<=", ">>=",
|
||||
))),
|
||||
ws(|i: &mut _| Expr::parse(i, false)),
|
||||
opt(Whitespace::parse),
|
||||
),
|
||||
),
|
||||
)
|
||||
.parse_next(i)?;
|
||||
|
||||
// For `a += b` this AST generates the code `let _ = a += b;`. This may look odd, but
|
||||
// is valid rust code, because the value of any assignment (compound or not) is `()`.
|
||||
// This way the generator does not need to know about compound assignments for them
|
||||
// to work.
|
||||
Ok(Box::new(Node::Let(WithSpan::new(
|
||||
Let {
|
||||
ws: Ws(pws, nws),
|
||||
var: Target::Placeholder(WithSpan::new((), span.clone())),
|
||||
val: Some(WithSpan::new(
|
||||
Box::new(Expr::BinOp(BinOp { op, lhs, rhs })),
|
||||
span.clone(),
|
||||
)),
|
||||
is_mutable: false,
|
||||
},
|
||||
span,
|
||||
))))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
|
||||
@ -117,6 +117,30 @@ to prevent changing ownership. The rules are as follows:
|
||||
* If the value is a field (`x.y`), it WILL BE put behind a reference.
|
||||
* If the expression ends with a question mark (like `x?`), it WILL NOT BE put behind a reference.
|
||||
|
||||
### Compound assignments
|
||||
|
||||
Using the keyword `mut`, [compound assignments][rust101-assignment-op] (also called
|
||||
"augmented assignments"), such as `x += 1` to increment `x` by 1, are possible, too:
|
||||
|
||||
```jinja
|
||||
{% let mut counter = 0 %}
|
||||
{% for i in 1..=10 %}
|
||||
{% mut counter += 1 %}
|
||||
{{ counter }}
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
This example will output [`1 3 6 10 15`…][wikipedia-prefix-sum].
|
||||
|
||||
The target can be a variable or a more complex expression. The rules are the same as in rust,
|
||||
e.g. the left-hand side of the expression, i.e. the assignment target, must be mutable.
|
||||
All [compound assignment operators][reference-compound-assignment] that are valid in rust are
|
||||
valid in askama, too.
|
||||
|
||||
[rust101-assignment-op]: <https://rustlabs.github.io/docs/rust101/assignment_compound_assignment_operators/#compound-assignment-operator> "Rust - Quick start: Assignment and Compound Assignment Operators"
|
||||
[wikipedia-prefix-sum]: <https://en.wikipedia.org/wiki/Prefix_sum?curid=6109308> "Wikipedia: Prefix sum"
|
||||
[reference-compound-assignment]: <https://doc.rust-lang.org/reference/expressions/operator-expr.html#compound-assignment-expressions> "The Rust Reference: Compound assignment expressions"
|
||||
|
||||
## Filters
|
||||
|
||||
Values such as those obtained from variables can be post-processed
|
||||
|
||||
169
testing/tests/compound-assignment.rs
Normal file
169
testing/tests/compound-assignment.rs
Normal file
@ -0,0 +1,169 @@
|
||||
use std::cell::Cell;
|
||||
|
||||
use askama::Template;
|
||||
|
||||
#[test]
|
||||
fn test_prefixsum() {
|
||||
#[derive(Template)]
|
||||
#[template(
|
||||
ext = "txt",
|
||||
source = "
|
||||
{%- let mut prefixsum = 0 -%}
|
||||
{%- for i in 0..limit -%}
|
||||
{%- mut prefixsum += i -%}
|
||||
{{ prefixsum }}.
|
||||
{%- endfor -%}
|
||||
"
|
||||
)]
|
||||
struct PrefixSum {
|
||||
limit: u32,
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
PrefixSum { limit: 10 }.render().unwrap(),
|
||||
"0.1.3.6.10.15.21.28.36.45."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_expr_on_lhs() {
|
||||
#[derive(Template)]
|
||||
#[template(
|
||||
ext = "txt",
|
||||
source = "
|
||||
{%- let mut prefixsum = Cell::new(0u32) -%}
|
||||
{%- for i in 0..limit -%}
|
||||
{%- mut *prefixsum.get_mut() += i -%}
|
||||
{{ prefixsum.get() }}.
|
||||
{%- endfor -%}
|
||||
"
|
||||
)]
|
||||
struct PrefixSum {
|
||||
limit: u32,
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
PrefixSum { limit: 10 }.render().unwrap(),
|
||||
"0.1.3.6.10.15.21.28.36.45."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add() {
|
||||
#[derive(Template)]
|
||||
#[template(
|
||||
ext = "txt",
|
||||
source = "{% let mut value = 9 %} {%- mut value += 2 -%} {{ value }}"
|
||||
)]
|
||||
struct Test;
|
||||
|
||||
assert_eq!(Test.render().unwrap(), "11");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sub() {
|
||||
#[derive(Template)]
|
||||
#[template(
|
||||
ext = "txt",
|
||||
source = "{% let mut value = 9 %} {%- mut value -= 2 -%} {{ value }}"
|
||||
)]
|
||||
struct Test;
|
||||
|
||||
assert_eq!(Test.render().unwrap(), "7");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mul() {
|
||||
#[derive(Template)]
|
||||
#[template(
|
||||
ext = "txt",
|
||||
source = "{% let mut value = 9 %} {%- mut value *= 2 -%} {{ value }}"
|
||||
)]
|
||||
struct Test;
|
||||
|
||||
assert_eq!(Test.render().unwrap(), "18");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_div() {
|
||||
#[derive(Template)]
|
||||
#[template(
|
||||
ext = "txt",
|
||||
source = "{% let mut value = 9 %} {%- mut value /= 2 -%} {{ value }}"
|
||||
)]
|
||||
struct Test;
|
||||
|
||||
assert_eq!(Test.render().unwrap(), "4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rem() {
|
||||
#[derive(Template)]
|
||||
#[template(
|
||||
ext = "txt",
|
||||
source = "{% let mut value = 9 %} {%- mut value %= 2 -%} {{ value }}"
|
||||
)]
|
||||
struct Test;
|
||||
|
||||
assert_eq!(Test.render().unwrap(), "1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_and() {
|
||||
#[derive(Template)]
|
||||
#[template(
|
||||
ext = "txt",
|
||||
source = "{% let mut value = 9 %} {%- mut value &= 3 -%} {{ value }}"
|
||||
)]
|
||||
struct Test;
|
||||
|
||||
assert_eq!(Test.render().unwrap(), "1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_or() {
|
||||
#[derive(Template)]
|
||||
#[template(
|
||||
ext = "txt",
|
||||
source = "{% let mut value = 9 %} {%- mut value |= 3 -%} {{ value }}"
|
||||
)]
|
||||
struct Test;
|
||||
|
||||
assert_eq!(Test.render().unwrap(), "11");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_xor() {
|
||||
#[derive(Template)]
|
||||
#[template(
|
||||
ext = "txt",
|
||||
source = "{% let mut value = 9 %} {%- mut value ^= 3 -%} {{ value }}"
|
||||
)]
|
||||
struct Test;
|
||||
|
||||
assert_eq!(Test.render().unwrap(), "10");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shl() {
|
||||
#[derive(Template)]
|
||||
#[template(
|
||||
ext = "txt",
|
||||
source = "{% let mut value = 9 %} {%- mut value <<= 2 -%} {{ value }}"
|
||||
)]
|
||||
struct Test;
|
||||
|
||||
assert_eq!(Test.render().unwrap(), "36");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shr() {
|
||||
#[derive(Template)]
|
||||
#[template(
|
||||
ext = "txt",
|
||||
source = "{% let mut value = 9 %} {%- mut value >>= 2 -%} {{ value }}"
|
||||
)]
|
||||
struct Test;
|
||||
|
||||
assert_eq!(Test.render().unwrap(), "2");
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user