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::alt;
use winnow::combinator::cut_err; use winnow::combinator::cut_err;
use winnow::combinator::delimited; use winnow::combinator::delimited;
use winnow::combinator::not;
use winnow::combinator::opt; use winnow::combinator::opt;
use winnow::combinator::preceded;
use winnow::combinator::repeat; use winnow::combinator::repeat;
use winnow::combinator::repeat_till; use winnow::combinator::repeat_till;
use winnow::error::AddContext; 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> { 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() if errors.is_empty()
&& let Some(val) = val && let Some(val) = val
@ -155,23 +157,46 @@ pub fn parse(input: &[TemplateToken]) -> Result<TemplateAst<'_>, AstFailure> {
pub enum TemplateAstExpr<'input> { pub enum TemplateAstExpr<'input> {
StaticContent(TemplateToken), StaticContent(TemplateToken),
Interpolation { Interpolation {
prev_whitespace: Option<TemplateToken>, prev_whitespace_content: Option<TemplateToken>,
wants_output: Option<TemplateToken>, wants_output: TemplateToken,
expression: Box<TemplateAstExpr<'input>>, 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), 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]), 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> { fn parse_asts<'input>(input: &mut Input<'input>) -> Result<Vec<TemplateAstExpr<'input>>, AstError> {
repeat( repeat(0.., parse_ast).parse_next(input)
0.., }
alt(( fn parse_ast<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, AstError> {
TokenKind::Content.map(TemplateAstExpr::StaticContent), alt((
parse_interpolation, TokenKind::Content.map(TemplateAstExpr::StaticContent),
)), parse_interpolation,
) parse_action,
))
.parse_next(input) .parse_next(input)
} }
@ -179,16 +204,16 @@ fn parse_interpolation<'input>(
input: &mut Input<'input>, input: &mut Input<'input>,
) -> Result<TemplateAstExpr<'input>, AstError> { ) -> Result<TemplateAstExpr<'input>, AstError> {
let expr_parser = resume_after_cut( let expr_parser = resume_after_cut(
alt((parse_variable_access,)), parse_value_expression,
repeat_till(1.., any, TokenKind::RightDelim).map(|((), _)| ()), repeat_till(1.., any, TokenKind::RightDelim).map(|((), _)| ()),
) )
.with_taken() .with_taken()
.map(|(expr, taken)| expr.unwrap_or(TemplateAstExpr::Invalid(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), opt(TokenKind::Whitespace),
TokenKind::LeftDelim, TokenKind::LeftDelim,
TokenKind::WantsOutput,
cut_err(( cut_err((
opt(TokenKind::WantsOutput),
delimited(ignore_ws, expr_parser, ignore_ws).map(Box::new), delimited(ignore_ws, expr_parser, ignore_ws).map(Box::new),
TokenKind::RightDelim, TokenKind::RightDelim,
opt(TokenKind::Whitespace), opt(TokenKind::Whitespace),
@ -197,13 +222,89 @@ fn parse_interpolation<'input>(
.parse_next(input)?; .parse_next(input)?;
Ok(TemplateAstExpr::Interpolation { Ok(TemplateAstExpr::Interpolation {
prev_whitespace, prev_whitespace_content: prev_whitespace,
wants_output, wants_output,
expression, 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>( fn parse_variable_access<'input>(
input: &mut Input<'input>, input: &mut Input<'input>,
) -> Result<TemplateAstExpr<'input>, AstError> { ) -> Result<TemplateAstExpr<'input>, AstError> {
@ -216,6 +317,20 @@ fn ignore_ws<'input>(input: &mut Input<'input>) -> Result<(), AstError> {
repeat(.., TokenKind::Whitespace).parse_next(input) 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)] #[cfg(test)]
mod tests { mod tests {
use crate::ast::parse; use crate::ast::parse;
@ -260,28 +375,96 @@ mod tests {
}, },
), ),
Interpolation { Interpolation {
prev_whitespace: Some( prev_whitespace_content: Some(
TemplateToken { TemplateToken {
kind: Whitespace, kind: Whitespace,
source: " ", source: " ",
}, },
), ),
wants_output: Some( wants_output: TemplateToken {
TemplateToken { kind: WantsOutput,
kind: WantsOutput, source: "=",
source: "=", },
},
),
expression: VariableAccess( expression: VariableAccess(
TemplateToken { TemplateToken {
kind: Ident, kind: Ident,
source: "world", 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)] #[derive(Debug)]
pub enum Instruction { pub enum Instruction {
AppendContent { AppendContent { content: NomoInput },
content: NomoInput, LoadFromContextToSlot { name: NomoInput, slot: VariableSlot },
}, EmitFromSlot { slot: VariableSlot },
LoadFromContextToSlot { PushScope { inherit_parent: bool },
name: NomoInput,
slot: VariableSlot,
},
EmitFromSlot {
slot: VariableSlot,
},
PushScope {
inherit_parent: bool,
},
Abort, Abort,
} }
@ -64,10 +55,10 @@ fn emit_ast_expr(
}); });
} }
TemplateAstExpr::Interpolation { TemplateAstExpr::Interpolation {
prev_whitespace, prev_whitespace_content: prev_whitespace,
wants_output, wants_output,
expression, expression,
post_whitespace, post_whitespace_content: post_whitespace,
} => { } => {
if let Some(ws) = prev_whitespace { if let Some(ws) = prev_whitespace {
eval.push(Instruction::AppendContent { eval.push(Instruction::AppendContent {
@ -77,10 +68,7 @@ fn emit_ast_expr(
let emit_slot = machine.reserve_slot(); let emit_slot = machine.reserve_slot();
emit_expr(machine, eval, emit_slot, expression); emit_expr(machine, eval, emit_slot, expression);
eval.push(Instruction::EmitFromSlot { slot: emit_slot });
if wants_output.is_some() {
eval.push(Instruction::EmitFromSlot { slot: emit_slot });
}
if let Some(ws) = post_whitespace { if let Some(ws) = post_whitespace {
eval.push(Instruction::AppendContent { eval.push(Instruction::AppendContent {
@ -91,6 +79,24 @@ fn emit_ast_expr(
TemplateAstExpr::Invalid { .. } | TemplateAstExpr::VariableAccess { .. } => { TemplateAstExpr::Invalid { .. } | TemplateAstExpr::VariableAccess { .. } => {
eval.push(Instruction::Abort) 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 { .. } => { TemplateAstExpr::StaticContent { .. } | TemplateAstExpr::Interpolation { .. } => {
unreachable!("Invalid AST here") 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> { 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")) .context(ParseError::ctx().msg("Expected an ident, but found a literal instead"))
.parse_next(input)?; .parse_next(input)?;
@ -564,7 +564,7 @@ mod tests {
source: " ", source: " ",
}, },
TemplateToken { TemplateToken {
kind: Ident, kind: ConditionalIf,
source: "if", source: "if",
}, },
TemplateToken { TemplateToken {
@ -608,7 +608,7 @@ mod tests {
source: " ", source: " ",
}, },
TemplateToken { TemplateToken {
kind: Ident, kind: End,
source: "end", source: "end",
}, },
TemplateToken { TemplateToken {

View file

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

View file

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

View file

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