use std::ops::Range; use std::sync::Arc; use annotate_snippets::AnnotationKind; use annotate_snippets::Level; use annotate_snippets::Renderer; use annotate_snippets::Snippet; use winnow::LocatingSlice; use winnow::Parser; use winnow::RecoverableParser; use winnow::ascii::multispace1; use winnow::combinator::alt; use winnow::combinator::eof; use winnow::combinator::peek; use winnow::combinator::repeat_till; use winnow::combinator::terminated; use winnow::combinator::trace; use winnow::error::AddContext; use winnow::error::FromRecoverableError; use winnow::error::ModalError; use winnow::error::ParserError; use winnow::stream::Location; use winnow::stream::Recoverable; use winnow::stream::Stream; use winnow::token::any; use winnow::token::one_of; use winnow::token::rest; use winnow::token::take_until; use winnow::token::take_while; type Input<'input> = Recoverable, ParseError>; type PResult<'input, T> = Result; #[derive(Debug, Clone)] pub struct SourceSpan { pub range: Range, } #[derive(Debug)] pub struct ParseFailure { source: Arc, errors: Vec, } impl ParseFailure { fn from_errors(errors: Vec, input: &str) -> ParseFailure { ParseFailure { source: Arc::from(input.to_string()), errors, } } pub fn to_report(&self) -> String { let reports = self .errors .iter() .map(|error| { Level::ERROR .primary_title( error .message .as_deref() .unwrap_or("An error occurred while parsing"), ) .element( Snippet::source(self.source.as_ref()).annotation( AnnotationKind::Primary .span(error.span.clone().map(|s| s.range).unwrap_or_else(|| 0..0)), ), ) .elements(error.help.as_ref().map(|help| Level::HELP.message(help))) }) .collect::>(); let renderer = Renderer::styled().decor_style(annotate_snippets::renderer::DecorStyle::Unicode); renderer.render(&reports) } } #[derive(Debug, Clone)] pub struct ParseError { pub(crate) message: Option, pub(crate) help: Option, pub(crate) span: Option, is_fatal: bool, } impl ParseError { fn ctx() -> Self { ParseError { 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 ParseError { fn cut(mut self) -> Self { self.is_fatal = true; self } fn backtrack(mut self) -> Self { self.is_fatal = false; self } } impl<'input> FromRecoverableError, ParseError> for ParseError { fn from_recoverable_error( token_start: & as winnow::stream::Stream>::Checkpoint, _err_start: & as winnow::stream::Stream>::Checkpoint, input: &Input<'input>, mut e: ParseError, ) -> Self { e.span = e .span .or_else(|| Some(span_from_checkpoint(input, token_start))); e } } impl<'input> AddContext, ParseError> for ParseError { fn add_context( mut self, _input: &Input<'input>, _token_start: & as Stream>::Checkpoint, context: ParseError, ) -> Self { self.message = context.message.or(self.message); self.help = context.help.or(self.help); self } } fn span_from_checkpoint( input: &I, token_start: &::Checkpoint, ) -> SourceSpan { let offset = input.offset_from(token_start); SourceSpan { range: (input.current_token_start() - offset)..input.current_token_start(), } } impl<'input> ParserError> for ParseError { type Inner = ParseError; fn from_input(_input: &Input<'input>) -> Self { ParseError::ctx() } fn into_inner(self) -> winnow::Result { Ok(self) } fn is_backtrack(&self) -> bool { !self.is_fatal } } #[derive(Debug)] pub struct ParsedTemplate<'input> { tokens: Vec>, } impl<'input> ParsedTemplate<'input> { pub fn tokens(&self) -> &[TemplateToken<'input>] { &self.tokens } } #[derive(Debug, Clone)] pub enum TemplateToken<'input> { Content(&'input str), LeftDelim(&'input str), RightDelim(&'input str), WantsOutput(&'input str), Ident(&'input str), Whitespace(&'input str), Invalid(&'input str), } pub fn parse(input: &str) -> Result, ParseFailure> { let (_remaining, val, errors) = parse_tokens.recoverable_parse(LocatingSlice::new(input)); if errors.is_empty() && let Some(val) = val { Ok(ParsedTemplate { tokens: val }) } else { Err(ParseFailure::from_errors(errors, input)) } } fn parse_tokens<'input>(input: &mut Input<'input>) -> PResult<'input, Vec>> { repeat_till(0.., alt((parse_interpolate, parse_content)), eof) .map(|(v, _): (Vec<_>, _)| v.into_iter().flatten().collect()) .parse_next(input) } fn parse_content<'input>(input: &mut Input<'input>) -> PResult<'input, Vec>> { alt((take_until(1.., "{{"), rest)) .map(TemplateToken::Content) .map(|v| vec![v]) .parse_next(input) } fn parse_interpolate<'input>( input: &mut Input<'input>, ) -> PResult<'input, Vec>> { let left_delim = "{{".map(TemplateToken::LeftDelim).parse_next(input)?; let get_tokens = repeat_till(1.., parse_interpolate_token, peek("}}")); let recover = take_until(0.., "}}").void(); let (inside_tokens, _): (Vec<_>, _) = get_tokens .resume_after(recover) .with_taken() .map(|(val, taken)| val.unwrap_or_else(|| (vec![TemplateToken::Invalid(taken)], ""))) .parse_next(input)?; let right_delim = "}}".map(TemplateToken::RightDelim).parse_next(input)?; let mut tokens = vec![left_delim]; tokens.extend(inside_tokens); tokens.push(right_delim); Ok(tokens) } fn parse_interpolate_token<'input>( input: &mut Input<'input>, ) -> PResult<'input, TemplateToken<'input>> { trace( "parse_interpolate_token", alt((parse_ident, parse_whitespace)), ) .parse_next(input) } fn parse_whitespace<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken<'input>> { trace( "parse_whitespace", multispace1.map(TemplateToken::Whitespace), ) .parse_next(input) } fn parse_ident<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken<'input>> { let ident = ident.map(TemplateToken::Ident).parse_next(input)?; ident_terminator_check .context( ParseError::ctx() .msg("Invalid variable identifier") .help("valid variable identifiers are alphanumeric"), ) .value(ident) .resume_after(bad_ident) .with_taken() .map(|(val, taken)| val.unwrap_or(TemplateToken::Invalid(taken))) .parse_next(input) } fn ident<'input>(input: &mut Input<'input>) -> PResult<'input, &'input str> { take_while(1.., char::is_alphanumeric).parse_next(input) } fn bad_ident<'input>(input: &mut Input<'input>) -> PResult<'input, ()> { repeat_till(1.., any, ident_terminator_check) .map(|((), _)| ()) .parse_next(input) } fn ident_terminator_check<'input>(input: &mut Input<'input>) -> PResult<'input, ()> { peek(ident_terminator).parse_next(input) } fn ident_terminator<'input>(input: &mut Input<'input>) -> PResult<'input, ()> { alt(( eof.void(), one_of(('{', '}')).void(), one_of((' ', '\t', '\r', '\n')).void(), )) .parse_next(input) } #[cfg(test)] mod tests { use crate::parser::parse; #[test] fn parse_simple() { let input = "Hello There"; let output = parse(input); insta::assert_debug_snapshot!(output, @r#" Ok( ParsedTemplate { tokens: [ Content( "Hello There", ), ], }, ) "#); } #[test] fn parse_interpolate() { let input = "Hello {{ there }}"; let output = parse(input); insta::assert_debug_snapshot!(output, @r#" Ok( ParsedTemplate { tokens: [ Content( "Hello ", ), LeftDelim( "{{", ), Whitespace( " ", ), Ident( "there", ), Whitespace( " ", ), RightDelim( "}}", ), ], }, ) "#); } #[test] fn parse_interpolate_bad() { let input = "Hello {{ the2re }} {{ the@re }}"; let output = parse(input); insta::assert_debug_snapshot!(output, @r#" Err( ParseFailure { source: "Hello {{ the2re }} {{ the@re }}", errors: [ ParseError { message: Some( "Invalid variable identifier", ), help: Some( "valid variable identifiers are alphanumeric", ), span: Some( SourceSpan { range: 25..28, }, ), is_fatal: false, }, ], }, ) "#); let error = output.unwrap_err(); insta::assert_snapshot!(error.to_report()); } }