diff --git a/minijinja/src/ast.rs b/minijinja/src/ast.rs index 405892a7..91e62202 100644 --- a/minijinja/src/ast.rs +++ b/minijinja/src/ast.rs @@ -57,6 +57,7 @@ pub enum Stmt<'a> { IfCond(Spanned>), WithBlock(Spanned>), Set(Spanned>), + SetBlock(Spanned>), Block(Spanned>), Extends(Spanned>), Include(Spanned>), @@ -75,6 +76,7 @@ impl<'a> fmt::Debug for Stmt<'a> { Stmt::IfCond(s) => fmt::Debug::fmt(s, f), Stmt::WithBlock(s) => fmt::Debug::fmt(s, f), Stmt::Set(s) => fmt::Debug::fmt(s, f), + Stmt::SetBlock(s) => fmt::Debug::fmt(s, f), Stmt::Block(s) => fmt::Debug::fmt(s, f), Stmt::Extends(s) => fmt::Debug::fmt(s, f), Stmt::Include(s) => fmt::Debug::fmt(s, f), @@ -160,6 +162,14 @@ pub struct Set<'a> { pub expr: Expr<'a>, } +/// A set capture statement. +#[cfg_attr(feature = "internal_debug", derive(Debug))] +pub struct SetBlock<'a> { + pub target: Expr<'a>, + pub filter: Option>, + pub body: Vec>, +} + /// A block for inheritance elements. #[cfg_attr(feature = "internal_debug", derive(Debug))] pub struct Block<'a> { diff --git a/minijinja/src/compiler.rs b/minijinja/src/compiler.rs index cb63cde7..778e324b 100644 --- a/minijinja/src/compiler.rs +++ b/minijinja/src/compiler.rs @@ -269,6 +269,18 @@ impl<'source> Compiler<'source> { self.compile_expr(&set.expr)?; self.compile_assignment(&set.target)?; } + ast::Stmt::SetBlock(set_block) => { + self.set_location_from_span(set_block.span()); + self.add(Instruction::BeginCapture); + for node in &set_block.body { + self.compile_stmt(node)?; + } + self.add(Instruction::EndCapture); + if let Some(ref filter) = set_block.filter { + self.compile_expr(filter)?; + } + self.compile_assignment(&set_block.target)?; + } ast::Stmt::Block(block) => { self.set_location_from_span(block.span()); let mut sub_compiler = diff --git a/minijinja/src/meta.rs b/minijinja/src/meta.rs index 7a3c57a0..ea4b5e34 100644 --- a/minijinja/src/meta.rs +++ b/minijinja/src/meta.rs @@ -179,6 +179,12 @@ pub fn find_undeclared_variables(source: &str) -> Result, Error> assign_nested(&stmt.target, state); visit_expr(&stmt.expr, state); } + ast::Stmt::SetBlock(stmt) => { + assign_nested(&stmt.target, state); + state.push(); + stmt.body.iter().for_each(|x| walk(x, state)); + state.pop(); + } ast::Stmt::Block(stmt) => { state.push(); state.assign("super"); @@ -241,6 +247,7 @@ pub fn find_referenced_templates(source: &str) -> Result, Error> match node { ast::Stmt::Template(stmt) => stmt.children.iter().for_each(|x| walk(x, out)), ast::Stmt::EmitExpr(_) | ast::Stmt::EmitRaw(_) | ast::Stmt::Set(_) => {} + ast::Stmt::SetBlock(stmt) => stmt.body.iter().for_each(|x| walk(x, out)), ast::Stmt::ForLoop(stmt) => stmt .body .iter() diff --git a/minijinja/src/parser.rs b/minijinja/src/parser.rs index e58c27f5..fba60edd 100644 --- a/minijinja/src/parser.rs +++ b/minijinja/src/parser.rs @@ -56,6 +56,11 @@ macro_rules! expect_token { }}; } +enum SetParseResult<'a> { + Set(ast::Set<'a>), + SetBlock(ast::SetBlock<'a>), +} + struct TokenStream<'a> { iter: Box, Span), Error>> + 'a>, current: Option, Span), Error>>, @@ -547,10 +552,14 @@ impl<'a> Parser<'a> { self.parse_with_block()?, self.stream.expand_span(span), ))), - Token::Ident("set") => Ok(ast::Stmt::Set(Spanned::new( - self.parse_set()?, - self.stream.expand_span(span), - ))), + Token::Ident("set") => Ok(match self.parse_set()? { + SetParseResult::Set(rv) => { + ast::Stmt::Set(Spanned::new(rv, self.stream.expand_span(span))) + } + SetParseResult::SetBlock(rv) => { + ast::Stmt::SetBlock(Spanned::new(rv, self.stream.expand_span(span))) + } + }), Token::Ident("block") => Ok(ast::Stmt::Block(Spanned::new( self.parse_block()?, self.stream.expand_span(span), @@ -722,19 +731,41 @@ impl<'a> Parser<'a> { Ok(ast::WithBlock { assignments, body }) } - fn parse_set(&mut self) -> Result, Error> { - let target = if matches!(self.stream.current()?, Some((Token::ParenOpen, _))) { + fn parse_set(&mut self) -> Result, Error> { + let (target, in_paren) = if matches!(self.stream.current()?, Some((Token::ParenOpen, _))) { self.stream.next()?; let assign = self.parse_assignment()?; expect_token!(self, Token::ParenClose, "`)`")?; - assign + (assign, true) } else { - self.parse_assign_name()? + (self.parse_assign_name()?, false) }; - expect_token!(self, Token::Assign, "assignment operator")?; - let expr = self.parse_expr()?; - Ok(ast::Set { target, expr }) + if !in_paren + && matches!( + self.stream.current()?, + Some((Token::BlockEnd(..), _)) | Some((Token::Pipe, _)) + ) + { + let filter = if matches!(self.stream.current()?, Some((Token::Pipe, _))) { + self.stream.next()?; + Some(self.parse_filter_chain()?) + } else { + None + }; + expect_token!(self, Token::BlockEnd(..), "end of block")?; + let body = self.subparse(&|tok| matches!(tok, Token::Ident("endset")))?; + self.stream.next()?; + Ok(SetParseResult::SetBlock(ast::SetBlock { + target, + filter, + body, + })) + } else { + expect_token!(self, Token::Assign, "assignment operator")?; + let expr = self.parse_expr()?; + Ok(SetParseResult::Set(ast::Set { target, expr })) + } } fn parse_block(&mut self) -> Result, Error> { @@ -786,7 +817,7 @@ impl<'a> Parser<'a> { Ok(ast::AutoEscape { enabled, body }) } - fn parse_filter_block(&mut self) -> Result, Error> { + fn parse_filter_chain(&mut self) -> Result, Error> { let mut filter = None; while !matches!(self.stream.current()?, Some((Token::BlockEnd(..), _))) { @@ -809,9 +840,11 @@ impl<'a> Parser<'a> { ))); } - let filter = filter - .ok_or_else(|| Error::new(ErrorKind::InvalidSyntax, "filter block without filter"))?; + filter.ok_or_else(|| Error::new(ErrorKind::InvalidSyntax, "expected a filter")) + } + fn parse_filter_block(&mut self) -> Result, Error> { + let filter = self.parse_filter_chain()?; expect_token!(self, Token::BlockEnd(..), "end of block")?; let body = self.subparse(&|tok| matches!(tok, Token::Ident("endfilter")))?; self.stream.next()?; diff --git a/minijinja/src/syntax.rs b/minijinja/src/syntax.rs index 79dff939..06a01378 100644 --- a/minijinja/src/syntax.rs +++ b/minijinja/src/syntax.rs @@ -419,6 +419,26 @@ //! and have them show up outside of it. This also applies to loops. The only //! exception to that rule are if statements which do not introduce a scope. //! +//! It's also possible to capture blocks of template code into a variable by using +//! the `set` statement as a block. In that case, instead of using an equals sign +//! and a value, you just write the variable name and then everything until +//! `{% endset %}` is captured. +//! +//! ```jinja +//! {% set navigation %} +//!
  • Index +//!
  • Downloads +//! {% endset %} +//! ``` +//! +//! The `navigation` variable then contains the navigation HTML source. +//! +//! This can also be combined with applying a filter: +//! +//! ```jinja +//! {% set title | upper %}Title of the page{% endset %} +//! ``` +//! //! ## `{% filter %}` //! //! Filter sections allow you to apply regular [filters](crate::filters) on a diff --git a/minijinja/tests/inputs/set.txt b/minijinja/tests/inputs/set.txt index dde4e7ab..fd27d65c 100644 --- a/minijinja/tests/inputs/set.txt +++ b/minijinja/tests/inputs/set.txt @@ -32,4 +32,12 @@ world" %} Multiline: {% set multiline = "hello world" %} -{{ multiline }} \ No newline at end of file +{{ multiline }} + +Block: +{% set var %}This is a {{ foo }}{% endset %} +[{{ var }}] + +Filter block +{% set upper_var | upper %}This is a {{ foo }}{% endset %} +[{{ upper_var }}] diff --git a/minijinja/tests/parser-inputs/set.txt b/minijinja/tests/parser-inputs/set.txt index c266ac00..a862cd51 100644 --- a/minijinja/tests/parser-inputs/set.txt +++ b/minijinja/tests/parser-inputs/set.txt @@ -1,2 +1,8 @@ {% set variable = value %} -{% set (a, b) = (1, 2) %} \ No newline at end of file +{% set (a, b) = (1, 2) %} +{% set variable2 %} + this is the {{ body }} +{% endset %} +{% set variable3 | upper %} + this is the {{ body }} with filter +{% endset %} diff --git a/minijinja/tests/snapshots/test_parser__parser@set.txt.snap b/minijinja/tests/snapshots/test_parser__parser@set.txt.snap index e6c8118e..31abbe1a 100644 --- a/minijinja/tests/snapshots/test_parser__parser@set.txt.snap +++ b/minijinja/tests/snapshots/test_parser__parser@set.txt.snap @@ -39,6 +39,56 @@ Ok( ], } @ 2:16-2:21, } @ 2:3-2:22, + EmitRaw { + raw: "\n", + } @ 2:25-3:0, + SetBlock { + target: Var { + id: "variable2", + } @ 3:7-3:16, + filter: None, + body: [ + EmitRaw { + raw: "\n this is the ", + } @ 3:19-4:16, + EmitExpr { + expr: Var { + id: "body", + } @ 4:19-4:23, + } @ 4:16-4:23, + EmitRaw { + raw: "\n", + } @ 4:26-5:0, + ], + } @ 3:3-5:9, + EmitRaw { + raw: "\n", + } @ 5:12-6:0, + SetBlock { + target: Var { + id: "variable3", + } @ 6:7-6:16, + filter: Some( + Filter { + name: "upper", + expr: None, + args: [], + } @ 6:19-6:24, + ), + body: [ + EmitRaw { + raw: "\n this is the ", + } @ 6:27-7:16, + EmitExpr { + expr: Var { + id: "body", + } @ 7:19-7:23, + } @ 7:16-7:23, + EmitRaw { + raw: " with filter\n", + } @ 7:26-8:0, + ], + } @ 6:3-8:9, ], - } @ 0:0-2:25, + } @ 0:0-8:12, ) diff --git a/minijinja/tests/snapshots/test_templates__vm@set.txt.snap b/minijinja/tests/snapshots/test_templates__vm@set.txt.snap index b1f842b2..dc12e4ae 100644 --- a/minijinja/tests/snapshots/test_templates__vm@set.txt.snap +++ b/minijinja/tests/snapshots/test_templates__vm@set.txt.snap @@ -43,3 +43,11 @@ Multiline: hello world +Block: + +[This is a was true] + +Filter block + +[THIS IS A WAS TRUE] +