Add parsing for conditionals (cont.)

Signed-off-by: Marcel Müller <neikos@neikos.email>
This commit is contained in:
Marcel Müller 2026-03-08 15:06:29 +01:00
parent 974086a877
commit 8afc2d1bde
29 changed files with 994 additions and 746 deletions

View file

@ -6,18 +6,22 @@ use winnow::combinator::cut_err;
use winnow::combinator::delimited;
use winnow::combinator::not;
use winnow::combinator::opt;
use winnow::combinator::peek;
use winnow::combinator::preceded;
use winnow::combinator::repeat;
use winnow::combinator::repeat_till;
use winnow::combinator::trace;
use winnow::error::AddContext;
use winnow::error::FromRecoverableError;
use winnow::error::ModalError;
use winnow::error::ParserError;
use winnow::stream::Offset;
use winnow::stream::Recoverable;
use winnow::stream::Stream;
use winnow::stream::TokenSlice;
use winnow::token::any;
use crate::SourceSpan;
use crate::parser::TemplateToken;
use crate::parser::TokenKind;
use crate::resume_after_cut;
@ -83,6 +87,29 @@ impl FromRecoverableError<Input<'_>, AstError> for AstError {
input: &Input,
mut e: AstError,
) -> Self {
e.span = e.span.or_else(|| {
let offset = input.offset_from(token_start);
let mut tokens = input
.previous_tokens()
.take(offset)
.filter(|t| t.kind() != TokenKind::Whitespace);
let last = tokens.next();
let first = tokens.last();
match (last, first) {
(None, None) => None,
(None, Some(single)) | (Some(single), None) => Some(SourceSpan {
range: single.source().get_range(),
}),
(Some(last), Some(first)) => {
let start = first.source().get_range().start;
let end = last.source().get_range().end;
Some(SourceSpan { range: start..end })
}
}
});
e
}
}
@ -117,7 +144,9 @@ impl ParserError<Input<'_>> for AstError {
}
#[derive(Debug, Error)]
pub struct AstFailure {}
pub struct AstFailure {
errors: Vec<AstError>,
}
impl std::fmt::Display for AstFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@ -126,8 +155,40 @@ impl std::fmt::Display for AstFailure {
}
impl AstFailure {
fn from_errors(_errors: Vec<AstError>, _input: &[TemplateToken]) -> AstFailure {
AstFailure {}
fn from_errors(errors: Vec<AstError>) -> AstFailure {
AstFailure { errors }
}
pub fn to_report(&self, source: &str) -> String {
let reports = self
.errors
.iter()
.map(|error| {
annotate_snippets::Level::ERROR
.primary_title(
error
.message
.as_deref()
.unwrap_or("An error occurred while producing an Ast"),
)
.element(
annotate_snippets::Snippet::source(source).annotation(
annotate_snippets::AnnotationKind::Primary
.span(error.span.clone().map(|s| s.range).unwrap_or_else(|| 0..0)),
),
)
.elements(
error
.help
.as_ref()
.map(|help| annotate_snippets::Level::HELP.message(help)),
)
})
.collect::<Vec<_>>();
let renderer = annotate_snippets::Renderer::styled()
.decor_style(annotate_snippets::renderer::DecorStyle::Unicode);
renderer.render(&reports)
}
}
@ -149,7 +210,7 @@ pub fn parse(input: &[TemplateToken]) -> Result<TemplateAst<'_>, AstFailure> {
{
Ok(TemplateAst { root: val })
} else {
Err(AstFailure::from_errors(errors, input))
Err(AstFailure::from_errors(errors))
}
}
@ -157,12 +218,6 @@ pub fn parse(input: &[TemplateToken]) -> Result<TemplateAst<'_>, AstFailure> {
pub enum TemplateAstExpr<'input> {
StaticContent(TemplateToken),
Interpolation {
prev_whitespace_content: Option<TemplateToken>,
wants_output: TemplateToken,
expression: Box<TemplateAstExpr<'input>>,
post_whitespace_content: Option<TemplateToken>,
},
Action {
prev_whitespace_content: Option<TemplateToken>,
expression: Box<TemplateAstExpr<'input>>,
post_whitespace_content: Option<TemplateToken>,
@ -172,7 +227,7 @@ pub enum TemplateAstExpr<'input> {
chain: Vec<TemplateAstExpr<'input>>,
},
IfConditional {
expression: Box<TemplateAstExpr<'input>>,
if_block: Box<TemplateAstExpr<'input>>,
content: Vec<TemplateAstExpr<'input>>,
end_block: Box<TemplateAstExpr<'input>>,
},
@ -193,8 +248,11 @@ fn parse_asts<'input>(input: &mut Input<'input>) -> Result<Vec<TemplateAstExpr<'
}
fn parse_ast<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, AstError> {
alt((
TokenKind::Content.map(TemplateAstExpr::StaticContent),
parse_interpolation,
trace(
"content",
TokenKind::Content.map(TemplateAstExpr::StaticContent),
),
trace("interpolation", parse_interpolation),
parse_action,
))
.parse_next(input)
@ -205,16 +263,16 @@ fn parse_interpolation<'input>(
) -> Result<TemplateAstExpr<'input>, AstError> {
let expr_parser = resume_after_cut(
parse_value_expression,
repeat_till(1.., any, TokenKind::RightDelim).map(|((), _)| ()),
repeat_till(0.., any, peek(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((
delimited(ignore_ws, expr_parser, ignore_ws).map(Box::new),
surrounded(ws, expr_parser).map(Box::new),
TokenKind::RightDelim,
opt(TokenKind::Whitespace),
)),
@ -223,52 +281,99 @@ fn parse_interpolation<'input>(
Ok(TemplateAstExpr::Interpolation {
prev_whitespace_content: prev_whitespace,
wants_output,
expression,
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)
trace(
"action",
alt((
parse_conditional_chain,
(parse_block(
cut_err(not(repeat_till(
0..,
any,
peek((ws, TokenKind::RightDelim)),
)
.map(|((), _)| ())))
.context(
AstError::ctx()
.msg("Standlone action block")
.help("If you want to output this expression, add a '=' to the block"),
)
.take()
.map(TemplateAstExpr::Invalid),
)),
)),
)
.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![];
trace("conditional_chain", |input: &mut Input<'input>| {
let if_block = parse_conditional.map(Box::new).parse_next(input)?;
let mut chain = vec![];
let (content, end_block): (Vec<_>, _) =
repeat_till(1.., parse_ast, parse_end).parse_next(input)?;
let (content, end_block): (Vec<_>, _) =
repeat_till(0.., parse_ast, parse_end.map(Box::new)).parse_next(input)?;
chain.push(TemplateAstExpr::IfConditional {
expression: Box::new(if_expression),
content,
end_block: Box::new(end_block),
});
chain.push(TemplateAstExpr::IfConditional {
if_block,
content,
end_block,
});
Ok(TemplateAstExpr::ConditionalChain { chain })
Ok(TemplateAstExpr::ConditionalChain { chain })
})
.parse_next(input)
}
fn parse_conditional<'input>(
input: &mut Input<'input>,
) -> Result<TemplateAstExpr<'input>, AstError> {
parse_block(preceded(
TokenKind::ConditionalIf,
surrounded(ignore_ws, parse_value_expression),
))
trace(
"conditional",
parse_block(preceded(
TokenKind::ConditionalIf,
cut_err(
surrounded(ws, parse_value_expression)
.context(AstError::ctx().msg("Expected an expression after 'if'")),
),
)),
)
.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)
trace(
"end",
parse_block(
TokenKind::End
.value(TemplateAstExpr::EndBlock)
.context(AstError::ctx().msg("Expected an end block here")),
),
)
.parse_next(input)
}
fn parse_value_expression<'input>(
input: &mut Input<'input>,
) -> Result<TemplateAstExpr<'input>, AstError> {
trace("value_expression", alt((parse_variable_access,))).parse_next(input)
}
fn parse_variable_access<'input>(
input: &mut Input<'input>,
) -> Result<TemplateAstExpr<'input>, AstError> {
trace(
"variable_access",
TokenKind::Ident.map(TemplateAstExpr::VariableAccess),
)
.parse_next(input)
}
fn parse_block<'input, ParseNext>(
@ -279,7 +384,7 @@ where
{
let expr_parser = resume_after_cut(
parser,
repeat_till(1.., any, TokenKind::RightDelim).map(|((), _)| ()),
repeat_till(0.., any, peek(TokenKind::RightDelim)).map(|((), _)| ()),
)
.with_taken()
.map(|(expr, taken)| expr.unwrap_or(TemplateAstExpr::Invalid(taken)));
@ -288,11 +393,11 @@ where
opt(TokenKind::Whitespace),
TokenKind::LeftDelim,
not(TokenKind::WantsOutput),
cut_err((
delimited(ignore_ws, expr_parser.map(Box::new), ignore_ws),
(
surrounded(ws, expr_parser.map(Box::new)),
TokenKind::RightDelim,
opt(TokenKind::Whitespace),
)),
),
)
.map(
|(prev_whitespace, _left, _not_token, (expression, _right, post_whitespace))| {
@ -305,15 +410,7 @@ where
)
}
fn parse_variable_access<'input>(
input: &mut Input<'input>,
) -> Result<TemplateAstExpr<'input>, AstError> {
TokenKind::Ident
.map(TemplateAstExpr::VariableAccess)
.parse_next(input)
}
fn ignore_ws<'input>(input: &mut Input<'input>) -> Result<(), AstError> {
fn ws<'input>(input: &mut Input<'input>) -> Result<(), AstError> {
repeat(.., TokenKind::Whitespace).parse_next(input)
}
@ -333,7 +430,19 @@ where
#[cfg(test)]
mod tests {
use winnow::Parser;
use winnow::combinator::alt;
use winnow::combinator::fail;
use winnow::stream::TokenSlice;
use crate::ast::AstError;
use crate::ast::AstFailure;
use crate::ast::TemplateAst;
use crate::ast::TemplateAstExpr;
use crate::ast::parse;
use crate::ast::parse_block;
use crate::ast::parse_end;
use crate::parser::TokenKind;
#[test]
fn check_only_content() {
@ -347,10 +456,7 @@ mod tests {
TemplateAst {
root: [
StaticContent(
TemplateToken {
kind: Content,
source: "Hello World",
},
"Hello World" (0..11),
),
],
}
@ -369,27 +475,14 @@ mod tests {
TemplateAst {
root: [
StaticContent(
TemplateToken {
kind: Content,
source: "Hello",
},
"Hello" (0..5),
),
Interpolation {
prev_whitespace_content: Some(
TemplateToken {
kind: Whitespace,
source: " ",
},
" " (5..6),
),
wants_output: TemplateToken {
kind: WantsOutput,
source: "=",
},
expression: VariableAccess(
TemplateToken {
kind: Ident,
source: "world",
},
"world" (10..15),
),
post_whitespace_content: None,
},
@ -412,35 +505,23 @@ mod tests {
ConditionalChain {
chain: [
IfConditional {
expression: Block {
if_block: Block {
prev_whitespace_content: None,
expression: VariableAccess(
TemplateToken {
kind: Ident,
source: "foo",
},
"foo" (6..9),
),
post_whitespace_content: Some(
TemplateToken {
kind: Whitespace,
source: " ",
},
" " (12..13),
),
},
content: [
StaticContent(
TemplateToken {
kind: Content,
source: "Hiii",
},
"Hiii" (13..17),
),
],
end_block: Block {
prev_whitespace_content: Some(
TemplateToken {
kind: Whitespace,
source: " ",
},
" " (17..18),
),
expression: EndBlock,
post_whitespace_content: None,
@ -453,18 +534,98 @@ mod tests {
"#);
}
fn panic_pretty<'a>(
input: &'_ str,
tokens: Result<TemplateAst<'a>, AstFailure>,
) -> TemplateAst<'a> {
match tokens {
Ok(ast) => ast,
Err(failure) => {
panic!("{}", failure.to_report(input));
}
}
}
#[test]
fn check_invalid_action() {
let input = r#"{{ value }}
{{ value }}
{{ value }}
{{ value }}
{{ value }}"#;
let parsed = crate::parser::parse(input.into()).unwrap();
let ast = parse(parsed.tokens()).unwrap_err();
insta::assert_snapshot!(ast.to_report(input));
}
#[test]
fn check_nested_simple_if() {
let input = r#"{{ if foo }}
{{ if bar }}
Hiii
{{ end }}
{{ end }}"#;
{{ end }}
{{= value }}
"#;
let parsed = crate::parser::parse(input.into()).unwrap();
let ast = parse(parsed.tokens()).unwrap();
insta::assert_debug_snapshot!("simple_if_tokens", parsed);
insta::assert_debug_snapshot!(ast);
let ast = panic_pretty(input, parse(parsed.tokens()));
insta::assert_debug_snapshot!("simple_if_ast", ast);
}
#[test]
fn check_parsing_block() {
use winnow::RecoverableParser;
let input = "{{ foo }}";
let parsed = crate::parser::parse(input.into()).unwrap();
let result = alt((
parse_end,
parse_block(
(TokenKind::Ident.void(), fail::<_, (), _>)
.void()
.context(AstError::ctx().msg("No ident allowed"))
.take()
.map(TemplateAstExpr::Invalid),
),
))
.recoverable_parse(TokenSlice::new(parsed.tokens()));
insta::assert_debug_snapshot!(result, @r#"
(
[
"{{" (0..2),
" " (2..3),
"foo" (3..6),
" " (6..7),
"}}" (7..9),
],
None,
[
AstError {
message: Some(
"No ident allowed",
),
help: None,
span: Some(
SourceSpan {
range: 0..6,
},
),
is_fatal: false,
},
],
)
"#);
}
}