Add parsing for conditionals (cont.)
Signed-off-by: Marcel Müller <neikos@neikos.email>
This commit is contained in:
parent
974086a877
commit
8afc2d1bde
29 changed files with 994 additions and 746 deletions
345
src/ast/mod.rs
345
src/ast/mod.rs
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
)
|
||||
"#);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue