Add parsing of simple conditionals

Signed-off-by: Marcel Müller <neikos@neikos.email>
This commit is contained in:
Marcel Müller 2026-03-07 11:49:40 +01:00
parent ffb5c92b89
commit 974086a877
7 changed files with 392 additions and 119 deletions

View file

@ -4,7 +4,9 @@ use winnow::RecoverableParser;
use winnow::combinator::alt;
use winnow::combinator::cut_err;
use winnow::combinator::delimited;
use winnow::combinator::not;
use winnow::combinator::opt;
use winnow::combinator::preceded;
use winnow::combinator::repeat;
use winnow::combinator::repeat_till;
use winnow::error::AddContext;
@ -140,7 +142,7 @@ impl<'input> Parser<Input<'input>, TemplateToken, AstError> for TokenKind {
}
pub fn parse(input: &[TemplateToken]) -> Result<TemplateAst<'_>, AstFailure> {
let (_remaining, val, errors) = parse_ast.recoverable_parse(TokenSlice::new(input));
let (_remaining, val, errors) = parse_asts.recoverable_parse(TokenSlice::new(input));
if errors.is_empty()
&& let Some(val) = val
@ -155,23 +157,46 @@ pub fn parse(input: &[TemplateToken]) -> Result<TemplateAst<'_>, AstFailure> {
pub enum TemplateAstExpr<'input> {
StaticContent(TemplateToken),
Interpolation {
prev_whitespace: Option<TemplateToken>,
wants_output: Option<TemplateToken>,
prev_whitespace_content: Option<TemplateToken>,
wants_output: TemplateToken,
expression: Box<TemplateAstExpr<'input>>,
post_whitespace: Option<TemplateToken>,
post_whitespace_content: Option<TemplateToken>,
},
Action {
prev_whitespace_content: Option<TemplateToken>,
expression: Box<TemplateAstExpr<'input>>,
post_whitespace_content: Option<TemplateToken>,
},
VariableAccess(TemplateToken),
ConditionalChain {
chain: Vec<TemplateAstExpr<'input>>,
},
IfConditional {
expression: Box<TemplateAstExpr<'input>>,
content: Vec<TemplateAstExpr<'input>>,
end_block: Box<TemplateAstExpr<'input>>,
},
ElseConditional {
expression: Vec<TemplateAstExpr<'input>>,
},
Invalid(&'input [TemplateToken]),
EndBlock,
Block {
prev_whitespace_content: Option<TemplateToken>,
expression: Box<TemplateAstExpr<'input>>,
post_whitespace_content: Option<TemplateToken>,
},
}
fn parse_ast<'input>(input: &mut Input<'input>) -> Result<Vec<TemplateAstExpr<'input>>, AstError> {
repeat(
0..,
fn parse_asts<'input>(input: &mut Input<'input>) -> Result<Vec<TemplateAstExpr<'input>>, AstError> {
repeat(0.., parse_ast).parse_next(input)
}
fn parse_ast<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, AstError> {
alt((
TokenKind::Content.map(TemplateAstExpr::StaticContent),
parse_interpolation,
)),
)
parse_action,
))
.parse_next(input)
}
@ -179,16 +204,16 @@ fn parse_interpolation<'input>(
input: &mut Input<'input>,
) -> Result<TemplateAstExpr<'input>, AstError> {
let expr_parser = resume_after_cut(
alt((parse_variable_access,)),
parse_value_expression,
repeat_till(1.., any, TokenKind::RightDelim).map(|((), _)| ()),
)
.with_taken()
.map(|(expr, taken)| expr.unwrap_or(TemplateAstExpr::Invalid(taken)));
let (prev_whitespace, _left, (wants_output, expression, _right, post_whitespace)) = (
let (prev_whitespace, _left, wants_output, (expression, _right, post_whitespace)) = (
opt(TokenKind::Whitespace),
TokenKind::LeftDelim,
TokenKind::WantsOutput,
cut_err((
opt(TokenKind::WantsOutput),
delimited(ignore_ws, expr_parser, ignore_ws).map(Box::new),
TokenKind::RightDelim,
opt(TokenKind::Whitespace),
@ -197,13 +222,89 @@ fn parse_interpolation<'input>(
.parse_next(input)?;
Ok(TemplateAstExpr::Interpolation {
prev_whitespace,
prev_whitespace_content: prev_whitespace,
wants_output,
expression,
post_whitespace,
post_whitespace_content: post_whitespace,
})
}
fn parse_value_expression<'input>(
input: &mut Input<'input>,
) -> Result<TemplateAstExpr<'input>, AstError> {
alt((parse_variable_access,)).parse_next(input)
}
fn parse_action<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, AstError> {
alt((parse_conditional_chain,)).parse_next(input)
}
fn parse_conditional_chain<'input>(
input: &mut Input<'input>,
) -> Result<TemplateAstExpr<'input>, AstError> {
let if_expression = parse_conditional.parse_next(input)?;
let mut chain = vec![];
let (content, end_block): (Vec<_>, _) =
repeat_till(1.., parse_ast, parse_end).parse_next(input)?;
chain.push(TemplateAstExpr::IfConditional {
expression: Box::new(if_expression),
content,
end_block: Box::new(end_block),
});
Ok(TemplateAstExpr::ConditionalChain { chain })
}
fn parse_conditional<'input>(
input: &mut Input<'input>,
) -> Result<TemplateAstExpr<'input>, AstError> {
parse_block(preceded(
TokenKind::ConditionalIf,
surrounded(ignore_ws, parse_value_expression),
))
.parse_next(input)
}
fn parse_end<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, AstError> {
parse_block(TokenKind::End.value(TemplateAstExpr::EndBlock)).parse_next(input)
}
fn parse_block<'input, ParseNext>(
parser: ParseNext,
) -> impl Parser<Input<'input>, TemplateAstExpr<'input>, AstError>
where
ParseNext: Parser<Input<'input>, TemplateAstExpr<'input>, AstError>,
{
let expr_parser = resume_after_cut(
parser,
repeat_till(1.., any, TokenKind::RightDelim).map(|((), _)| ()),
)
.with_taken()
.map(|(expr, taken)| expr.unwrap_or(TemplateAstExpr::Invalid(taken)));
(
opt(TokenKind::Whitespace),
TokenKind::LeftDelim,
not(TokenKind::WantsOutput),
cut_err((
delimited(ignore_ws, expr_parser.map(Box::new), ignore_ws),
TokenKind::RightDelim,
opt(TokenKind::Whitespace),
)),
)
.map(
|(prev_whitespace, _left, _not_token, (expression, _right, post_whitespace))| {
TemplateAstExpr::Block {
prev_whitespace_content: prev_whitespace,
expression,
post_whitespace_content: post_whitespace,
}
},
)
}
fn parse_variable_access<'input>(
input: &mut Input<'input>,
) -> Result<TemplateAstExpr<'input>, AstError> {
@ -216,6 +317,20 @@ fn ignore_ws<'input>(input: &mut Input<'input>) -> Result<(), AstError> {
repeat(.., TokenKind::Whitespace).parse_next(input)
}
fn surrounded<Input, IgnoredParser, Ignored1, Output, Error, ParseNext>(
ignored: IgnoredParser,
parser: ParseNext,
) -> impl Parser<Input, Output, Error>
where
Input: Stream,
Error: ParserError<Input>,
IgnoredParser: Parser<Input, Ignored1, Error>,
IgnoredParser: Clone,
ParseNext: Parser<Input, Output, Error>,
{
delimited(ignored.clone(), parser, ignored)
}
#[cfg(test)]
mod tests {
use crate::ast::parse;
@ -260,28 +375,96 @@ mod tests {
},
),
Interpolation {
prev_whitespace: Some(
prev_whitespace_content: Some(
TemplateToken {
kind: Whitespace,
source: " ",
},
),
wants_output: Some(
TemplateToken {
wants_output: TemplateToken {
kind: WantsOutput,
source: "=",
},
),
expression: VariableAccess(
TemplateToken {
kind: Ident,
source: "world",
},
),
post_whitespace: None,
post_whitespace_content: None,
},
],
}
"#);
}
#[test]
fn check_simple_if() {
let input = "{{ if foo }} Hiii {{ end }}";
let parsed = crate::parser::parse(input.into()).unwrap();
let ast = parse(parsed.tokens()).unwrap();
insta::assert_debug_snapshot!(ast, @r#"
TemplateAst {
root: [
ConditionalChain {
chain: [
IfConditional {
expression: Block {
prev_whitespace_content: None,
expression: VariableAccess(
TemplateToken {
kind: Ident,
source: "foo",
},
),
post_whitespace_content: Some(
TemplateToken {
kind: Whitespace,
source: " ",
},
),
},
content: [
StaticContent(
TemplateToken {
kind: Content,
source: "Hiii",
},
),
],
end_block: Block {
prev_whitespace_content: Some(
TemplateToken {
kind: Whitespace,
source: " ",
},
),
expression: EndBlock,
post_whitespace_content: None,
},
},
],
},
],
}
"#);
}
#[test]
fn check_nested_simple_if() {
let input = r#"{{ if foo }}
{{ if bar }}
Hiii
{{ end }}
{{ end }}"#;
let parsed = crate::parser::parse(input.into()).unwrap();
let ast = parse(parsed.tokens()).unwrap();
insta::assert_debug_snapshot!(ast);
}
}

