Add initial parsing
Signed-off-by: Marcel Müller <neikos@neikos.email>
This commit is contained in:
parent
31e102a4ee
commit
f4e8137e17
6 changed files with 1098 additions and 6 deletions
367
src/parser/mod.rs
Normal file
367
src/parser/mod.rs
Normal file
|
|
@ -0,0 +1,367 @@
|
|||
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::alpha1;
|
||||
use winnow::ascii::multispace0;
|
||||
use winnow::ascii::multispace1;
|
||||
use winnow::combinator::alt;
|
||||
use winnow::combinator::cut_err;
|
||||
use winnow::combinator::eof;
|
||||
use winnow::combinator::opt;
|
||||
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::rest;
|
||||
use winnow::token::take_until;
|
||||
|
||||
type Input<'input> = Recoverable<LocatingSlice<&'input str>, ParseError>;
|
||||
type PResult<'input, T> = Result<T, ParseError>;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SourceSpan {
|
||||
pub range: Range<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParseFailure {
|
||||
source: Arc<str>,
|
||||
errors: Vec<ParseError>,
|
||||
}
|
||||
|
||||
impl ParseFailure {
|
||||
fn from_errors(errors: Vec<ParseError>, input: &str) -> ParseFailure {
|
||||
ParseFailure {
|
||||
source: Arc::from(input.to_string()),
|
||||
errors,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_report(&self) -> String {
|
||||
let mut report = String::new();
|
||||
|
||||
for error in &self.errors {
|
||||
let rep = &[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)),
|
||||
),
|
||||
)];
|
||||
|
||||
let renderer =
|
||||
Renderer::styled().decor_style(annotate_snippets::renderer::DecorStyle::Unicode);
|
||||
report.push_str(&renderer.render(rep));
|
||||
}
|
||||
|
||||
report
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParseError {
|
||||
pub(crate) message: Option<String>,
|
||||
pub(crate) span: Option<SourceSpan>,
|
||||
|
||||
is_fatal: bool,
|
||||
}
|
||||
|
||||
impl ParseError {
|
||||
fn ctx() -> Self {
|
||||
ParseError {
|
||||
message: None,
|
||||
span: None,
|
||||
|
||||
is_fatal: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn msg(mut self, message: &str) -> Self {
|
||||
self.message = Some(message.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<Input<'input>, ParseError> for ParseError {
|
||||
fn from_recoverable_error(
|
||||
token_start: &<Input<'input> as winnow::stream::Stream>::Checkpoint,
|
||||
_err_start: &<Input<'input> 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<Input<'input>, ParseError> for ParseError {
|
||||
fn add_context(
|
||||
mut self,
|
||||
_input: &Input<'input>,
|
||||
_token_start: &<Input<'input> as Stream>::Checkpoint,
|
||||
context: ParseError,
|
||||
) -> Self {
|
||||
self.message = context.message.or(self.message);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn span_from_checkpoint<I: Stream + Location>(
|
||||
input: &I,
|
||||
token_start: &<I as Stream>::Checkpoint,
|
||||
) -> SourceSpan {
|
||||
let offset = input.offset_from(token_start);
|
||||
|
||||
SourceSpan {
|
||||
range: (input.current_token_start() - offset)..input.current_token_start(),
|
||||
}
|
||||
}
|
||||
|
||||
impl<'input> ParserError<Input<'input>> for ParseError {
|
||||
type Inner = ParseError;
|
||||
|
||||
fn from_input(_input: &Input<'input>) -> Self {
|
||||
ParseError {
|
||||
message: None,
|
||||
span: None,
|
||||
is_fatal: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_inner(self) -> winnow::Result<Self::Inner, Self> {
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn is_backtrack(&self) -> bool {
|
||||
!self.is_fatal
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParsedTemplate<'input> {
|
||||
content: Vec<TemplateChunk<'input>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TemplateChunk<'input> {
|
||||
Content(&'input str),
|
||||
Expression(InterpolateExpression<'input>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InterpolateExpression<'input> {
|
||||
pub left_delim: &'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> {
|
||||
let (_remaining, val, errors) = parse_chunks.recoverable_parse(LocatingSlice::new(input));
|
||||
|
||||
dbg!(&val);
|
||||
|
||||
if errors.is_empty()
|
||||
&& let Some(val) = val
|
||||
{
|
||||
Ok(ParsedTemplate { content: val })
|
||||
} else {
|
||||
Err(ParseFailure::from_errors(errors, input))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_chunks<'input>(input: &mut Input<'input>) -> PResult<'input, Vec<TemplateChunk<'input>>> {
|
||||
repeat_till(0.., alt((parse_interpolate, parse_content)), eof)
|
||||
.map(|(v, _)| v)
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
fn parse_content<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateChunk<'input>> {
|
||||
alt((take_until(1.., "{{"), rest))
|
||||
.map(TemplateChunk::Content)
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
fn parse_interpolate<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateChunk<'input>> {
|
||||
let left_delim = "{{".parse_next(input)?;
|
||||
let (wants_output, value, right_delim) =
|
||||
cut_err((opt("="), parse_value.map(Box::new), "}}")).parse_next(input)?;
|
||||
|
||||
Ok(TemplateChunk::Expression(InterpolateExpression {
|
||||
left_delim,
|
||||
wants_output,
|
||||
value,
|
||||
right_delim,
|
||||
}))
|
||||
}
|
||||
|
||||
fn parse_value<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateExpression<'input>> {
|
||||
let before_ws = multispace0(input)?;
|
||||
|
||||
let expr = trace("parse_value", alt((parse_variable,))).parse_next(input)?;
|
||||
|
||||
let after_ws = multispace0(input)?;
|
||||
|
||||
Ok(TemplateExpression {
|
||||
before_ws,
|
||||
expr,
|
||||
after_ws,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_variable<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateExpr<'input>> {
|
||||
terminated(alpha1, ident_terminator_check)
|
||||
.map(TemplateExpr::Variable)
|
||||
.context(ParseError::ctx().msg("valid variables are alpha"))
|
||||
.resume_after(bad_ident)
|
||||
.map(|v| v.unwrap_or(TemplateExpr::Variable("BAD_VARIABLE")))
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
fn ident_terminator<'input>(input: &mut Input<'input>) -> PResult<'input, ()> {
|
||||
alt((eof.void(), "{".void(), "}".void(), multispace1.void())).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, ()> {
|
||||
repeat_till(1.., any, peek(ident_terminator))
|
||||
.map(|((), _)| ())
|
||||
.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 {
|
||||
content: [
|
||||
Content(
|
||||
"Hello There",
|
||||
),
|
||||
],
|
||||
},
|
||||
)
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_interpolate() {
|
||||
let input = "Hello {{ there }}";
|
||||
let output = parse(input);
|
||||
|
||||
insta::assert_debug_snapshot!(output, @r#"
|
||||
Ok(
|
||||
ParsedTemplate {
|
||||
content: [
|
||||
Content(
|
||||
"Hello ",
|
||||
),
|
||||
Expression(
|
||||
InterpolateExpression {
|
||||
left_delim: "{{",
|
||||
wants_output: None,
|
||||
value: TemplateExpression {
|
||||
before_ws: " ",
|
||||
expr: Variable(
|
||||
"there",
|
||||
),
|
||||
after_ws: " ",
|
||||
},
|
||||
right_delim: "}}",
|
||||
},
|
||||
),
|
||||
],
|
||||
},
|
||||
)
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_interpolate_bad() {
|
||||
let input = "Hello {{ the2re }}";
|
||||
let output = parse(input);
|
||||
|
||||
insta::assert_debug_snapshot!(output, @r#"
|
||||
Err(
|
||||
ParseFailure {
|
||||
source: "Hello {{ the2re }}",
|
||||
errors: [
|
||||
ParseError {
|
||||
message: Some(
|
||||
"valid variables are alpha",
|
||||
),
|
||||
span: Some(
|
||||
SourceSpan {
|
||||
range: 9..15,
|
||||
},
|
||||
),
|
||||
is_fatal: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
"#);
|
||||
|
||||
let error = output.unwrap_err();
|
||||
|
||||
insta::assert_snapshot!(error.to_report());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue