From e64256b65fffe8c78c76b23198a20bba6a25d7f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Wed, 11 Mar 2026 14:00:45 +0100 Subject: [PATCH] Add ast parsing for for loops MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- src/ast/mod.rs | 97 +++++++++++++++++++ .../nomo__ast__tests__check_for_loop.snap | 61 ++++++++++++ src/emit/mod.rs | 7 ++ 3 files changed, 165 insertions(+) create mode 100644 src/ast/snapshots/nomo__ast__tests__check_for_loop.snap diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 5be6458..fa78ccc 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -230,6 +230,18 @@ pub enum TemplateAstExpr<'input> { ConditionalChain { chain: Vec>, }, + ForChain { + for_block: Box>, + content: Vec>, + else_block: Option>>, + else_content: Option>>, + end_block: Box>, + }, + For { + value_ident: TemplateToken, + value_expression: Box>, + }, + ForElse, VariableAccess(TemplateToken), IfConditional { expression: Box>, @@ -316,6 +328,7 @@ fn parse_action<'input>(input: &mut Input<'input>) -> Result(input: &mut Input<'input>) -> Result(input: &mut Input<'input>) -> Result, AstError> { + trace("for_loop", |input: &mut Input<'input>| { + let for_block = parse_for_loop.map(Box::new).parse_next(input)?; + + let loop_end = ( + opt(( + parse_loop_else.map(Box::new), + repeat_till(0.., parse_ast, peek(parse_end)).map(|(val, _)| val), + )), + parse_end.map(Box::new), + ); + + let (content, taken) = resume_after_cut( + repeat_till(0.., parse_ast, loop_end), + repeat_till(0.., any, parse_end).map(|((), _)| ()), + ) + .with_taken() + .parse_next(input)?; + + let Some((content, (else_stuff, end_block))) = content else { + return Ok(TemplateAstExpr::Invalid(taken)); + }; + + let (else_block, else_content) = else_stuff.unzip(); + + Ok(TemplateAstExpr::ForChain { + for_block, + content, + else_block, + else_content, + end_block, + }) + }) + .parse_next(input) +} + +fn parse_for_loop<'input>(input: &mut Input<'input>) -> Result, AstError> { + trace( + "for_loop_inner", + parse_block( + ( + ws, + TokenKind::For, + ws, + TokenKind::Ident, + ws, + TokenKind::In, + ws, + parse_value_expression.map(Box::new), + ) + .map(|(_, _for, _, value_ident, _, _in, _, value_expression)| { + TemplateAstExpr::For { + value_ident, + value_expression, + } + }), + ), + ) + .parse_next(input) +} + fn parse_conditional_chain<'input>( input: &mut Input<'input>, ) -> Result, AstError> { @@ -447,6 +521,18 @@ fn parse_conditional_else<'input>( .parse_next(input) } +fn parse_loop_else<'input>(input: &mut Input<'input>) -> Result, AstError> { + trace( + "end", + parse_block( + TokenKind::ConditionalElse + .value(TemplateAstExpr::ForElse) + .context(AstError::ctx().msg("Expected an else block here")), + ), + ) + .parse_next(input) +} + fn parse_end<'input>(input: &mut Input<'input>) -> Result, AstError> { trace( "end", @@ -815,4 +901,15 @@ mod tests { insta::assert_debug_snapshot!(ast); } + + #[test] + fn check_for_loop() { + let input = "{{ for value in array }} Hi: {{= value }} {{ else }} No Content :C {{ end }}"; + + let parsed = crate::parser::parse(input.into()).unwrap(); + + let ast = panic_pretty(input, parse(parsed.tokens())); + + insta::assert_debug_snapshot!(ast); + } } diff --git a/src/ast/snapshots/nomo__ast__tests__check_for_loop.snap b/src/ast/snapshots/nomo__ast__tests__check_for_loop.snap new file mode 100644 index 0000000..737e2c0 --- /dev/null +++ b/src/ast/snapshots/nomo__ast__tests__check_for_loop.snap @@ -0,0 +1,61 @@ +--- +source: src/ast/mod.rs +expression: ast +--- +TemplateAst { + root: [ + ForChain { + for_block: Block { + prev_whitespace_content: None, + expression: For { + value_ident: [Ident]"value" (7..12), + value_expression: VariableAccess( + [Ident]"array" (16..21), + ), + }, + post_whitespace_content: Some( + [Whitespace]" " (24..25), + ), + }, + content: [ + StaticContent( + [Content]"Hi:" (25..28), + ), + Interpolation { + prev_whitespace_content: Some( + [Whitespace]" " (28..29), + ), + expression: VariableAccess( + [Ident]"value" (33..38), + ), + post_whitespace_content: Some( + [Whitespace]" " (41..42), + ), + }, + ], + else_block: Some( + Block { + prev_whitespace_content: None, + expression: ForElse, + post_whitespace_content: Some( + [Whitespace]" " (52..53), + ), + }, + ), + else_content: Some( + [ + StaticContent( + [Content]"No Content :C" (53..66), + ), + ], + ), + end_block: Block { + prev_whitespace_content: Some( + [Whitespace]" " (66..67), + ), + expression: EndBlock, + post_whitespace_content: None, + }, + }, + ], +} diff --git a/src/emit/mod.rs b/src/emit/mod.rs index daf8e0d..04930c6 100644 --- a/src/emit/mod.rs +++ b/src/emit/mod.rs @@ -205,6 +205,9 @@ fn emit_ast_expr( | TemplateAstExpr::IfConditional { .. } | TemplateAstExpr::ConditionalContent { .. } | TemplateAstExpr::ElseConditional { .. } + | TemplateAstExpr::ForChain { .. } + | TemplateAstExpr::For { .. } + | TemplateAstExpr::ForElse | TemplateAstExpr::Invalid { .. } | TemplateAstExpr::VariableAccess { .. } => eval.push(Instruction::Abort), } @@ -231,6 +234,10 @@ fn emit_expr_load( TemplateAstExpr::ElseConditional { .. } => todo!(), TemplateAstExpr::EndBlock => todo!(), TemplateAstExpr::Block { .. } => todo!(), + TemplateAstExpr::ForChain { .. } => todo!(), + TemplateAstExpr::For { .. } => todo!(), + TemplateAstExpr::ForElse => todo!(), + TemplateAstExpr::IfConditional { .. } => todo!(), TemplateAstExpr::ConditionalContent { .. } => todo!(), }