Properly recover from errors
Signed-off-by: Marcel Müller <neikos@neikos.email>
This commit is contained in:
parent
f4e8137e17
commit
b07bef7904
4 changed files with 157 additions and 120 deletions
7
src/eval/mod.rs
Normal file
7
src/eval/mod.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
pub struct EvalStack {
|
||||||
|
stack: Vec<Evaluation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Evaluation {
|
||||||
|
AppendContent { content: String },
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ use serde::Serialize;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
pub mod parser;
|
pub mod parser;
|
||||||
|
pub mod eval;
|
||||||
|
|
||||||
#[derive(Debug, Error, Display)]
|
#[derive(Debug, Error, Display)]
|
||||||
pub enum TempleError {
|
pub enum TempleError {
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,9 @@ use annotate_snippets::Snippet;
|
||||||
use winnow::LocatingSlice;
|
use winnow::LocatingSlice;
|
||||||
use winnow::Parser;
|
use winnow::Parser;
|
||||||
use winnow::RecoverableParser;
|
use winnow::RecoverableParser;
|
||||||
use winnow::ascii::alpha1;
|
|
||||||
use winnow::ascii::multispace0;
|
|
||||||
use winnow::ascii::multispace1;
|
use winnow::ascii::multispace1;
|
||||||
use winnow::combinator::alt;
|
use winnow::combinator::alt;
|
||||||
use winnow::combinator::cut_err;
|
|
||||||
use winnow::combinator::eof;
|
use winnow::combinator::eof;
|
||||||
use winnow::combinator::opt;
|
|
||||||
use winnow::combinator::peek;
|
use winnow::combinator::peek;
|
||||||
use winnow::combinator::repeat_till;
|
use winnow::combinator::repeat_till;
|
||||||
use winnow::combinator::terminated;
|
use winnow::combinator::terminated;
|
||||||
|
|
@ -27,8 +23,10 @@ use winnow::stream::Location;
|
||||||
use winnow::stream::Recoverable;
|
use winnow::stream::Recoverable;
|
||||||
use winnow::stream::Stream;
|
use winnow::stream::Stream;
|
||||||
use winnow::token::any;
|
use winnow::token::any;
|
||||||
|
use winnow::token::one_of;
|
||||||
use winnow::token::rest;
|
use winnow::token::rest;
|
||||||
use winnow::token::take_until;
|
use winnow::token::take_until;
|
||||||
|
use winnow::token::take_while;
|
||||||
|
|
||||||
type Input<'input> = Recoverable<LocatingSlice<&'input str>, ParseError>;
|
type Input<'input> = Recoverable<LocatingSlice<&'input str>, ParseError>;
|
||||||
type PResult<'input, T> = Result<T, ParseError>;
|
type PResult<'input, T> = Result<T, ParseError>;
|
||||||
|
|
@ -53,35 +51,37 @@ impl ParseFailure {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_report(&self) -> String {
|
pub fn to_report(&self) -> String {
|
||||||
let mut report = String::new();
|
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::<Vec<_>>();
|
||||||
|
|
||||||
for error in &self.errors {
|
let renderer =
|
||||||
let rep = &[Level::ERROR
|
Renderer::styled().decor_style(annotate_snippets::renderer::DecorStyle::Unicode);
|
||||||
.primary_title(
|
renderer.render(&reports)
|
||||||
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)),
|
|
||||||
),
|
|
||||||
)];
|
|
||||||
|
|
||||||
let renderer =
|
|
||||||
Renderer::styled().decor_style(annotate_snippets::renderer::DecorStyle::Unicode);
|
|
||||||
report.push_str(&renderer.render(rep));
|
|
||||||
}
|
|
||||||
|
|
||||||
report
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ParseError {
|
pub struct ParseError {
|
||||||
pub(crate) message: Option<String>,
|
pub(crate) message: Option<String>,
|
||||||
|
pub(crate) help: Option<String>,
|
||||||
pub(crate) span: Option<SourceSpan>,
|
pub(crate) span: Option<SourceSpan>,
|
||||||
|
|
||||||
is_fatal: bool,
|
is_fatal: bool,
|
||||||
|
|
@ -91,6 +91,7 @@ impl ParseError {
|
||||||
fn ctx() -> Self {
|
fn ctx() -> Self {
|
||||||
ParseError {
|
ParseError {
|
||||||
message: None,
|
message: None,
|
||||||
|
help: None,
|
||||||
span: None,
|
span: None,
|
||||||
|
|
||||||
is_fatal: false,
|
is_fatal: false,
|
||||||
|
|
@ -101,6 +102,11 @@ impl ParseError {
|
||||||
self.message = Some(message.to_string());
|
self.message = Some(message.to_string());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn help(mut self, help: &str) -> Self {
|
||||||
|
self.help = Some(help.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModalError for ParseError {
|
impl ModalError for ParseError {
|
||||||
|
|
@ -138,6 +144,7 @@ impl<'input> AddContext<Input<'input>, ParseError> for ParseError {
|
||||||
context: ParseError,
|
context: ParseError,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.message = context.message.or(self.message);
|
self.message = context.message.or(self.message);
|
||||||
|
self.help = context.help.or(self.help);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -157,11 +164,7 @@ impl<'input> ParserError<Input<'input>> for ParseError {
|
||||||
type Inner = ParseError;
|
type Inner = ParseError;
|
||||||
|
|
||||||
fn from_input(_input: &Input<'input>) -> Self {
|
fn from_input(_input: &Input<'input>) -> Self {
|
||||||
ParseError {
|
ParseError::ctx()
|
||||||
message: None,
|
|
||||||
span: None,
|
|
||||||
is_fatal: false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn into_inner(self) -> winnow::Result<Self::Inner, Self> {
|
fn into_inner(self) -> winnow::Result<Self::Inner, Self> {
|
||||||
|
|
@ -175,111 +178,131 @@ impl<'input> ParserError<Input<'input>> for ParseError {
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ParsedTemplate<'input> {
|
pub struct ParsedTemplate<'input> {
|
||||||
content: Vec<TemplateChunk<'input>>,
|
tokens: Vec<TemplateToken<'input>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
impl<'input> ParsedTemplate<'input> {
|
||||||
pub enum TemplateChunk<'input> {
|
pub fn tokens(&self) -> &[TemplateToken<'input>] {
|
||||||
|
&self.tokens
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum TemplateToken<'input> {
|
||||||
Content(&'input str),
|
Content(&'input str),
|
||||||
Expression(InterpolateExpression<'input>),
|
LeftDelim(&'input str),
|
||||||
}
|
RightDelim(&'input str),
|
||||||
|
WantsOutput(&'input str),
|
||||||
#[derive(Debug)]
|
Ident(&'input str),
|
||||||
pub struct InterpolateExpression<'input> {
|
Whitespace(&'input str),
|
||||||
pub left_delim: &'input str,
|
Invalid(&'input str),
|
||||||
pub wants_output: Option<&'input str>,
|
|
||||||
pub value: Box<TemplateExpression<'input>>,
|
|
||||||
pub right_delim: &'input str,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct TemplateExpression<'input> {
|
|
||||||
pub before_ws: &'input str,
|
|
||||||
pub expr: TemplateExpr<'input>,
|
|
||||||
pub after_ws: &'input str,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum TemplateExpr<'input> {
|
|
||||||
Variable(&'input str),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse(input: &str) -> Result<ParsedTemplate<'_>, ParseFailure> {
|
pub fn parse(input: &str) -> Result<ParsedTemplate<'_>, ParseFailure> {
|
||||||
let (_remaining, val, errors) = parse_chunks.recoverable_parse(LocatingSlice::new(input));
|
let (_remaining, val, errors) = parse_tokens.recoverable_parse(LocatingSlice::new(input));
|
||||||
|
|
||||||
dbg!(&val);
|
|
||||||
|
|
||||||
if errors.is_empty()
|
if errors.is_empty()
|
||||||
&& let Some(val) = val
|
&& let Some(val) = val
|
||||||
{
|
{
|
||||||
Ok(ParsedTemplate { content: val })
|
Ok(ParsedTemplate { tokens: val })
|
||||||
} else {
|
} else {
|
||||||
Err(ParseFailure::from_errors(errors, input))
|
Err(ParseFailure::from_errors(errors, input))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_chunks<'input>(input: &mut Input<'input>) -> PResult<'input, Vec<TemplateChunk<'input>>> {
|
fn parse_tokens<'input>(input: &mut Input<'input>) -> PResult<'input, Vec<TemplateToken<'input>>> {
|
||||||
repeat_till(0.., alt((parse_interpolate, parse_content)), eof)
|
repeat_till(0.., alt((parse_interpolate, parse_content)), eof)
|
||||||
.map(|(v, _)| v)
|
.map(|(v, _): (Vec<_>, _)| v.into_iter().flatten().collect())
|
||||||
.parse_next(input)
|
.parse_next(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_content<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateChunk<'input>> {
|
fn parse_content<'input>(input: &mut Input<'input>) -> PResult<'input, Vec<TemplateToken<'input>>> {
|
||||||
alt((take_until(1.., "{{"), rest))
|
alt((take_until(1.., "{{"), rest))
|
||||||
.map(TemplateChunk::Content)
|
.map(TemplateToken::Content)
|
||||||
|
.map(|v| vec![v])
|
||||||
.parse_next(input)
|
.parse_next(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_interpolate<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateChunk<'input>> {
|
fn parse_interpolate<'input>(
|
||||||
let left_delim = "{{".parse_next(input)?;
|
input: &mut Input<'input>,
|
||||||
let (wants_output, value, right_delim) =
|
) -> PResult<'input, Vec<TemplateToken<'input>>> {
|
||||||
cut_err((opt("="), parse_value.map(Box::new), "}}")).parse_next(input)?;
|
let left_delim = "{{".map(TemplateToken::LeftDelim).parse_next(input)?;
|
||||||
|
|
||||||
Ok(TemplateChunk::Expression(InterpolateExpression {
|
let get_tokens = repeat_till(1.., parse_interpolate_token, peek("}}"));
|
||||||
left_delim,
|
let recover = take_until(0.., "}}").void();
|
||||||
wants_output,
|
|
||||||
value,
|
let (inside_tokens, _): (Vec<_>, _) = get_tokens
|
||||||
right_delim,
|
.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_value<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateExpression<'input>> {
|
fn parse_interpolate_token<'input>(
|
||||||
let before_ws = multispace0(input)?;
|
input: &mut Input<'input>,
|
||||||
|
) -> PResult<'input, TemplateToken<'input>> {
|
||||||
let expr = trace("parse_value", alt((parse_variable,))).parse_next(input)?;
|
trace(
|
||||||
|
"parse_interpolate_token",
|
||||||
let after_ws = multispace0(input)?;
|
alt((parse_ident, parse_whitespace)),
|
||||||
|
)
|
||||||
Ok(TemplateExpression {
|
.parse_next(input)
|
||||||
before_ws,
|
|
||||||
expr,
|
|
||||||
after_ws,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_variable<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateExpr<'input>> {
|
fn parse_whitespace<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken<'input>> {
|
||||||
terminated(alpha1, ident_terminator_check)
|
trace(
|
||||||
.map(TemplateExpr::Variable)
|
"parse_whitespace",
|
||||||
.context(ParseError::ctx().msg("valid variables are alpha"))
|
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)
|
.resume_after(bad_ident)
|
||||||
.map(|v| v.unwrap_or(TemplateExpr::Variable("BAD_VARIABLE")))
|
.with_taken()
|
||||||
|
.map(|(val, taken)| val.unwrap_or(TemplateToken::Invalid(taken)))
|
||||||
.parse_next(input)
|
.parse_next(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ident_terminator<'input>(input: &mut Input<'input>) -> PResult<'input, ()> {
|
fn ident<'input>(input: &mut Input<'input>) -> PResult<'input, &'input str> {
|
||||||
alt((eof.void(), "{".void(), "}".void(), multispace1.void())).parse_next(input)
|
take_while(1.., char::is_alphanumeric).parse_next(input)
|
||||||
}
|
|
||||||
|
|
||||||
fn ident_terminator_check<'input>(input: &mut Input<'input>) -> PResult<'input, ()> {
|
|
||||||
cut_err(peek(ident_terminator)).parse_next(input)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bad_ident<'input>(input: &mut Input<'input>) -> PResult<'input, ()> {
|
fn bad_ident<'input>(input: &mut Input<'input>) -> PResult<'input, ()> {
|
||||||
repeat_till(1.., any, peek(ident_terminator))
|
repeat_till(1.., any, ident_terminator_check)
|
||||||
.map(|((), _)| ())
|
.map(|((), _)| ())
|
||||||
.parse_next(input)
|
.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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::parser::parse;
|
use crate::parser::parse;
|
||||||
|
|
@ -292,7 +315,7 @@ mod tests {
|
||||||
insta::assert_debug_snapshot!(output, @r#"
|
insta::assert_debug_snapshot!(output, @r#"
|
||||||
Ok(
|
Ok(
|
||||||
ParsedTemplate {
|
ParsedTemplate {
|
||||||
content: [
|
tokens: [
|
||||||
Content(
|
Content(
|
||||||
"Hello There",
|
"Hello There",
|
||||||
),
|
),
|
||||||
|
|
@ -310,23 +333,24 @@ mod tests {
|
||||||
insta::assert_debug_snapshot!(output, @r#"
|
insta::assert_debug_snapshot!(output, @r#"
|
||||||
Ok(
|
Ok(
|
||||||
ParsedTemplate {
|
ParsedTemplate {
|
||||||
content: [
|
tokens: [
|
||||||
Content(
|
Content(
|
||||||
"Hello ",
|
"Hello ",
|
||||||
),
|
),
|
||||||
Expression(
|
LeftDelim(
|
||||||
InterpolateExpression {
|
"{{",
|
||||||
left_delim: "{{",
|
),
|
||||||
wants_output: None,
|
Whitespace(
|
||||||
value: TemplateExpression {
|
" ",
|
||||||
before_ws: " ",
|
),
|
||||||
expr: Variable(
|
Ident(
|
||||||
"there",
|
"there",
|
||||||
),
|
),
|
||||||
after_ws: " ",
|
Whitespace(
|
||||||
},
|
" ",
|
||||||
right_delim: "}}",
|
),
|
||||||
},
|
RightDelim(
|
||||||
|
"}}",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
@ -336,24 +360,27 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_interpolate_bad() {
|
fn parse_interpolate_bad() {
|
||||||
let input = "Hello {{ the2re }}";
|
let input = "Hello {{ the2re }} {{ the@re }}";
|
||||||
let output = parse(input);
|
let output = parse(input);
|
||||||
|
|
||||||
insta::assert_debug_snapshot!(output, @r#"
|
insta::assert_debug_snapshot!(output, @r#"
|
||||||
Err(
|
Err(
|
||||||
ParseFailure {
|
ParseFailure {
|
||||||
source: "Hello {{ the2re }}",
|
source: "Hello {{ the2re }} {{ the@re }}",
|
||||||
errors: [
|
errors: [
|
||||||
ParseError {
|
ParseError {
|
||||||
message: Some(
|
message: Some(
|
||||||
"valid variables are alpha",
|
"Invalid variable identifier",
|
||||||
|
),
|
||||||
|
help: Some(
|
||||||
|
"valid variable identifiers are alphanumeric",
|
||||||
),
|
),
|
||||||
span: Some(
|
span: Some(
|
||||||
SourceSpan {
|
SourceSpan {
|
||||||
range: 9..15,
|
range: 25..28,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
is_fatal: true,
|
is_fatal: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@
|
||||||
source: src/parser/mod.rs
|
source: src/parser/mod.rs
|
||||||
expression: error.to_report()
|
expression: error.to_report()
|
||||||
---
|
---
|
||||||
[1m[91merror[0m[1m: valid variables are alpha[0m
|
[1m[91merror[0m[1m: Invalid variable identifier[0m
|
||||||
[1m[94m ╭▸ [0m
|
[1m[94m ╭▸ [0m
|
||||||
[1m[94m1[0m [1m[94m│[0m Hello {{ the2re }}
|
[1m[94m1[0m [1m[94m│[0m Hello {{ the2re }} {{ the@re }}
|
||||||
[1m[94m╰╴[0m [1m[91m━━━━━━[0m
|
[1m[94m│[0m [1m[91m━━━[0m
|
||||||
|
[1m[94m│[0m
|
||||||
|
[1m[94m╰ [0m[1mhelp[0m: valid variable identifiers are alphanumeric
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue