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..,
alt((
TokenKind::Content.map(TemplateAstExpr::StaticContent),
parse_interpolation,
)),
)
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 {
kind: WantsOutput,
source: "=",
},
),
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);
}
}