1091 lines
31 KiB
Rust
1091 lines
31 KiB
Rust
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<TemplateAstExpr<'input>>,
|
|
}
|
|
|
|
impl TemplateAst<'_> {
|
|
pub fn root(&self) -> &[TemplateAstExpr<'_>] {
|
|
&self.root
|
|
}
|
|
}
|
|
|
|
#[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 FromRecoverableError<Input<'_>, AstError> for AstError {
|
|
fn from_recoverable_error(
|
|
token_start: &<Input as winnow::stream::Stream>::Checkpoint,
|
|
_err_start: &<Input as winnow::stream::Stream>::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<Input<'_>, AstError> for AstError {
|
|
fn add_context(
|
|
mut self,
|
|
_input: &Input,
|
|
_token_start: &<Input as Stream>::Checkpoint,
|
|
context: AstError,
|
|
) -> Self {
|
|
self.message = context.message.or(self.message);
|
|
self.help = context.help.or(self.help);
|
|
self
|
|
}
|
|
}
|
|
|
|
impl ParserError<Input<'_>> for AstError {
|
|
type Inner = AstError;
|
|
|
|
fn from_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, Error)]
|
|
pub struct AstFailure {
|
|
errors: Vec<AstError>,
|
|
}
|
|
|
|
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<AstError>) -> 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::<Vec<_>>();
|
|
|
|
let renderer = annotate_snippets::Renderer::styled()
|
|
.decor_style(annotate_snippets::renderer::DecorStyle::Unicode);
|
|
renderer.render(&reports)
|
|
}
|
|
}
|
|
|
|
type Input<'input> = Recoverable<TokenSlice<'input, TemplateToken>, AstError>;
|
|
|
|
impl<'input> Parser<Input<'input>, TemplateToken, AstError> for TokenKind {
|
|
fn parse_next(&mut self, input: &mut Input<'input>) -> winnow::Result<TemplateToken, AstError> {
|
|
winnow::token::literal(*self)
|
|
.parse_next(input)
|
|
.map(|t| t[0].clone())
|
|
}
|
|
}
|
|
|
|
pub fn parse(input: &[TemplateToken]) -> Result<TemplateAst<'_>, 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<TemplateToken>,
|
|
expression: Box<TemplateAstExpr<'input>>,
|
|
post_whitespace_content: Option<TemplateToken>,
|
|
},
|
|
Interpolation {
|
|
prev_whitespace_content: Option<TemplateToken>,
|
|
expression: Box<TemplateAstExpr<'input>>,
|
|
post_whitespace_content: Option<TemplateToken>,
|
|
},
|
|
ConditionalChain {
|
|
chain: Vec<TemplateAstExpr<'input>>,
|
|
},
|
|
ForChain {
|
|
for_block: Box<TemplateAstExpr<'input>>,
|
|
content: Vec<TemplateAstExpr<'input>>,
|
|
else_block: Option<Box<TemplateAstExpr<'input>>>,
|
|
else_content: Option<Vec<TemplateAstExpr<'input>>>,
|
|
end_block: Box<TemplateAstExpr<'input>>,
|
|
},
|
|
For {
|
|
value_ident: TemplateToken,
|
|
value_expression: Box<TemplateAstExpr<'input>>,
|
|
},
|
|
ForElse,
|
|
ConditionalAccess(TemplateToken),
|
|
VariableAccess(TemplateToken),
|
|
IfConditional {
|
|
expression: Box<TemplateAstExpr<'input>>,
|
|
},
|
|
ConditionalContent {
|
|
content: Vec<TemplateAstExpr<'input>>,
|
|
},
|
|
ElseConditional {
|
|
expression: Option<Box<TemplateAstExpr<'input>>>,
|
|
},
|
|
Invalid(&'input [TemplateToken]),
|
|
MathOperation {
|
|
op: TokenOperator,
|
|
lhs: Box<TemplateAstExpr<'input>>,
|
|
rhs: Box<TemplateAstExpr<'input>>,
|
|
},
|
|
AccessOperation {
|
|
op: TokenOperator,
|
|
lhs: Box<TemplateAstExpr<'input>>,
|
|
rhs: Box<TemplateAstExpr<'input>>,
|
|
},
|
|
EndBlock,
|
|
Literal {
|
|
source: TemplateToken,
|
|
value: NomoValue,
|
|
},
|
|
FunctionCall {
|
|
name: TemplateToken,
|
|
args: Vec<TemplateAstExpr<'input>>,
|
|
},
|
|
}
|
|
|
|
fn parse_asts<'input>(input: &mut Input<'input>) -> Result<Vec<TemplateAstExpr<'input>>, AstError> {
|
|
repeat(0.., parse_ast).parse_next(input)
|
|
}
|
|
fn parse_ast<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, 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<TemplateAstExpr<'input>, 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<TemplateAstExpr<'input>, 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<TemplateAstExpr<'input>, 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<TemplateAstExpr<'input>, 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<TemplateAstExpr<'input>, 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<Vec<TemplateAstExpr<'input>>, 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<TemplateAstExpr<'input>, 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<TemplateAstExpr<'input>, 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<TemplateAstExpr<'input>, 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<TemplateAstExpr<'input>, 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<TemplateAstExpr<'input>, 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<TemplateAstExpr<'input>, AstError> {
|
|
trace(
|
|
"variable_access",
|
|
TokenKind::Ident.map(TemplateAstExpr::VariableAccess),
|
|
)
|
|
.parse_next(input)
|
|
}
|
|
|
|
fn parse_block<'input, ParseNext>(
|
|
parser: ParseNext,
|
|
) -> impl Parser<Input<'input>, TemplateAstExpr<'input>, AstError>
|
|
where
|
|
ParseNext: Parser<Input<'input>, 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<TemplateAstExpr<'input>, AstError> {
|
|
trace(
|
|
"operand",
|
|
alt((parse_function, parse_variable_access, parse_literal)),
|
|
)
|
|
.parse_next(input)
|
|
}
|
|
|
|
fn parse_literal<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, 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<TokenOperator, AstError> {
|
|
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<TemplateAstExpr<'input>, 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<Input, IgnoredParser, Ignored1, Output, Error, ParseNext>(
|
|
ignored: IgnoredParser,
|
|
parser: ParseNext,
|
|
) -> impl Parser<Input, Output, Error>
|
|
where
|
|
Input: Stream,
|
|
Error: ParserError<Input>,
|
|
IgnoredParser: Parser<Input, Ignored1, Error>,
|
|
IgnoredParser: Clone,
|
|
ParseNext: Parser<Input, Output, Error>,
|
|
{
|
|
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<TemplateAst<'a>, 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);
|
|
}
|
|
}
|