Add parsing for conditionals (cont.)

Signed-off-by: Marcel Müller <neikos@neikos.email>
This commit is contained in:
Marcel Müller 2026-03-08 15:06:29 +01:00
parent 974086a877
commit 8afc2d1bde
29 changed files with 994 additions and 746 deletions

6
flake.lock generated
View file

@ -63,11 +63,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1772593411, "lastModified": 1772939270,
"narHash": "sha256-47WOnCSyOL6AghZiMIJaTLWM359DHe3be9R1cNCdGUE=", "narHash": "sha256-HbxD5DJAKxzo0G8on5wdY+OZNiUWt3FTvGmXmVEmg7g=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "a741b36b77440f5db15fcf2ab6d7d592d2f9ee8f", "rev": "bb93f191a07c0165992ed6d0b4197ee5c7e6e641",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -1,2 +1,2 @@
[toolchain] [toolchain]
channel = "1.93.1" channel = "1.94.0"

View file

@ -6,18 +6,22 @@ use winnow::combinator::cut_err;
use winnow::combinator::delimited; use winnow::combinator::delimited;
use winnow::combinator::not; use winnow::combinator::not;
use winnow::combinator::opt; use winnow::combinator::opt;
use winnow::combinator::peek;
use winnow::combinator::preceded; use winnow::combinator::preceded;
use winnow::combinator::repeat; use winnow::combinator::repeat;
use winnow::combinator::repeat_till; use winnow::combinator::repeat_till;
use winnow::combinator::trace;
use winnow::error::AddContext; use winnow::error::AddContext;
use winnow::error::FromRecoverableError; use winnow::error::FromRecoverableError;
use winnow::error::ModalError; use winnow::error::ModalError;
use winnow::error::ParserError; use winnow::error::ParserError;
use winnow::stream::Offset;
use winnow::stream::Recoverable; use winnow::stream::Recoverable;
use winnow::stream::Stream; use winnow::stream::Stream;
use winnow::stream::TokenSlice; use winnow::stream::TokenSlice;
use winnow::token::any; use winnow::token::any;
use crate::SourceSpan;
use crate::parser::TemplateToken; use crate::parser::TemplateToken;
use crate::parser::TokenKind; use crate::parser::TokenKind;
use crate::resume_after_cut; use crate::resume_after_cut;
@ -83,6 +87,29 @@ impl FromRecoverableError<Input<'_>, AstError> for AstError {
input: &Input, input: &Input,
mut e: AstError, mut e: AstError,
) -> Self { ) -> 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 e
} }
} }
@ -117,7 +144,9 @@ impl ParserError<Input<'_>> for AstError {
} }
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub struct AstFailure {} pub struct AstFailure {
errors: Vec<AstError>,
}
impl std::fmt::Display for AstFailure { impl std::fmt::Display for AstFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@ -126,8 +155,40 @@ impl std::fmt::Display for AstFailure {
} }
impl AstFailure { impl AstFailure {
fn from_errors(_errors: Vec<AstError>, _input: &[TemplateToken]) -> AstFailure { fn from_errors(errors: Vec<AstError>) -> AstFailure {
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)
} }
} }
@ -149,7 +210,7 @@ pub fn parse(input: &[TemplateToken]) -> Result<TemplateAst<'_>, AstFailure> {
{ {
Ok(TemplateAst { root: val }) Ok(TemplateAst { root: val })
} else { } else {
Err(AstFailure::from_errors(errors, input)) Err(AstFailure::from_errors(errors))
} }
} }
@ -157,12 +218,6 @@ pub fn parse(input: &[TemplateToken]) -> Result<TemplateAst<'_>, AstFailure> {
pub enum TemplateAstExpr<'input> { pub enum TemplateAstExpr<'input> {
StaticContent(TemplateToken), StaticContent(TemplateToken),
Interpolation { Interpolation {
prev_whitespace_content: Option<TemplateToken>,
wants_output: TemplateToken,
expression: Box<TemplateAstExpr<'input>>,
post_whitespace_content: Option<TemplateToken>,
},
Action {
prev_whitespace_content: Option<TemplateToken>, prev_whitespace_content: Option<TemplateToken>,
expression: Box<TemplateAstExpr<'input>>, expression: Box<TemplateAstExpr<'input>>,
post_whitespace_content: Option<TemplateToken>, post_whitespace_content: Option<TemplateToken>,
@ -172,7 +227,7 @@ pub enum TemplateAstExpr<'input> {
chain: Vec<TemplateAstExpr<'input>>, chain: Vec<TemplateAstExpr<'input>>,
}, },
IfConditional { IfConditional {
expression: Box<TemplateAstExpr<'input>>, if_block: Box<TemplateAstExpr<'input>>,
content: Vec<TemplateAstExpr<'input>>, content: Vec<TemplateAstExpr<'input>>,
end_block: Box<TemplateAstExpr<'input>>, end_block: Box<TemplateAstExpr<'input>>,
}, },
@ -193,8 +248,11 @@ fn parse_asts<'input>(input: &mut Input<'input>) -> Result<Vec<TemplateAstExpr<'
} }
fn parse_ast<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, AstError> { fn parse_ast<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, AstError> {
alt(( alt((
trace(
"content",
TokenKind::Content.map(TemplateAstExpr::StaticContent), TokenKind::Content.map(TemplateAstExpr::StaticContent),
parse_interpolation, ),
trace("interpolation", parse_interpolation),
parse_action, parse_action,
)) ))
.parse_next(input) .parse_next(input)
@ -205,16 +263,16 @@ fn parse_interpolation<'input>(
) -> Result<TemplateAstExpr<'input>, AstError> { ) -> Result<TemplateAstExpr<'input>, AstError> {
let expr_parser = resume_after_cut( let expr_parser = resume_after_cut(
parse_value_expression, parse_value_expression,
repeat_till(1.., any, TokenKind::RightDelim).map(|((), _)| ()), repeat_till(0.., any, peek(TokenKind::RightDelim)).map(|((), _)| ()),
) )
.with_taken() .with_taken()
.map(|(expr, taken)| expr.unwrap_or(TemplateAstExpr::Invalid(taken))); .map(|(expr, taken)| expr.unwrap_or(TemplateAstExpr::Invalid(taken)));
let (prev_whitespace, _left, wants_output, (expression, _right, post_whitespace)) = ( let (prev_whitespace, _left, _wants_output, (expression, _right, post_whitespace)) = (
opt(TokenKind::Whitespace), opt(TokenKind::Whitespace),
TokenKind::LeftDelim, TokenKind::LeftDelim,
TokenKind::WantsOutput, TokenKind::WantsOutput,
cut_err(( cut_err((
delimited(ignore_ws, expr_parser, ignore_ws).map(Box::new), surrounded(ws, expr_parser).map(Box::new),
TokenKind::RightDelim, TokenKind::RightDelim,
opt(TokenKind::Whitespace), opt(TokenKind::Whitespace),
)), )),
@ -223,52 +281,99 @@ fn parse_interpolation<'input>(
Ok(TemplateAstExpr::Interpolation { Ok(TemplateAstExpr::Interpolation {
prev_whitespace_content: prev_whitespace, prev_whitespace_content: prev_whitespace,
wants_output,
expression, expression,
post_whitespace_content: post_whitespace, post_whitespace_content: post_whitespace,
}) })
} }
fn parse_value_expression<'input>(
input: &mut Input<'input>,
) -> Result<TemplateAstExpr<'input>, AstError> {
alt((parse_variable_access,)).parse_next(input)
}
fn parse_action<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, AstError> { fn parse_action<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, AstError> {
alt((parse_conditional_chain,)).parse_next(input) trace(
"action",
alt((
parse_conditional_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_conditional_chain<'input>( fn parse_conditional_chain<'input>(
input: &mut Input<'input>, input: &mut Input<'input>,
) -> Result<TemplateAstExpr<'input>, AstError> { ) -> Result<TemplateAstExpr<'input>, AstError> {
let if_expression = parse_conditional.parse_next(input)?; trace("conditional_chain", |input: &mut Input<'input>| {
let if_block = parse_conditional.map(Box::new).parse_next(input)?;
let mut chain = vec![]; let mut chain = vec![];
let (content, end_block): (Vec<_>, _) = let (content, end_block): (Vec<_>, _) =
repeat_till(1.., parse_ast, parse_end).parse_next(input)?; repeat_till(0.., parse_ast, parse_end.map(Box::new)).parse_next(input)?;
chain.push(TemplateAstExpr::IfConditional { chain.push(TemplateAstExpr::IfConditional {
expression: Box::new(if_expression), if_block,
content, content,
end_block: Box::new(end_block), end_block,
}); });
Ok(TemplateAstExpr::ConditionalChain { chain }) Ok(TemplateAstExpr::ConditionalChain { chain })
})
.parse_next(input)
} }
fn parse_conditional<'input>( fn parse_conditional<'input>(
input: &mut Input<'input>, input: &mut Input<'input>,
) -> Result<TemplateAstExpr<'input>, AstError> { ) -> Result<TemplateAstExpr<'input>, AstError> {
trace(
"conditional",
parse_block(preceded( parse_block(preceded(
TokenKind::ConditionalIf, TokenKind::ConditionalIf,
surrounded(ignore_ws, parse_value_expression), cut_err(
)) surrounded(ws, parse_value_expression)
.context(AstError::ctx().msg("Expected an expression after 'if'")),
),
)),
)
.parse_next(input) .parse_next(input)
} }
fn parse_end<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, AstError> { fn parse_end<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, AstError> {
parse_block(TokenKind::End.value(TemplateAstExpr::EndBlock)).parse_next(input) trace(
"end",
parse_block(
TokenKind::End
.value(TemplateAstExpr::EndBlock)
.context(AstError::ctx().msg("Expected an end block here")),
),
)
.parse_next(input)
}
fn parse_value_expression<'input>(
input: &mut Input<'input>,
) -> Result<TemplateAstExpr<'input>, AstError> {
trace("value_expression", alt((parse_variable_access,))).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>( fn parse_block<'input, ParseNext>(
@ -279,7 +384,7 @@ where
{ {
let expr_parser = resume_after_cut( let expr_parser = resume_after_cut(
parser, parser,
repeat_till(1.., any, TokenKind::RightDelim).map(|((), _)| ()), repeat_till(0.., any, peek(TokenKind::RightDelim)).map(|((), _)| ()),
) )
.with_taken() .with_taken()
.map(|(expr, taken)| expr.unwrap_or(TemplateAstExpr::Invalid(taken))); .map(|(expr, taken)| expr.unwrap_or(TemplateAstExpr::Invalid(taken)));
@ -288,11 +393,11 @@ where
opt(TokenKind::Whitespace), opt(TokenKind::Whitespace),
TokenKind::LeftDelim, TokenKind::LeftDelim,
not(TokenKind::WantsOutput), not(TokenKind::WantsOutput),
cut_err(( (
delimited(ignore_ws, expr_parser.map(Box::new), ignore_ws), surrounded(ws, expr_parser.map(Box::new)),
TokenKind::RightDelim, TokenKind::RightDelim,
opt(TokenKind::Whitespace), opt(TokenKind::Whitespace),
)), ),
) )
.map( .map(
|(prev_whitespace, _left, _not_token, (expression, _right, post_whitespace))| { |(prev_whitespace, _left, _not_token, (expression, _right, post_whitespace))| {
@ -305,15 +410,7 @@ where
) )
} }
fn parse_variable_access<'input>( fn ws<'input>(input: &mut Input<'input>) -> Result<(), AstError> {
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) repeat(.., TokenKind::Whitespace).parse_next(input)
} }
@ -333,7 +430,19 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use winnow::Parser;
use winnow::combinator::alt;
use winnow::combinator::fail;
use winnow::stream::TokenSlice;
use crate::ast::AstError;
use crate::ast::AstFailure;
use crate::ast::TemplateAst;
use crate::ast::TemplateAstExpr;
use crate::ast::parse; use crate::ast::parse;
use crate::ast::parse_block;
use crate::ast::parse_end;
use crate::parser::TokenKind;
#[test] #[test]
fn check_only_content() { fn check_only_content() {
@ -347,10 +456,7 @@ mod tests {
TemplateAst { TemplateAst {
root: [ root: [
StaticContent( StaticContent(
TemplateToken { "Hello World" (0..11),
kind: Content,
source: "Hello World",
},
), ),
], ],
} }
@ -369,27 +475,14 @@ mod tests {
TemplateAst { TemplateAst {
root: [ root: [
StaticContent( StaticContent(
TemplateToken { "Hello" (0..5),
kind: Content,
source: "Hello",
},
), ),
Interpolation { Interpolation {
prev_whitespace_content: Some( prev_whitespace_content: Some(
TemplateToken { " " (5..6),
kind: Whitespace,
source: " ",
},
), ),
wants_output: TemplateToken {
kind: WantsOutput,
source: "=",
},
expression: VariableAccess( expression: VariableAccess(
TemplateToken { "world" (10..15),
kind: Ident,
source: "world",
},
), ),
post_whitespace_content: None, post_whitespace_content: None,
}, },
@ -412,35 +505,23 @@ mod tests {
ConditionalChain { ConditionalChain {
chain: [ chain: [
IfConditional { IfConditional {
expression: Block { if_block: Block {
prev_whitespace_content: None, prev_whitespace_content: None,
expression: VariableAccess( expression: VariableAccess(
TemplateToken { "foo" (6..9),
kind: Ident,
source: "foo",
},
), ),
post_whitespace_content: Some( post_whitespace_content: Some(
TemplateToken { " " (12..13),
kind: Whitespace,
source: " ",
},
), ),
}, },
content: [ content: [
StaticContent( StaticContent(
TemplateToken { "Hiii" (13..17),
kind: Content,
source: "Hiii",
},
), ),
], ],
end_block: Block { end_block: Block {
prev_whitespace_content: Some( prev_whitespace_content: Some(
TemplateToken { " " (17..18),
kind: Whitespace,
source: " ",
},
), ),
expression: EndBlock, expression: EndBlock,
post_whitespace_content: None, post_whitespace_content: None,
@ -453,18 +534,98 @@ mod tests {
"#); "#);
} }
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_invalid_action() {
let input = r#"{{ value }}
{{ value }}
{{ value }}
{{ value }}
{{ value }}"#;
let parsed = crate::parser::parse(input.into()).unwrap();
let ast = parse(parsed.tokens()).unwrap_err();
insta::assert_snapshot!(ast.to_report(input));
}
#[test] #[test]
fn check_nested_simple_if() { fn check_nested_simple_if() {
let input = r#"{{ if foo }} let input = r#"{{ if foo }}
{{ if bar }} {{ if bar }}
Hiii Hiii
{{ end }} {{ end }}
{{ end }}"#; {{ end }}
{{= value }}
"#;
let parsed = crate::parser::parse(input.into()).unwrap(); let parsed = crate::parser::parse(input.into()).unwrap();
let ast = parse(parsed.tokens()).unwrap(); insta::assert_debug_snapshot!("simple_if_tokens", parsed);
insta::assert_debug_snapshot!(ast); 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::parser::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#"
(
[
"{{" (0..2),
" " (2..3),
"foo" (3..6),
" " (6..7),
"}}" (7..9),
],
None,
[
AstError {
message: Some(
"No ident allowed",
),
help: None,
span: Some(
SourceSpan {
range: 0..6,
},
),
is_fatal: false,
},
],
)
"#);
} }
} }

View file

@ -0,0 +1,30 @@
---
source: src/ast/mod.rs
expression: ast.to_report(input)
---
error: Standlone action block
 ╭▸ 
1 │ {{ value }}
│ ━━━━━
│
╰ help: If you want to output this expression, add a '=' to the block
error: Standlone action block
 ╭▸ 
2 │ {{ value }}
│ ━━━━━
╰ help: If you want to output this expression, add a '=' to the block
error: Standlone action block
 ╭▸ 
3 │ {{ value }}
│ ━━━━━
╰ help: If you want to output this expression, add a '=' to the block
error: Standlone action block
 ╭▸ 
4 │ {{ value }}
│ ━━━━━
╰ help: If you want to output this expression, add a '=' to the block
error: Standlone action block
 ╭▸ 
5 │ {{ value }}
│ ━━━━━
╰ help: If you want to output this expression, add a '=' to the block

View file

@ -1,84 +1,152 @@
--- ---
source: src/ast/mod.rs source: src/ast/mod.rs
expression: ast expression: parsed
--- ---
TemplateAst { ParsedTemplate {
root: [ tokens: [
ConditionalChain {
chain: [
IfConditional {
expression: Block {
prev_whitespace_content: None,
expression: VariableAccess(
TemplateToken { TemplateToken {
kind: Ident, kind: LeftDelim,
source: "foo", source: "{{" (0..2),
}, },
),
post_whitespace_content: Some(
TemplateToken { TemplateToken {
kind: Whitespace, kind: Whitespace,
source: " source: " " (2..3),
",
}, },
),
},
content: [
ConditionalChain {
chain: [
IfConditional {
expression: Block {
prev_whitespace_content: None,
expression: VariableAccess(
TemplateToken { TemplateToken {
kind: Ident, kind: ConditionalIf,
source: "bar", source: "if" (3..5),
}, },
),
post_whitespace_content: Some(
TemplateToken { TemplateToken {
kind: Whitespace, kind: Whitespace,
source: " source: " " (5..6),
",
}, },
), TemplateToken {
kind: Ident,
source: "foo" (6..9),
},
TemplateToken {
kind: Whitespace,
source: " " (9..10),
},
TemplateToken {
kind: RightDelim,
source: "}}" (10..12),
},
TemplateToken {
kind: Whitespace,
source: "\n " (12..25),
},
TemplateToken {
kind: LeftDelim,
source: "{{" (25..27),
},
TemplateToken {
kind: Whitespace,
source: " " (27..28),
},
TemplateToken {
kind: ConditionalIf,
source: "if" (28..30),
},
TemplateToken {
kind: Whitespace,
source: " " (30..31),
},
TemplateToken {
kind: Ident,
source: "bar" (31..34),
},
TemplateToken {
kind: Whitespace,
source: " " (34..35),
},
TemplateToken {
kind: RightDelim,
source: "}}" (35..37),
},
TemplateToken {
kind: Whitespace,
source: "\n " (37..54),
}, },
content: [
StaticContent(
TemplateToken { TemplateToken {
kind: Content, kind: Content,
source: "Hiii", source: "Hiii" (54..58),
}, },
),
],
end_block: Block {
prev_whitespace_content: Some(
TemplateToken { TemplateToken {
kind: Whitespace, kind: Whitespace,
source: " source: "\n " (58..71),
", },
TemplateToken {
kind: LeftDelim,
source: "{{" (71..73),
}, },
),
expression: EndBlock,
post_whitespace_content: Some(
TemplateToken { TemplateToken {
kind: Whitespace, kind: Whitespace,
source: " source: " " (73..74),
",
}, },
), TemplateToken {
kind: End,
source: "end" (74..77),
}, },
TemplateToken {
kind: Whitespace,
source: " " (77..78),
}, },
], TemplateToken {
kind: RightDelim,
source: "}}" (78..80),
}, },
], TemplateToken {
end_block: Block { kind: Whitespace,
prev_whitespace_content: None, source: "\n " (80..89),
expression: EndBlock,
post_whitespace_content: None,
}, },
TemplateToken {
kind: LeftDelim,
source: "{{" (89..91),
}, },
], TemplateToken {
kind: Whitespace,
source: " " (91..92),
},
TemplateToken {
kind: End,
source: "end" (92..95),
},
TemplateToken {
kind: Whitespace,
source: " " (95..96),
},
TemplateToken {
kind: RightDelim,
source: "}}" (96..98),
},
TemplateToken {
kind: Whitespace,
source: "\n\n " (98..108),
},
TemplateToken {
kind: LeftDelim,
source: "{{" (108..110),
},
TemplateToken {
kind: Whitespace,
source: " " (110..111),
},
TemplateToken {
kind: Ident,
source: "value" (111..116),
},
TemplateToken {
kind: Whitespace,
source: " " (116..117),
},
TemplateToken {
kind: RightDelim,
source: "}}" (117..119),
},
TemplateToken {
kind: Whitespace,
source: "\n " (119..128),
}, },
], ],
} }

View file

@ -0,0 +1,70 @@
---
source: src/ast/mod.rs
expression: ast
---
TemplateAst {
root: [
ConditionalChain {
chain: [
IfConditional {
if_block: Block {
prev_whitespace_content: None,
expression: VariableAccess(
"foo" (6..9),
),
post_whitespace_content: Some(
"\n " (12..25),
),
},
content: [
ConditionalChain {
chain: [
IfConditional {
if_block: Block {
prev_whitespace_content: None,
expression: VariableAccess(
"bar" (31..34),
),
post_whitespace_content: Some(
"\n " (37..54),
),
},
content: [
StaticContent(
"Hiii" (54..58),
),
],
end_block: Block {
prev_whitespace_content: Some(
"\n " (58..71),
),
expression: EndBlock,
post_whitespace_content: Some(
"\n " (80..89),
),
},
},
],
},
],
end_block: Block {
prev_whitespace_content: None,
expression: EndBlock,
post_whitespace_content: Some(
"\n\n " (98..108),
),
},
},
],
},
Interpolation {
prev_whitespace_content: None,
expression: VariableAccess(
"value" (112..117),
),
post_whitespace_content: Some(
"\n " (120..129),
),
},
],
}

View file

@ -0,0 +1,45 @@
---
source: src/ast/mod.rs
expression: parsed
---
ParsedTemplate {
tokens: [
"{{" (0..2),
" " (2..3),
"if" (3..5),
" " (5..6),
"foo" (6..9),
" " (9..10),
"}}" (10..12),
"\n " (12..25),
"{{" (25..27),
" " (27..28),
"if" (28..30),
" " (30..31),
"bar" (31..34),
" " (34..35),
"}}" (35..37),
"\n " (37..54),
"Hiii" (54..58),
"\n " (58..71),
"{{" (71..73),
" " (73..74),
"end" (74..77),
" " (77..78),
"}}" (78..80),
"\n " (80..89),
"{{" (89..91),
" " (91..92),
"end" (92..95),
" " (95..96),
"}}" (96..98),
"\n\n " (98..108),
"{{" (108..110),
"=" (110..111),
" " (111..112),
"value" (112..117),
" " (117..118),
"}}" (118..120),
"\n " (120..129),
],
}

View file

@ -24,11 +24,24 @@ pub struct VariableSlot {
#[derive(Debug)] #[derive(Debug)]
pub enum Instruction { pub enum Instruction {
AppendContent { content: NomoInput }, AppendContent {
LoadFromContextToSlot { name: NomoInput, slot: VariableSlot }, content: NomoInput,
EmitFromSlot { slot: VariableSlot }, },
PushScope { inherit_parent: bool }, LoadFromContextToSlot {
name: NomoInput,
slot: VariableSlot,
},
EmitFromSlot {
slot: VariableSlot,
},
PushScope {
inherit_parent: bool,
},
Abort, Abort,
JumpIfNotTrue {
emit_slot: VariableSlot,
jump: isize,
},
} }
pub fn emit_machine(input: crate::ast::TemplateAst<'_>) -> Vec<Instruction> { pub fn emit_machine(input: crate::ast::TemplateAst<'_>) -> Vec<Instruction> {
@ -55,53 +68,110 @@ fn emit_ast_expr(
}); });
} }
TemplateAstExpr::Interpolation { TemplateAstExpr::Interpolation {
prev_whitespace_content: prev_whitespace, prev_whitespace_content,
wants_output,
expression, expression,
post_whitespace_content: post_whitespace, post_whitespace_content,
} => { } => {
if let Some(ws) = prev_whitespace { if let Some(ws) = prev_whitespace_content {
eval.push(Instruction::AppendContent { eval.push(Instruction::AppendContent {
content: ws.source().clone(), content: ws.source().clone(),
}); });
} }
let emit_slot = machine.reserve_slot(); let emit_slot = machine.reserve_slot();
emit_expr(machine, eval, emit_slot, expression); emit_expr_load(machine, eval, emit_slot, expression);
eval.push(Instruction::EmitFromSlot { slot: emit_slot }); eval.push(Instruction::EmitFromSlot { slot: emit_slot });
if let Some(ws) = post_whitespace { if let Some(ws) = post_whitespace_content {
eval.push(Instruction::AppendContent { eval.push(Instruction::AppendContent {
content: ws.source().clone(), content: ws.source().clone(),
}); });
} }
} }
TemplateAstExpr::Invalid { .. } | TemplateAstExpr::VariableAccess { .. } => { TemplateAstExpr::ConditionalChain { chain } => {
eval.push(Instruction::Abort) let mut chain = chain.iter();
} let Some(TemplateAstExpr::IfConditional {
TemplateAstExpr::ConditionalChain { chain } => todo!(), if_block: expression,
TemplateAstExpr::ElseConditional { expression } => todo!(),
TemplateAstExpr::Action {
prev_whitespace_content,
expression,
post_whitespace_content,
} => todo!(),
TemplateAstExpr::EndBlock => todo!(),
TemplateAstExpr::Block {
prev_whitespace_content,
expression,
post_whitespace_content,
} => todo!(),
TemplateAstExpr::IfConditional {
expression,
content, content,
end_block, end_block,
} => todo!(), }) = chain.next()
else {
unreachable!("First element in conditional chain should be an IfConditional");
};
let TemplateAstExpr::Block {
prev_whitespace_content,
expression,
post_whitespace_content,
} = expression.as_ref()
else {
unreachable!("The end of an IfConditional must be a Block");
};
if let Some(ws) = prev_whitespace_content {
eval.push(Instruction::AppendContent {
content: ws.source().clone(),
});
}
let emit_slot = machine.reserve_slot();
emit_expr_load(machine, eval, emit_slot, expression);
let index = eval.len();
eval.push(Instruction::JumpIfNotTrue {
emit_slot,
jump: isize::MAX,
});
if let Some(ws) = post_whitespace_content {
eval.push(Instruction::AppendContent {
content: ws.source().clone(),
});
}
for ast in content {
emit_ast_expr(machine, eval, ast);
}
let TemplateAstExpr::Block {
prev_whitespace_content,
post_whitespace_content,
..
} = end_block.as_ref()
else {
unreachable!("The end of an IfConditional must be a Block");
};
if let Some(ws) = prev_whitespace_content {
eval.push(Instruction::AppendContent {
content: ws.source().clone(),
});
}
let jump = eval.len() - index - 1;
eval[index] = Instruction::JumpIfNotTrue {
emit_slot,
jump: jump as isize,
};
if let Some(ws) = post_whitespace_content {
eval.push(Instruction::AppendContent {
content: ws.source().clone(),
});
}
}
TemplateAstExpr::Block { .. }
| TemplateAstExpr::EndBlock
| TemplateAstExpr::IfConditional { .. }
| TemplateAstExpr::ElseConditional { .. }
| TemplateAstExpr::Invalid { .. }
| TemplateAstExpr::VariableAccess { .. } => eval.push(Instruction::Abort),
} }
} }
fn emit_expr( fn emit_expr_load(
machine: &mut EmitMachine, _machine: &mut EmitMachine,
eval: &mut Vec<Instruction>, eval: &mut Vec<Instruction>,
emit_slot: VariableSlot, emit_slot: VariableSlot,
expression: &TemplateAstExpr<'_>, expression: &TemplateAstExpr<'_>,
@ -117,24 +187,11 @@ fn emit_expr(
TemplateAstExpr::StaticContent { .. } | TemplateAstExpr::Interpolation { .. } => { TemplateAstExpr::StaticContent { .. } | TemplateAstExpr::Interpolation { .. } => {
unreachable!("Invalid AST here") unreachable!("Invalid AST here")
} }
TemplateAstExpr::ConditionalChain { chain } => todo!(), TemplateAstExpr::ConditionalChain { .. } => todo!(),
TemplateAstExpr::ElseConditional { expression } => todo!(), TemplateAstExpr::ElseConditional { .. } => todo!(),
TemplateAstExpr::Action {
prev_whitespace_content,
expression,
post_whitespace_content,
} => todo!(),
TemplateAstExpr::EndBlock => todo!(), TemplateAstExpr::EndBlock => todo!(),
TemplateAstExpr::Block { TemplateAstExpr::Block { .. } => todo!(),
prev_whitespace_content, TemplateAstExpr::IfConditional { .. } => todo!(),
expression,
post_whitespace_content,
} => todo!(),
TemplateAstExpr::IfConditional {
expression,
content,
end_block,
} => todo!(),
} }
} }
@ -155,13 +212,13 @@ mod tests {
insta::assert_debug_snapshot!(emit, @r#" insta::assert_debug_snapshot!(emit, @r#"
[ [
AppendContent { AppendContent {
content: "Hello", content: "Hello" (0..5),
}, },
AppendContent { AppendContent {
content: " ", content: " " (5..6),
}, },
LoadFromContextToSlot { LoadFromContextToSlot {
name: "world", name: "world" (10..15),
slot: VariableSlot { slot: VariableSlot {
index: 0, index: 0,
}, },

View file

@ -13,6 +13,11 @@ pub enum EvaluationError {
UnknownVariable(NomoInput), UnknownVariable(NomoInput),
/// An explicit abort was requested /// An explicit abort was requested
ExplicitAbort, ExplicitAbort,
/** The instruction pointer overflowed
**
** This is an internal error and is a bug that should be reported
*/
InstructionPointerOverflow,
} }
pub fn execute( pub fn execute(
@ -23,7 +28,14 @@ pub fn execute(
let mut scopes: HashMap<crate::emit::VariableSlot, serde_json::Value> = HashMap::new(); let mut scopes: HashMap<crate::emit::VariableSlot, serde_json::Value> = HashMap::new();
for instr in instructions { let mut ip = 0;
loop {
if ip >= instructions.len() {
break;
}
let instr = instructions.get(ip).unwrap();
match instr { match instr {
Instruction::AppendContent { content } => output.push_str(content), Instruction::AppendContent { content } => output.push_str(content),
Instruction::LoadFromContextToSlot { name, slot } => { Instruction::LoadFromContextToSlot { name, slot } => {
@ -40,8 +52,25 @@ pub fn execute(
} }
Instruction::PushScope { inherit_parent: _ } => todo!(), Instruction::PushScope { inherit_parent: _ } => todo!(),
Instruction::Abort => return Err(EvaluationError::ExplicitAbort), Instruction::Abort => return Err(EvaluationError::ExplicitAbort),
Instruction::JumpIfNotTrue { emit_slot, jump } => {
let dont_jump = scopes.get(emit_slot).unwrap().as_bool().unwrap();
if dont_jump {
// We are done
} else {
let (new_ip, overflow) = ip.overflowing_add_signed(*jump);
if overflow {
return Err(EvaluationError::InstructionPointerOverflow);
} else {
ip = new_ip;
continue;
} }
} }
}
}
ip += 1;
}
Ok(output) Ok(output)
} }

View file

@ -22,11 +22,20 @@ impl NomoInput {
pub fn into_parts(self) -> (Arc<str>, Range<usize>) { pub fn into_parts(self) -> (Arc<str>, Range<usize>) {
(self.backing, self.range) (self.backing, self.range)
} }
pub fn get_range(&self) -> Range<usize> {
self.range.clone()
}
}
#[derive(Debug, Clone)]
pub struct NomoInputCheckpoint {
range: Range<usize>,
} }
impl std::fmt::Debug for NomoInput { impl std::fmt::Debug for NomoInput {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "\"{}\"", self.as_str()) write!(f, "{:?} ({:?})", self.as_str(), self.range)
} }
} }
@ -74,6 +83,18 @@ impl NomoInput {
} }
} }
impl Offset for NomoInputCheckpoint {
fn offset_from(&self, start: &Self) -> usize {
self.range.start - start.range.start
}
}
impl Offset<NomoInputCheckpoint> for NomoInput {
fn offset_from(&self, start: &NomoInputCheckpoint) -> usize {
self.range.start - start.range.start
}
}
impl Offset for NomoInput { impl Offset for NomoInput {
fn offset_from(&self, start: &Self) -> usize { fn offset_from(&self, start: &Self) -> usize {
self.as_str().offset_from(&start.as_str()) self.as_str().offset_from(&start.as_str())
@ -116,7 +137,7 @@ impl Stream for NomoInput {
type IterOffsets = NomoInputIter; type IterOffsets = NomoInputIter;
type Checkpoint = NomoInput; type Checkpoint = NomoInputCheckpoint;
fn iter_offsets(&self) -> Self::IterOffsets { fn iter_offsets(&self) -> Self::IterOffsets {
NomoInputIter { NomoInputIter {
@ -167,7 +188,9 @@ impl Stream for NomoInput {
} }
fn checkpoint(&self) -> Self::Checkpoint { fn checkpoint(&self) -> Self::Checkpoint {
self.clone() NomoInputCheckpoint {
range: self.get_range(),
}
} }
fn reset(&mut self, checkpoint: &Self::Checkpoint) { fn reset(&mut self, checkpoint: &Self::Checkpoint) {

View file

@ -65,13 +65,8 @@ impl Nomo {
let instructions = emit::emit_machine(ast); let instructions = emit::emit_machine(ast);
self.templates.insert( self.templates
name.into(), .insert(name.into(), Template { instructions });
Template {
source,
instructions,
},
);
Ok(()) Ok(())
} }
@ -89,7 +84,6 @@ impl Nomo {
} }
struct Template { struct Template {
source: NomoInput,
instructions: Vec<Instruction>, instructions: Vec<Instruction>,
} }

View file

@ -245,12 +245,28 @@ impl<const LEN: usize> winnow::stream::ContainsToken<&'_ TemplateToken> for [Tok
} }
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Clone, PartialEq, Eq)]
pub struct TemplateToken { pub struct TemplateToken {
kind: TokenKind, kind: TokenKind,
source: NomoInput, source: NomoInput,
} }
impl std::fmt::Debug for TemplateToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self.source())
}
}
impl Location for TemplateToken {
fn previous_token_end(&self) -> usize {
NomoInput::get_range(&self.source).start
}
fn current_token_start(&self) -> usize {
NomoInput::get_range(&self.source).start
}
}
macro_rules! impl_token_kind_builders { macro_rules! impl_token_kind_builders {
($($name:ident => $kind:expr),+ $(,)?) => { ($($name:ident => $kind:expr),+ $(,)?) => {
$( $(
@ -459,10 +475,7 @@ mod tests {
Ok( Ok(
ParsedTemplate { ParsedTemplate {
tokens: [ tokens: [
TemplateToken { "Hello There" (0..11),
kind: Content,
source: "Hello There",
},
], ],
}, },
) )
@ -478,34 +491,13 @@ mod tests {
Ok( Ok(
ParsedTemplate { ParsedTemplate {
tokens: [ tokens: [
TemplateToken { "Hello" (0..5),
kind: Content, " " (5..6),
source: "Hello", "{{" (6..8),
}, " " (8..9),
TemplateToken { "there" (9..14),
kind: Whitespace, " " (14..15),
source: " ", "}}" (15..17),
},
TemplateToken {
kind: LeftDelim,
source: "{{",
},
TemplateToken {
kind: Whitespace,
source: " ",
},
TemplateToken {
kind: Ident,
source: "there",
},
TemplateToken {
kind: Whitespace,
source: " ",
},
TemplateToken {
kind: RightDelim,
source: "}}",
},
], ],
}, },
) )
@ -555,70 +547,21 @@ mod tests {
Ok( Ok(
ParsedTemplate { ParsedTemplate {
tokens: [ tokens: [
TemplateToken { "{{" (0..2),
kind: LeftDelim, " " (2..3),
source: "{{", "if" (3..5),
}, " " (5..6),
TemplateToken { "true" (6..10),
kind: Whitespace, " " (10..11),
source: " ", "}}" (11..13),
}, " " (13..14),
TemplateToken { "Hello!" (14..20),
kind: ConditionalIf, " " (20..21),
source: "if", "{{" (21..23),
}, " " (23..24),
TemplateToken { "end" (24..27),
kind: Whitespace, " " (27..28),
source: " ", "}}" (28..30),
},
TemplateToken {
kind: Literal(
Bool(
true,
),
),
source: "true",
},
TemplateToken {
kind: Whitespace,
source: " ",
},
TemplateToken {
kind: RightDelim,
source: "}}",
},
TemplateToken {
kind: Whitespace,
source: " ",
},
TemplateToken {
kind: Content,
source: "Hello!",
},
TemplateToken {
kind: Whitespace,
source: " ",
},
TemplateToken {
kind: LeftDelim,
source: "{{",
},
TemplateToken {
kind: Whitespace,
source: " ",
},
TemplateToken {
kind: End,
source: "end",
},
TemplateToken {
kind: Whitespace,
source: " ",
},
TemplateToken {
kind: RightDelim,
source: "}}",
},
], ],
}, },
) )

View file

@ -0,0 +1,31 @@
---
source: tests/file_tests.rs
expression: parsed
input_file: tests/cases/condition.nomo
---
ParsedTemplate {
tokens: [
"{{" (0..2),
" " (2..3),
"if" (3..5),
" " (5..6),
"test" (6..10),
" " (10..11),
"}}" (11..13),
"\n " (13..18),
"Hello World!" (18..30),
"\n" (30..31),
"{{" (31..33),
" " (33..34),
"end" (34..37),
" " (37..38),
"}}" (38..40),
"\n\n" (40..42),
"{{" (42..44),
"=" (44..45),
" " (45..46),
"stuff" (46..51),
" " (51..52),
"}}" (52..54),
],
}

View file

@ -5,174 +5,46 @@ input_file: tests/cases/identifiers.nomo
--- ---
ParsedTemplate { ParsedTemplate {
tokens: [ tokens: [
TemplateToken { "{{" (0..2),
kind: LeftDelim, "=" (2..3),
source: "{{", " " (3..4),
}, "_name" (4..9),
TemplateToken { " " (9..10),
kind: WantsOutput, "}}" (10..12),
source: "=", "\n" (12..13),
}, "{{" (13..15),
TemplateToken { "=" (15..16),
kind: Whitespace, " " (16..17),
source: " ", "a_name" (17..23),
}, " " (23..24),
TemplateToken { "}}" (24..26),
kind: Ident, "\n" (26..27),
source: "_name", "{{" (27..29),
}, "=" (29..30),
TemplateToken { " " (30..31),
kind: Whitespace, "1name" (31..36),
source: " ", " " (36..37),
}, "}}" (37..39),
TemplateToken { "\n" (39..40),
kind: RightDelim, "{{" (40..42),
source: "}}", "=" (42..43),
}, " " (43..44),
TemplateToken { "_name1" (44..50),
kind: Whitespace, " " (50..51),
source: " "}}" (51..53),
", "\n" (53..54),
}, "{{" (54..56),
TemplateToken { "=" (56..57),
kind: LeftDelim, " " (57..58),
source: "{{", "_namE" (58..63),
}, " " (63..64),
TemplateToken { "}}" (64..66),
kind: WantsOutput, "\n" (66..67),
source: "=", "{{" (67..69),
}, "=" (69..70),
TemplateToken { " " (70..71),
kind: Whitespace, "name1" (71..76),
source: " ", " " (76..77),
}, "}}" (77..79),
TemplateToken {
kind: Ident,
source: "a_name",
},
TemplateToken {
kind: Whitespace,
source: " ",
},
TemplateToken {
kind: RightDelim,
source: "}}",
},
TemplateToken {
kind: Whitespace,
source: "
",
},
TemplateToken {
kind: LeftDelim,
source: "{{",
},
TemplateToken {
kind: WantsOutput,
source: "=",
},
TemplateToken {
kind: Whitespace,
source: " ",
},
TemplateToken {
kind: Ident,
source: "1name",
},
TemplateToken {
kind: Whitespace,
source: " ",
},
TemplateToken {
kind: RightDelim,
source: "}}",
},
TemplateToken {
kind: Whitespace,
source: "
",
},
TemplateToken {
kind: LeftDelim,
source: "{{",
},
TemplateToken {
kind: WantsOutput,
source: "=",
},
TemplateToken {
kind: Whitespace,
source: " ",
},
TemplateToken {
kind: Ident,
source: "_name1",
},
TemplateToken {
kind: Whitespace,
source: " ",
},
TemplateToken {
kind: RightDelim,
source: "}}",
},
TemplateToken {
kind: Whitespace,
source: "
",
},
TemplateToken {
kind: LeftDelim,
source: "{{",
},
TemplateToken {
kind: WantsOutput,
source: "=",
},
TemplateToken {
kind: Whitespace,
source: " ",
},
TemplateToken {
kind: Ident,
source: "_namE",
},
TemplateToken {
kind: Whitespace,
source: " ",
},
TemplateToken {
kind: RightDelim,
source: "}}",
},
TemplateToken {
kind: Whitespace,
source: "
",
},
TemplateToken {
kind: LeftDelim,
source: "{{",
},
TemplateToken {
kind: WantsOutput,
source: "=",
},
TemplateToken {
kind: Whitespace,
source: " ",
},
TemplateToken {
kind: Ident,
source: "name1",
},
TemplateToken {
kind: Whitespace,
source: " ",
},
TemplateToken {
kind: RightDelim,
source: "}}",
},
], ],
} }

View file

@ -5,37 +5,13 @@ input_file: tests/cases/interpolation.nomo
--- ---
ParsedTemplate { ParsedTemplate {
tokens: [ tokens: [
TemplateToken { "Hello! I'm" (0..10),
kind: Content, " " (10..11),
source: "Hello! I'm", "{{" (11..13),
}, "=" (13..14),
TemplateToken { " " (14..15),
kind: Whitespace, "name" (15..19),
source: " ", " " (19..20),
}, "}}" (20..22),
TemplateToken {
kind: LeftDelim,
source: "{{",
},
TemplateToken {
kind: WantsOutput,
source: "=",
},
TemplateToken {
kind: Whitespace,
source: " ",
},
TemplateToken {
kind: Ident,
source: "name",
},
TemplateToken {
kind: Whitespace,
source: " ",
},
TemplateToken {
kind: RightDelim,
source: "}}",
},
], ],
} }

View file

@ -5,65 +5,20 @@ input_file: tests/cases/multiple.nomo
--- ---
ParsedTemplate { ParsedTemplate {
tokens: [ tokens: [
TemplateToken { "Hi there! My name is" (0..20),
kind: Content, " " (20..21),
source: "Hi there! My name is", "{{" (21..23),
}, "=" (23..24),
TemplateToken { " " (24..25),
kind: Whitespace, "name" (25..29),
source: " ", " " (29..30),
}, "}}" (30..32),
TemplateToken { " " (32..33),
kind: LeftDelim, "{{" (33..35),
source: "{{", "=" (35..36),
}, " " (36..37),
TemplateToken { "lastname" (37..45),
kind: WantsOutput, " " (45..46),
source: "=", "}}" (46..48),
},
TemplateToken {
kind: Whitespace,
source: " ",
},
TemplateToken {
kind: Ident,
source: "name",
},
TemplateToken {
kind: Whitespace,
source: " ",
},
TemplateToken {
kind: RightDelim,
source: "}}",
},
TemplateToken {
kind: Whitespace,
source: " ",
},
TemplateToken {
kind: LeftDelim,
source: "{{",
},
TemplateToken {
kind: WantsOutput,
source: "=",
},
TemplateToken {
kind: Whitespace,
source: " ",
},
TemplateToken {
kind: Ident,
source: "lastname",
},
TemplateToken {
kind: Whitespace,
source: " ",
},
TemplateToken {
kind: RightDelim,
source: "}}",
},
], ],
} }

View file

@ -5,9 +5,6 @@ input_file: tests/cases/simple.nomo
--- ---
ParsedTemplate { ParsedTemplate {
tokens: [ tokens: [
TemplateToken { "Hello World!" (0..12),
kind: Content,
source: "Hello World!",
},
], ],
} }

View file

@ -0,0 +1,45 @@
---
source: tests/file_tests.rs
expression: ast
input_file: tests/cases/condition.nomo
---
TemplateAst {
root: [
ConditionalChain {
chain: [
IfConditional {
if_block: Block {
prev_whitespace_content: None,
expression: VariableAccess(
"test" (6..10),
),
post_whitespace_content: Some(
"\n " (13..18),
),
},
content: [
StaticContent(
"Hello World!" (18..30),
),
],
end_block: Block {
prev_whitespace_content: Some(
"\n" (30..31),
),
expression: EndBlock,
post_whitespace_content: Some(
"\n\n" (40..42),
),
},
},
],
},
Interpolation {
prev_whitespace_content: None,
expression: VariableAccess(
"stuff" (46..51),
),
post_whitespace_content: None,
},
],
}

View file

@ -7,115 +7,53 @@ TemplateAst {
root: [ root: [
Interpolation { Interpolation {
prev_whitespace_content: None, prev_whitespace_content: None,
wants_output: TemplateToken {
kind: WantsOutput,
source: "=",
},
expression: VariableAccess( expression: VariableAccess(
TemplateToken { "_name" (4..9),
kind: Ident,
source: "_name",
},
), ),
post_whitespace_content: Some( post_whitespace_content: Some(
TemplateToken { "\n" (12..13),
kind: Whitespace,
source: "
",
},
), ),
}, },
Interpolation { Interpolation {
prev_whitespace_content: None, prev_whitespace_content: None,
wants_output: TemplateToken {
kind: WantsOutput,
source: "=",
},
expression: VariableAccess( expression: VariableAccess(
TemplateToken { "a_name" (17..23),
kind: Ident,
source: "a_name",
},
), ),
post_whitespace_content: Some( post_whitespace_content: Some(
TemplateToken { "\n" (26..27),
kind: Whitespace,
source: "
",
},
), ),
}, },
Interpolation { Interpolation {
prev_whitespace_content: None, prev_whitespace_content: None,
wants_output: TemplateToken {
kind: WantsOutput,
source: "=",
},
expression: VariableAccess( expression: VariableAccess(
TemplateToken { "1name" (31..36),
kind: Ident,
source: "1name",
},
), ),
post_whitespace_content: Some( post_whitespace_content: Some(
TemplateToken { "\n" (39..40),
kind: Whitespace,
source: "
",
},
), ),
}, },
Interpolation { Interpolation {
prev_whitespace_content: None, prev_whitespace_content: None,
wants_output: TemplateToken {
kind: WantsOutput,
source: "=",
},
expression: VariableAccess( expression: VariableAccess(
TemplateToken { "_name1" (44..50),
kind: Ident,
source: "_name1",
},
), ),
post_whitespace_content: Some( post_whitespace_content: Some(
TemplateToken { "\n" (53..54),
kind: Whitespace,
source: "
",
},
), ),
}, },
Interpolation { Interpolation {
prev_whitespace_content: None, prev_whitespace_content: None,
wants_output: TemplateToken {
kind: WantsOutput,
source: "=",
},
expression: VariableAccess( expression: VariableAccess(
TemplateToken { "_namE" (58..63),
kind: Ident,
source: "_namE",
},
), ),
post_whitespace_content: Some( post_whitespace_content: Some(
TemplateToken { "\n" (66..67),
kind: Whitespace,
source: "
",
},
), ),
}, },
Interpolation { Interpolation {
prev_whitespace_content: None, prev_whitespace_content: None,
wants_output: TemplateToken {
kind: WantsOutput,
source: "=",
},
expression: VariableAccess( expression: VariableAccess(
TemplateToken { "name1" (71..76),
kind: Ident,
source: "name1",
},
), ),
post_whitespace_content: None, post_whitespace_content: None,
}, },

View file

@ -6,27 +6,14 @@ input_file: tests/cases/interpolation.nomo
TemplateAst { TemplateAst {
root: [ root: [
StaticContent( StaticContent(
TemplateToken { "Hello! I'm" (0..10),
kind: Content,
source: "Hello! I'm",
},
), ),
Interpolation { Interpolation {
prev_whitespace_content: Some( prev_whitespace_content: Some(
TemplateToken { " " (10..11),
kind: Whitespace,
source: " ",
},
), ),
wants_output: TemplateToken {
kind: WantsOutput,
source: "=",
},
expression: VariableAccess( expression: VariableAccess(
TemplateToken { "name" (15..19),
kind: Ident,
source: "name",
},
), ),
post_whitespace_content: None, post_whitespace_content: None,
}, },

View file

@ -6,46 +6,23 @@ input_file: tests/cases/multiple.nomo
TemplateAst { TemplateAst {
root: [ root: [
StaticContent( StaticContent(
TemplateToken { "Hi there! My name is" (0..20),
kind: Content,
source: "Hi there! My name is",
},
), ),
Interpolation { Interpolation {
prev_whitespace_content: Some( prev_whitespace_content: Some(
TemplateToken { " " (20..21),
kind: Whitespace,
source: " ",
},
), ),
wants_output: TemplateToken {
kind: WantsOutput,
source: "=",
},
expression: VariableAccess( expression: VariableAccess(
TemplateToken { "name" (25..29),
kind: Ident,
source: "name",
},
), ),
post_whitespace_content: Some( post_whitespace_content: Some(
TemplateToken { " " (32..33),
kind: Whitespace,
source: " ",
},
), ),
}, },
Interpolation { Interpolation {
prev_whitespace_content: None, prev_whitespace_content: None,
wants_output: TemplateToken {
kind: WantsOutput,
source: "=",
},
expression: VariableAccess( expression: VariableAccess(
TemplateToken { "lastname" (37..45),
kind: Ident,
source: "lastname",
},
), ),
post_whitespace_content: None, post_whitespace_content: None,
}, },

View file

@ -6,10 +6,7 @@ input_file: tests/cases/simple.nomo
TemplateAst { TemplateAst {
root: [ root: [
StaticContent( StaticContent(
TemplateToken { "Hello World!" (0..12),
kind: Content,
source: "Hello World!",
},
), ),
], ],
} }

View file

@ -0,0 +1,42 @@
---
source: tests/file_tests.rs
expression: emit
input_file: tests/cases/condition.nomo
---
[
LoadFromContextToSlot {
name: "test" (6..10),
slot: VariableSlot {
index: 0,
},
},
JumpIfNotTrue {
emit_slot: VariableSlot {
index: 0,
},
jump: 3,
},
AppendContent {
content: "\n " (13..18),
},
AppendContent {
content: "Hello World!" (18..30),
},
AppendContent {
content: "\n" (30..31),
},
AppendContent {
content: "\n\n" (40..42),
},
LoadFromContextToSlot {
name: "stuff" (46..51),
slot: VariableSlot {
index: 1,
},
},
EmitFromSlot {
slot: VariableSlot {
index: 1,
},
},
]

View file

@ -5,7 +5,7 @@ input_file: tests/cases/identifiers.nomo
--- ---
[ [
LoadFromContextToSlot { LoadFromContextToSlot {
name: "_name", name: "_name" (4..9),
slot: VariableSlot { slot: VariableSlot {
index: 0, index: 0,
}, },
@ -16,11 +16,10 @@ input_file: tests/cases/identifiers.nomo
}, },
}, },
AppendContent { AppendContent {
content: " content: "\n" (12..13),
",
}, },
LoadFromContextToSlot { LoadFromContextToSlot {
name: "a_name", name: "a_name" (17..23),
slot: VariableSlot { slot: VariableSlot {
index: 1, index: 1,
}, },
@ -31,11 +30,10 @@ input_file: tests/cases/identifiers.nomo
}, },
}, },
AppendContent { AppendContent {
content: " content: "\n" (26..27),
",
}, },
LoadFromContextToSlot { LoadFromContextToSlot {
name: "1name", name: "1name" (31..36),
slot: VariableSlot { slot: VariableSlot {
index: 2, index: 2,
}, },
@ -46,11 +44,10 @@ input_file: tests/cases/identifiers.nomo
}, },
}, },
AppendContent { AppendContent {
content: " content: "\n" (39..40),
",
}, },
LoadFromContextToSlot { LoadFromContextToSlot {
name: "_name1", name: "_name1" (44..50),
slot: VariableSlot { slot: VariableSlot {
index: 3, index: 3,
}, },
@ -61,11 +58,10 @@ input_file: tests/cases/identifiers.nomo
}, },
}, },
AppendContent { AppendContent {
content: " content: "\n" (53..54),
",
}, },
LoadFromContextToSlot { LoadFromContextToSlot {
name: "_namE", name: "_namE" (58..63),
slot: VariableSlot { slot: VariableSlot {
index: 4, index: 4,
}, },
@ -76,11 +72,10 @@ input_file: tests/cases/identifiers.nomo
}, },
}, },
AppendContent { AppendContent {
content: " content: "\n" (66..67),
",
}, },
LoadFromContextToSlot { LoadFromContextToSlot {
name: "name1", name: "name1" (71..76),
slot: VariableSlot { slot: VariableSlot {
index: 5, index: 5,
}, },

View file

@ -5,13 +5,13 @@ input_file: tests/cases/interpolation.nomo
--- ---
[ [
AppendContent { AppendContent {
content: "Hello! I'm", content: "Hello! I'm" (0..10),
}, },
AppendContent { AppendContent {
content: " ", content: " " (10..11),
}, },
LoadFromContextToSlot { LoadFromContextToSlot {
name: "name", name: "name" (15..19),
slot: VariableSlot { slot: VariableSlot {
index: 0, index: 0,
}, },

View file

@ -5,13 +5,13 @@ input_file: tests/cases/multiple.nomo
--- ---
[ [
AppendContent { AppendContent {
content: "Hi there! My name is", content: "Hi there! My name is" (0..20),
}, },
AppendContent { AppendContent {
content: " ", content: " " (20..21),
}, },
LoadFromContextToSlot { LoadFromContextToSlot {
name: "name", name: "name" (25..29),
slot: VariableSlot { slot: VariableSlot {
index: 0, index: 0,
}, },
@ -22,10 +22,10 @@ input_file: tests/cases/multiple.nomo
}, },
}, },
AppendContent { AppendContent {
content: " ", content: " " (32..33),
}, },
LoadFromContextToSlot { LoadFromContextToSlot {
name: "lastname", name: "lastname" (37..45),
slot: VariableSlot { slot: VariableSlot {
index: 1, index: 1,
}, },

View file

@ -5,6 +5,6 @@ input_file: tests/cases/simple.nomo
--- ---
[ [
AppendContent { AppendContent {
content: "Hello World!", content: "Hello World!" (0..12),
}, },
] ]

View file

@ -0,0 +1,6 @@
---
source: tests/file_tests.rs
expression: output
input_file: tests/cases/condition.nomo
---
"\n Hello World!\n\n\nmore"

View file

@ -0,0 +1,10 @@
{
"test": true,
"stuff": "more"
}
---
{{ if test }}
Hello World!
{{ end }}
{{= stuff }}