Add ast parsing
Signed-off-by: Marcel Müller <neikos@neikos.email>
This commit is contained in:
parent
c5a2179b9e
commit
f5050e369e
3 changed files with 467 additions and 100 deletions
267
src/ast/mod.rs
Normal file
267
src/ast/mod.rs
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
use winnow::Parser;
|
||||
use winnow::RecoverableParser;
|
||||
use winnow::combinator::alt;
|
||||
use winnow::combinator::cut_err;
|
||||
use winnow::combinator::delimited;
|
||||
use winnow::combinator::opt;
|
||||
use winnow::combinator::repeat;
|
||||
use winnow::combinator::repeat_till;
|
||||
use winnow::error::AddContext;
|
||||
use winnow::error::FromRecoverableError;
|
||||
use winnow::error::ModalError;
|
||||
use winnow::error::ParserError;
|
||||
use winnow::stream::Recoverable;
|
||||
use winnow::stream::Stream;
|
||||
use winnow::stream::TokenSlice;
|
||||
use winnow::token::any;
|
||||
|
||||
use crate::parser::TemplateToken;
|
||||
use crate::parser::TokenKind;
|
||||
use crate::resume_after_cut;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TemplateAst<'input> {
|
||||
root: Vec<TemplateAstExpr<'input>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AstError {
|
||||
pub(crate) message: Option<String>,
|
||||
pub(crate) help: Option<String>,
|
||||
pub(crate) span: Option<crate::SourceSpan>,
|
||||
|
||||
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<'input> FromRecoverableError<Input<'input>, AstError> for AstError {
|
||||
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: AstError,
|
||||
) -> Self {
|
||||
e
|
||||
}
|
||||
}
|
||||
|
||||
impl<'input> AddContext<Input<'input>, AstError> for AstError {
|
||||
fn add_context(
|
||||
mut self,
|
||||
_input: &Input<'input>,
|
||||
_token_start: &<Input<'input> as Stream>::Checkpoint,
|
||||
context: AstError,
|
||||
) -> Self {
|
||||
self.message = context.message.or(self.message);
|
||||
self.help = context.help.or(self.help);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'input> ParserError<Input<'input>> for AstError {
|
||||
type Inner = AstError;
|
||||
|
||||
fn from_input(_input: &Input<'input>) -> Self {
|
||||
AstError::ctx()
|
||||
}
|
||||
|
||||
fn into_inner(self) -> winnow::Result<Self::Inner, Self> {
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn is_backtrack(&self) -> bool {
|
||||
!self.is_fatal
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AstFailure {}
|
||||
|
||||
impl AstFailure {
|
||||
fn from_errors(_errors: Vec<AstError>, _input: &[TemplateToken<'_>]) -> AstFailure {
|
||||
AstFailure {}
|
||||
}
|
||||
}
|
||||
|
||||
type Input<'input> = Recoverable<TokenSlice<'input, TemplateToken<'input>>, AstError>;
|
||||
|
||||
impl<'i> Parser<Input<'i>, TemplateToken<'i>, AstError> for TokenKind {
|
||||
fn parse_next(&mut self, input: &mut Input<'i>) -> winnow::Result<TemplateToken<'i>, AstError> {
|
||||
winnow::token::literal(*self)
|
||||
.parse_next(input)
|
||||
.map(|t| t[0].clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse<'input>(
|
||||
input: &'input [TemplateToken<'input>],
|
||||
) -> Result<TemplateAst<'input>, AstFailure> {
|
||||
let (_remaining, val, errors) = parse_ast.recoverable_parse(TokenSlice::new(input));
|
||||
|
||||
if errors.is_empty()
|
||||
&& let Some(val) = val
|
||||
{
|
||||
Ok(TemplateAst { root: val })
|
||||
} else {
|
||||
Err(AstFailure::from_errors(errors, input))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TemplateAstExpr<'input> {
|
||||
StaticContent(TemplateToken<'input>),
|
||||
Interpolation {
|
||||
prev_whitespace: Option<TemplateToken<'input>>,
|
||||
expression: Box<TemplateAstExpr<'input>>,
|
||||
post_whitespace: Option<TemplateToken<'input>>,
|
||||
},
|
||||
VariableAccess(TemplateToken<'input>),
|
||||
Invalid(&'input [TemplateToken<'input>]),
|
||||
}
|
||||
|
||||
fn parse_ast<'input>(input: &mut Input<'input>) -> Result<Vec<TemplateAstExpr<'input>>, AstError> {
|
||||
repeat(
|
||||
0..,
|
||||
alt((
|
||||
TokenKind::Content.map(TemplateAstExpr::StaticContent),
|
||||
parse_interpolation,
|
||||
)),
|
||||
)
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
fn parse_interpolation<'input>(
|
||||
input: &mut Input<'input>,
|
||||
) -> Result<TemplateAstExpr<'input>, AstError> {
|
||||
let expr_parser = resume_after_cut(
|
||||
alt((parse_variable_access,)),
|
||||
repeat_till(1.., any, TokenKind::RightDelim).map(|((), _)| ()),
|
||||
)
|
||||
.with_taken()
|
||||
.map(|(expr, taken)| expr.unwrap_or(TemplateAstExpr::Invalid(taken)));
|
||||
let (prev_whitespace, _left, (expression, _right, post_whitespace)) = (
|
||||
opt(TokenKind::Whitespace),
|
||||
TokenKind::LeftDelim,
|
||||
cut_err((
|
||||
delimited(ignore_ws, expr_parser, ignore_ws).map(Box::new),
|
||||
TokenKind::RightDelim,
|
||||
opt(TokenKind::Whitespace),
|
||||
)),
|
||||
)
|
||||
.parse_next(input)?;
|
||||
|
||||
Ok(TemplateAstExpr::Interpolation {
|
||||
prev_whitespace,
|
||||
expression,
|
||||
post_whitespace,
|
||||
})
|
||||
}
|
||||
|
||||
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> {
|
||||
repeat(.., TokenKind::Whitespace).parse_next(input)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::ast::parse;
|
||||
|
||||
#[test]
|
||||
fn check_only_content() {
|
||||
let input = "Hello World";
|
||||
|
||||
let parsed = crate::parser::parse(input).unwrap();
|
||||
|
||||
let ast = parse(parsed.tokens()).unwrap();
|
||||
|
||||
insta::assert_debug_snapshot!(ast, @r#"
|
||||
TemplateAst {
|
||||
root: [
|
||||
StaticContent(
|
||||
TemplateToken {
|
||||
kind: Content,
|
||||
source: "Hello World",
|
||||
},
|
||||
),
|
||||
],
|
||||
}
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_simple_variable_interpolation() {
|
||||
let input = "Hello {{ world }}";
|
||||
|
||||
let parsed = crate::parser::parse(input).unwrap();
|
||||
|
||||
let ast = parse(parsed.tokens()).unwrap();
|
||||
|
||||
insta::assert_debug_snapshot!(ast, @r#"
|
||||
TemplateAst {
|
||||
root: [
|
||||
StaticContent(
|
||||
TemplateToken {
|
||||
kind: Content,
|
||||
source: "Hello",
|
||||
},
|
||||
),
|
||||
Interpolation {
|
||||
prev_whitespace: Some(
|
||||
TemplateToken {
|
||||
kind: Whitespace,
|
||||
source: " ",
|
||||
},
|
||||
),
|
||||
expression: VariableAccess(
|
||||
TemplateToken {
|
||||
kind: Ident,
|
||||
source: "world",
|
||||
},
|
||||
),
|
||||
post_whitespace: None,
|
||||
},
|
||||
],
|
||||
}
|
||||
"#);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue