use thiserror::Error; use winnow::Parser; use winnow::RecoverableParser; use winnow::combinator::Infix::Left; use winnow::combinator::Postfix; use winnow::combinator::alt; use winnow::combinator::cut_err; use winnow::combinator::delimited; use winnow::combinator::dispatch; use winnow::combinator::expression; use winnow::combinator::fail; 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::separated; 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::lexer::TemplateToken; use crate::lexer::TokenKind; use crate::lexer::TokenOperator; use crate::resume_after_cut; use crate::value::NomoValue; #[derive(Debug, Clone)] pub struct TemplateAst<'input> { root: Vec>, } impl TemplateAst<'_> { pub fn root(&self) -> &[TemplateAstExpr<'_>] { &self.root } } #[derive(Debug, Clone)] pub struct AstError { pub(crate) message: Option, pub(crate) help: Option, pub(crate) span: Option, is_fatal: bool, } impl AstError { fn ctx() -> Self { AstError { message: None, help: None, span: None, is_fatal: false, } } fn msg(mut self, message: &str) -> Self { self.message = Some(message.to_string()); self } fn help(mut self, help: &str) -> Self { self.help = Some(help.to_string()); self } } impl ModalError for AstError { fn cut(mut self) -> Self { self.is_fatal = true; self } fn backtrack(mut self) -> Self { self.is_fatal = false; self } } impl FromRecoverableError, AstError> for AstError { fn from_recoverable_error( token_start: &::Checkpoint, _err_start: &::Checkpoint, 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 } } impl AddContext, AstError> for AstError { fn add_context( mut self, _input: &Input, _token_start: &::Checkpoint, context: AstError, ) -> Self { self.message = context.message.or(self.message); self.help = context.help.or(self.help); self } } impl ParserError> for AstError { type Inner = AstError; fn from_input(_input: &Input<'_>) -> Self { AstError::ctx() } fn into_inner(self) -> winnow::Result { Ok(self) } fn is_backtrack(&self) -> bool { !self.is_fatal } } #[derive(Debug, Error)] pub struct AstFailure { errors: Vec, } impl std::fmt::Display for AstFailure { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("TODO") } } impl AstFailure { fn from_errors(errors: Vec) -> 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::>(); let renderer = annotate_snippets::Renderer::styled() .decor_style(annotate_snippets::renderer::DecorStyle::Unicode); renderer.render(&reports) } } type Input<'input> = Recoverable, AstError>; impl<'input> Parser, TemplateToken, AstError> for TokenKind { fn parse_next(&mut self, input: &mut Input<'input>) -> winnow::Result { winnow::token::literal(*self) .parse_next(input) .map(|t| t[0].clone()) } } pub fn parse(input: &[TemplateToken]) -> Result, AstFailure> { let (_remaining, val, errors) = parse_asts.recoverable_parse(TokenSlice::new(input)); if errors.is_empty() && let Some(val) = val { Ok(TemplateAst { root: val }) } else { Err(AstFailure::from_errors(errors)) } } #[derive(Debug, Clone)] pub enum TemplateAstExpr<'input> { StaticContent(TemplateToken), Block { prev_whitespace_content: Option, expression: Box>, post_whitespace_content: Option, }, Interpolation { prev_whitespace_content: Option, expression: Box>, post_whitespace_content: Option, }, ConditionalChain { chain: Vec>, }, ForChain { for_block: Box>, content: Vec>, else_block: Option>>, else_content: Option>>, end_block: Box>, }, For { value_ident: TemplateToken, value_expression: Box>, }, ForElse, ConditionalAccess(TemplateToken), VariableAccess(TemplateToken), IfConditional { expression: Box>, }, ConditionalContent { content: Vec>, }, ElseConditional { expression: Option>>, }, Invalid(&'input [TemplateToken]), MathOperation { op: TokenOperator, lhs: Box>, rhs: Box>, }, AccessOperation { op: TokenOperator, lhs: Box>, rhs: Box>, }, EndBlock, Literal { source: TemplateToken, value: NomoValue, }, FunctionCall { name: TemplateToken, args: Vec>, }, } fn parse_asts<'input>(input: &mut Input<'input>) -> Result>, AstError> { repeat(0.., parse_ast).parse_next(input) } fn parse_ast<'input>(input: &mut Input<'input>) -> Result, AstError> { alt(( trace( "content", TokenKind::Content.map(TemplateAstExpr::StaticContent), ), trace("interpolation", parse_interpolation), parse_action, )) .parse_next(input) } fn parse_interpolation<'input>( input: &mut Input<'input>, ) -> Result, AstError> { let expr_parser = resume_after_cut( parse_expression, repeat_till( 0.., any, peek(preceded( opt(TokenKind::TrimWhitespace), TokenKind::RightDelim, )), ) .map(|((), _)| ()), ) .with_taken() .map(|(expr, taken)| expr.unwrap_or(TemplateAstExpr::Invalid(taken))); let ( prev_whitespace, _left, left_trim, _wants_output, (expression, right_trim, _right, post_whitespace), ) = ( opt(TokenKind::Whitespace), TokenKind::LeftDelim, opt(TokenKind::TrimWhitespace), TokenKind::WantsOutput, cut_err(( surrounded(ws, expr_parser).map(Box::new), opt(TokenKind::TrimWhitespace), TokenKind::RightDelim, opt(TokenKind::Whitespace), )), ) .parse_next(input)?; Ok(TemplateAstExpr::Interpolation { prev_whitespace_content: if left_trim.is_some() { None } else { prev_whitespace }, expression, post_whitespace_content: if right_trim.is_some() { None } else { post_whitespace }, }) } fn parse_action<'input>(input: &mut Input<'input>) -> Result, AstError> { trace( "action", alt(( parse_conditional_chain, parse_for_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_for_chain<'input>(input: &mut Input<'input>) -> Result, AstError> { trace("for_loop", |input: &mut Input<'input>| { let for_block = parse_for_loop.map(Box::new).parse_next(input)?; let loop_end = ( opt(( parse_loop_else.map(Box::new), repeat_till(0.., parse_ast, peek(parse_end)).map(|(val, _)| val), )), parse_end.map(Box::new), ); let (content, taken) = resume_after_cut( repeat_till(0.., parse_ast, loop_end), repeat_till(0.., any, parse_end).map(|((), _)| ()), ) .with_taken() .parse_next(input)?; let Some((content, (else_stuff, end_block))) = content else { return Ok(TemplateAstExpr::Invalid(taken)); }; let (else_block, else_content) = else_stuff.unzip(); Ok(TemplateAstExpr::ForChain { for_block, content, else_block, else_content, end_block, }) }) .parse_next(input) } fn parse_for_loop<'input>(input: &mut Input<'input>) -> Result, AstError> { trace( "for_loop_inner", parse_block( ( ws, TokenKind::For, ws, TokenKind::Ident, ws, TokenKind::In, ws, parse_expression.map(Box::new), ) .map(|(_, _for, _, value_ident, _, _in, _, value_expression)| { TemplateAstExpr::For { value_ident, value_expression, } }), ), ) .parse_next(input) } fn parse_conditional_chain<'input>( input: &mut Input<'input>, ) -> Result, AstError> { trace("conditional_chain", |input: &mut Input<'input>| { let if_block = parse_conditional_if.parse_next(input)?; let mut chain = vec![]; chain.push(if_block); let content = resume_after_cut( cut_err(inner_conditional_chain), repeat_till(0.., any, parse_end).map(|((), _)| ()), ) .parse_next(input)?; chain.extend(content.into_iter().flatten()); Ok(TemplateAstExpr::ConditionalChain { chain }) }) .parse_next(input) } fn inner_conditional_chain<'input>( input: &mut Input<'input>, ) -> Result>, AstError> { let mut needs_end = false; let mut chain = vec![]; loop { let (content, end_block): (Vec<_>, _) = repeat_till( 0.., parse_ast, trace( "conditional_chain_else/end", alt((parse_end, parse_conditional_else)), ), ) .parse_next(input)?; chain.push(TemplateAstExpr::ConditionalContent { content }); let is_end = if let TemplateAstExpr::Block { ref expression, .. } = end_block && let TemplateAstExpr::EndBlock = &**expression { true } else { false }; if !is_end && needs_end { return Err(AstError::from_input(input)); } if let TemplateAstExpr::Block { expression, .. } = &end_block && let TemplateAstExpr::ElseConditional { expression: None } = &**expression { needs_end = true; } chain.push(end_block); if is_end { break; } } Ok(chain) } fn parse_conditional_if<'input>( input: &mut Input<'input>, ) -> Result, AstError> { trace( "conditional", parse_block( preceded( TokenKind::ConditionalIf, cut_err( surrounded(ws, parse_expression) .map(Box::new) .context(AstError::ctx().msg("Expected an expression after 'if'")), ), ) .map(|expression| TemplateAstExpr::IfConditional { expression }), ), ) .parse_next(input) } fn parse_conditional_else<'input>( input: &mut Input<'input>, ) -> Result, AstError> { trace( "conditional_else", parse_block( preceded( surrounded(ws, TokenKind::ConditionalElse), opt(preceded( TokenKind::ConditionalIf, cut_err(surrounded(ws, parse_expression)).map(Box::new), )), ) .map(|else_expr| TemplateAstExpr::ElseConditional { expression: else_expr, }), ), ) .parse_next(input) } fn parse_loop_else<'input>(input: &mut Input<'input>) -> Result, AstError> { trace( "end", parse_block( TokenKind::ConditionalElse .value(TemplateAstExpr::ForElse) .context(AstError::ctx().msg("Expected an else block here")), ), ) .parse_next(input) } fn parse_end<'input>(input: &mut Input<'input>) -> Result, AstError> { trace( "end", parse_block( TokenKind::End .value(TemplateAstExpr::EndBlock) .context(AstError::ctx().msg("Expected an end block here")), ), ) .parse_next(input) } fn parse_function<'input>(input: &mut Input<'input>) -> Result, AstError> { trace( "variable_access", ( TokenKind::Ident, TokenKind::LeftArgList, separated(0.., parse_expression, TokenKind::ArgSeperator), TokenKind::RightArgList, ) .map(|(name, _left, args, _right)| TemplateAstExpr::FunctionCall { name, args }), ) .parse_next(input) } fn parse_variable_access<'input>( input: &mut Input<'input>, ) -> Result, AstError> { trace( "variable_access", TokenKind::Ident.map(TemplateAstExpr::VariableAccess), ) .parse_next(input) } fn parse_block<'input, ParseNext>( parser: ParseNext, ) -> impl Parser, TemplateAstExpr<'input>, AstError> where ParseNext: Parser, TemplateAstExpr<'input>, AstError>, { let expr_parser = resume_after_cut( parser, repeat_till(0.., any, peek(TokenKind::RightDelim)).map(|((), _)| ()), ) .with_taken() .map(|(expr, taken)| expr.unwrap_or(TemplateAstExpr::Invalid(taken))); ( opt(TokenKind::Whitespace), TokenKind::LeftDelim, opt(TokenKind::TrimWhitespace), not(TokenKind::WantsOutput), ( surrounded(ws, expr_parser.map(Box::new)), opt(TokenKind::TrimWhitespace), TokenKind::RightDelim, opt(TokenKind::Whitespace), ), ) .map( |( prev_whitespace, _left, left_trim, _not_token, (expression, right_trim, _right, post_whitespace), )| { TemplateAstExpr::Block { prev_whitespace_content: if left_trim.is_some() { None } else { prev_whitespace }, expression, post_whitespace_content: if right_trim.is_some() { None } else { post_whitespace }, } }, ) } fn parse_operand<'input>(input: &mut Input<'input>) -> Result, AstError> { trace( "operand", alt((parse_function, parse_variable_access, parse_literal)), ) .parse_next(input) } fn parse_literal<'input>(input: &mut Input<'input>) -> Result, AstError> { trace( "literal", any.verify_map(|token: &TemplateToken| match token.kind() { TokenKind::Literal(literal) => Some(TemplateAstExpr::Literal { source: token.clone(), value: match literal { crate::lexer::TokenLiteral::Bool(bool) => NomoValue::Bool { value: bool }, crate::lexer::TokenLiteral::Integer(int) => NomoValue::Integer { value: int }, }, }), _ => None, }), ) .parse_next(input) } fn parse_operator<'input>(input: &mut Input<'input>) -> Result { trace( "operator", any.verify_map(|t: &'input TemplateToken| match t.kind() { TokenKind::Operator(op) => Some(op), _ => None, }), ) .parse_next(input) } fn parse_expression<'input>( input: &mut Input<'input>, ) -> Result, AstError> { macro_rules! infix { ($parser:expr => [ $($side:tt $op:tt $val:tt => $prec:expr),* $(,)? ]) => { dispatch! { surrounded(ws, parse_operator); $( TokenOperator::$val => $side($prec, |_, lhs, rhs| Ok(TemplateAstExpr::$op { op: TokenOperator::$val, lhs: Box::new(lhs), rhs: Box::new(rhs) })), )* _ => fail } }; } trace( "expression", expression(surrounded(ws, parse_operand)).infix(infix! { surrounded(ws, parse_operator) => [ Left MathOperation Plus => 18, Left MathOperation Minus => 18, Left MathOperation Times => 20, Left MathOperation Divide => 20, Left MathOperation And => 10, Left MathOperation Or => 7, Left MathOperation Equal => 12, Left MathOperation NotEqual => 12, Left MathOperation Greater => 15, Left MathOperation GreaterOrEqual => 15, Left MathOperation Lesser => 15, Left MathOperation LesserOrEqual => 15, Left AccessOperation Dot => 22, ] }).postfix(dispatch! { surrounded(ws, parse_operator); TokenOperator::QuestionMark => Postfix(23, |input, rhs| { match rhs { TemplateAstExpr::VariableAccess(access) => Ok(TemplateAstExpr::ConditionalAccess(access)), _ => Err(AstError::from_input(input)), } }), _ => fail }), ) .parse_next(input) } fn ws<'input>(input: &mut Input<'input>) -> Result<(), AstError> { repeat(.., TokenKind::Whitespace).parse_next(input) } fn surrounded( ignored: IgnoredParser, parser: ParseNext, ) -> impl Parser where Input: Stream, Error: ParserError, IgnoredParser: Parser, IgnoredParser: Clone, ParseNext: Parser, { delimited(ignored.clone(), parser, ignored) } #[cfg(test)] mod tests { use winnow::Parser; use winnow::combinator::alt; use winnow::combinator::fail; use winnow::stream::TokenSlice; use crate::lexer::TokenKind; use crate::parser::AstError; use crate::parser::AstFailure; use crate::parser::TemplateAst; use crate::parser::TemplateAstExpr; use crate::parser::parse; use crate::parser::parse_block; use crate::parser::parse_end; fn panic_pretty<'a>( input: &'_ str, tokens: Result, AstFailure>, ) -> TemplateAst<'a> { match tokens { Ok(ast) => ast, Err(failure) => { panic!("{}", failure.to_report(input)); } } } #[test] fn check_only_content() { let input = "Hello World"; let parsed = crate::lexer::parse(input.into()).unwrap(); let ast = parse(parsed.tokens()).unwrap(); insta::assert_debug_snapshot!(ast, @r#" TemplateAst { root: [ StaticContent( [Content]"Hello World" (0..11), ), ], } "#); } #[test] fn check_simple_variable_interpolation() { let input = "Hello {{= world }}"; let parsed = crate::lexer::parse(input.into()).unwrap(); let ast = parse(parsed.tokens()).unwrap(); insta::assert_debug_snapshot!(ast, @r#" TemplateAst { root: [ StaticContent( [Content]"Hello" (0..5), ), Interpolation { prev_whitespace_content: Some( [Whitespace]" " (5..6), ), expression: VariableAccess( [Ident]"world" (10..15), ), post_whitespace_content: None, }, ], } "#); } #[test] fn check_simple_if() { let input = "{{ if foo }} Hiii {{ end }}"; let parsed = crate::lexer::parse(input.into()).unwrap(); let ast = panic_pretty(input, parse(parsed.tokens())); insta::assert_debug_snapshot!(ast, @r#" TemplateAst { root: [ ConditionalChain { chain: [ Block { prev_whitespace_content: None, expression: IfConditional { expression: VariableAccess( [Ident]"foo" (6..9), ), }, post_whitespace_content: Some( [Whitespace]" " (12..13), ), }, ConditionalContent { content: [ StaticContent( [Content]"Hiii" (13..17), ), ], }, Block { prev_whitespace_content: Some( [Whitespace]" " (17..18), ), expression: EndBlock, post_whitespace_content: None, }, ], }, ], } "#); } #[test] fn check_invalid_action() { let input = r#"{{ value }} {{ value }} {{ value }} {{ value }} {{ value }}"#; let parsed = crate::lexer::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 }} {{= value }} "#; let parsed = crate::lexer::parse(input.into()).unwrap(); insta::assert_debug_snapshot!("simple_if_tokens", parsed); 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::lexer::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#" ( [ [LeftDelim]"{{" (0..2), [Whitespace]" " (2..3), [Ident]"foo" (3..6), [Whitespace]" " (6..7), [RightDelim]"}}" (7..9), ], None, [ AstError { message: Some( "No ident allowed", ), help: None, span: Some( SourceSpan { range: 0..6, }, ), is_fatal: false, }, ], ) "#); } #[test] fn check_empty_if_output() { let input = "{{ if foo }}{{ end }}"; let parsed = crate::lexer::parse(input.into()).unwrap(); let ast = panic_pretty(input, parse(parsed.tokens())); insta::assert_debug_snapshot!(ast, @r#" TemplateAst { root: [ ConditionalChain { chain: [ Block { prev_whitespace_content: None, expression: IfConditional { expression: VariableAccess( [Ident]"foo" (6..9), ), }, post_whitespace_content: None, }, ConditionalContent { content: [], }, Block { prev_whitespace_content: None, expression: EndBlock, post_whitespace_content: None, }, ], }, ], } "#); } #[test] fn check_if_else() { let input = "{{ if foo }} foo {{ else }} bar {{ end }}"; let parsed = crate::lexer::parse(input.into()).unwrap(); let ast = panic_pretty(input, parse(parsed.tokens())); insta::assert_debug_snapshot!(ast); } #[test] fn check_if_else_if() { let input = "{{ if foo }} foo {{ else if bar }} bar {{ end }}"; let parsed = crate::lexer::parse(input.into()).unwrap(); let ast = panic_pretty(input, parse(parsed.tokens())); insta::assert_debug_snapshot!(ast); } #[test] fn check_trim_whitespace() { let input = "{{ if foo -}} foo {{- else if bar -}} bar {{- end }}"; let parsed = crate::lexer::parse(input.into()).unwrap(); let ast = panic_pretty(input, parse(parsed.tokens())); insta::assert_debug_snapshot!(ast); } #[test] fn check_for_loop() { let input = "{{ for value in array }} Hi: {{= value }} {{ else }} No Content :C {{ end }}"; let parsed = crate::lexer::parse(input.into()).unwrap(); let ast = panic_pretty(input, parse(parsed.tokens())); insta::assert_debug_snapshot!(ast); } #[test] fn check_math_expression() { let input = "{{= 5 * 3 + 2 / 3 }}"; let parsed = crate::lexer::parse(input.into()).unwrap(); let ast = panic_pretty(input, parse(parsed.tokens())); insta::assert_debug_snapshot!(ast); } #[test] fn check_logical_expression() { let input = "{{= true && false || 3 >= 2 && 5 == 2 }}"; let parsed = crate::lexer::parse(input.into()).unwrap(); let ast = panic_pretty(input, parse(parsed.tokens())); insta::assert_debug_snapshot!(ast); } #[test] fn check_function_call() { let input = "{{= foo(2 * 3, bar(2 + baz)) }}"; let parsed = crate::lexer::parse(input.into()).unwrap(); let ast = panic_pretty(input, parse(parsed.tokens())); insta::assert_debug_snapshot!(ast); } #[test] fn check_conditional_access() { let input = "{{= foo? }}"; let parsed = crate::lexer::parse(input.into()).unwrap(); let ast = panic_pretty(input, parse(parsed.tokens())); insta::assert_debug_snapshot!(ast); } #[test] fn check_access_operator() { let input = "{{= foo?.bar }}"; let parsed = crate::lexer::parse(input.into()).unwrap(); let ast = panic_pretty(input, parse(parsed.tokens())); insta::assert_debug_snapshot!(ast); } }