View file

@ -0,0 +1,84 @@
---
source: src/ast/mod.rs
expression: ast
---
TemplateAst {
root: [
ConditionalChain {
chain: [
IfConditional {
expression: Block {
prev_whitespace_content: None,
expression: VariableAccess(
TemplateToken {
kind: Ident,
source: "foo",
},
),
post_whitespace_content: Some(
TemplateToken {
kind: Whitespace,
source: "
",
},
),
},
content: [
ConditionalChain {
chain: [
IfConditional {
expression: Block {
prev_whitespace_content: None,
expression: VariableAccess(
TemplateToken {
kind: Ident,
source: "bar",
},
),
post_whitespace_content: Some(
TemplateToken {
kind: Whitespace,
source: "
",
},
),
},
content: [
StaticContent(
TemplateToken {
kind: Content,
source: "Hiii",
},
),
],
end_block: Block {
prev_whitespace_content: Some(
TemplateToken {
kind: Whitespace,
source: "
",
},
),
expression: EndBlock,
post_whitespace_content: Some(
TemplateToken {
kind: Whitespace,
source: "
",
},
),
},
},
],
},
],
end_block: Block {
prev_whitespace_content: None,
expression: EndBlock,
post_whitespace_content: None,
},
},
],
},
],
}

View file

@ -24,19 +24,10 @@ pub struct VariableSlot {
#[derive(Debug)]
pub enum Instruction {
AppendContent {
content: NomoInput,
},
LoadFromContextToSlot {
name: NomoInput,
slot: VariableSlot,
},
EmitFromSlot {
slot: VariableSlot,
},
PushScope {
inherit_parent: bool,
},
AppendContent { content: NomoInput },
LoadFromContextToSlot { name: NomoInput, slot: VariableSlot },
EmitFromSlot { slot: VariableSlot },
PushScope { inherit_parent: bool },
Abort,
}
@ -64,10 +55,10 @@ fn emit_ast_expr(
});
}
TemplateAstExpr::Interpolation {
prev_whitespace,
prev_whitespace_content: prev_whitespace,
wants_output,
expression,
post_whitespace,
post_whitespace_content: post_whitespace,
} => {
if let Some(ws) = prev_whitespace {
eval.push(Instruction::AppendContent {
@ -77,10 +68,7 @@ fn emit_ast_expr(
let emit_slot = machine.reserve_slot();
emit_expr(machine, eval, emit_slot, expression);
if wants_output.is_some() {
eval.push(Instruction::EmitFromSlot { slot: emit_slot });
}
if let Some(ws) = post_whitespace {
eval.push(Instruction::AppendContent {
@ -91,6 +79,24 @@ fn emit_ast_expr(
TemplateAstExpr::Invalid { .. } | TemplateAstExpr::VariableAccess { .. } => {
eval.push(Instruction::Abort)
}
TemplateAstExpr::ConditionalChain { chain } => todo!(),
TemplateAstExpr::ElseConditional { expression } => todo!(),
TemplateAstExpr::Action {
prev_whitespace_content,
expression,
post_whitespace_content,
} => todo!(),
TemplateAstExpr::EndBlock => todo!(),
TemplateAstExpr::Block {
prev_whitespace_content,
expression,
post_whitespace_content,
} => todo!(),
TemplateAstExpr::IfConditional {
expression,
content,
end_block,
} => todo!(),
}
}
@ -111,6 +117,24 @@ fn emit_expr(
TemplateAstExpr::StaticContent { .. } | TemplateAstExpr::Interpolation { .. } => {
unreachable!("Invalid AST here")
}
TemplateAstExpr::ConditionalChain { chain } => todo!(),
TemplateAstExpr::ElseConditional { expression } => todo!(),
TemplateAstExpr::Action {
prev_whitespace_content,
expression,
post_whitespace_content,
} => todo!(),
TemplateAstExpr::EndBlock => todo!(),
TemplateAstExpr::Block {
prev_whitespace_content,
expression,
post_whitespace_content,
} => todo!(),
TemplateAstExpr::IfConditional {
expression,
content,
end_block,
} => todo!(),
}
}

View file

@ -420,7 +420,7 @@ fn parse_ident<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateTok
}
fn ident<'input>(input: &mut Input<'input>) -> PResult<'input, NomoInput> {
peek(not(parse_literal))
peek(not(alt((parse_literal, parse_condition, parse_end))))
.context(ParseError::ctx().msg("Expected an ident, but found a literal instead"))
.parse_next(input)?;
@ -564,7 +564,7 @@ mod tests {
source: " ",
},
TemplateToken {
kind: Ident,
kind: ConditionalIf,
source: "if",
},
TemplateToken {
@ -608,7 +608,7 @@ mod tests {
source: " ",
},
TemplateToken {
kind: Ident,
kind: End,
source: "end",
},
TemplateToken {

View file

@ -6,20 +6,18 @@ input_file: tests/cases/identifiers.nomo
TemplateAst {
root: [
Interpolation {
prev_whitespace: None,
wants_output: Some(
TemplateToken {
prev_whitespace_content: None,
wants_output: TemplateToken {
kind: WantsOutput,
source: "=",
},
),
expression: VariableAccess(
TemplateToken {
kind: Ident,
source: "_name",
},
),
post_whitespace: Some(
post_whitespace_content: Some(
TemplateToken {
kind: Whitespace,
source: "
@ -28,20 +26,18 @@ TemplateAst {
),
},
Interpolation {
prev_whitespace: None,
wants_output: Some(
TemplateToken {
prev_whitespace_content: None,
wants_output: TemplateToken {
kind: WantsOutput,
source: "=",
},
),
expression: VariableAccess(
TemplateToken {
kind: Ident,
source: "a_name",
},
),
post_whitespace: Some(
post_whitespace_content: Some(
TemplateToken {
kind: Whitespace,
source: "
@ -50,20 +46,18 @@ TemplateAst {
),
},
Interpolation {
prev_whitespace: None,
wants_output: Some(
TemplateToken {
prev_whitespace_content: None,
wants_output: TemplateToken {
kind: WantsOutput,
source: "=",
},
),
expression: VariableAccess(
TemplateToken {
kind: Ident,
source: "1name",
},
),
post_whitespace: Some(
post_whitespace_content: Some(
TemplateToken {
kind: Whitespace,
source: "
@ -72,20 +66,18 @@ TemplateAst {
),
},
Interpolation {
prev_whitespace: None,
wants_output: Some(
TemplateToken {
prev_whitespace_content: None,
wants_output: TemplateToken {
kind: WantsOutput,
source: "=",
},
),
expression: VariableAccess(
TemplateToken {
kind: Ident,
source: "_name1",
},
),
post_whitespace: Some(
post_whitespace_content: Some(
TemplateToken {
kind: Whitespace,
source: "
@ -94,20 +86,18 @@ TemplateAst {
),
},
Interpolation {
prev_whitespace: None,
wants_output: Some(
TemplateToken {
prev_whitespace_content: None,
wants_output: TemplateToken {
kind: WantsOutput,
source: "=",
},
),
expression: VariableAccess(
TemplateToken {
kind: Ident,
source: "_namE",
},
),
post_whitespace: Some(
post_whitespace_content: Some(
TemplateToken {
kind: Whitespace,
source: "
@ -116,20 +106,18 @@ TemplateAst {
),
},
Interpolation {
prev_whitespace: None,
wants_output: Some(
TemplateToken {
prev_whitespace_content: None,
wants_output: TemplateToken {
kind: WantsOutput,
source: "=",
},
),
expression: VariableAccess(
TemplateToken {
kind: Ident,
source: "name1",
},
),
post_whitespace: None,
post_whitespace_content: None,
},
],
}

View file

@ -12,25 +12,23 @@ TemplateAst {
},
),
Interpolation {
prev_whitespace: Some(
prev_whitespace_content: Some(
TemplateToken {
kind: Whitespace,
source: " ",
},
),
wants_output: Some(
TemplateToken {
wants_output: TemplateToken {
kind: WantsOutput,
source: "=",
},
),
expression: VariableAccess(
TemplateToken {
kind: Ident,
source: "name",
},
),
post_whitespace: None,
post_whitespace_content: None,
},
],
}

View file

@ -12,25 +12,23 @@ TemplateAst {
},
),
Interpolation {
prev_whitespace: Some(
prev_whitespace_content: Some(
TemplateToken {
kind: Whitespace,
source: " ",
},
),
wants_output: Some(
TemplateToken {
wants_output: TemplateToken {
kind: WantsOutput,
source: "=",
},
),
expression: VariableAccess(
TemplateToken {
kind: Ident,
source: "name",
},
),
post_whitespace: Some(
post_whitespace_content: Some(
TemplateToken {
kind: Whitespace,
source: " ",
@ -38,20 +36,18 @@ TemplateAst {
),
},
Interpolation {
prev_whitespace: None,
wants_output: Some(
TemplateToken {
prev_whitespace_content: None,
wants_output: TemplateToken {
kind: WantsOutput,
source: "=",
},
),
expression: VariableAccess(
TemplateToken {
kind: Ident,
source: "lastname",
},
),
post_whitespace: None,
post_whitespace_content: None,
},
],
}