Compare commits

..

2 commits

Author SHA1 Message Date
59f92e31fe Allow trimming of whitespace
Signed-off-by: Marcel Müller <neikos@neikos.email>
2026-03-09 13:24:40 +01:00
587cfdea53 Add parsing of whitespace trimming
Signed-off-by: Marcel Müller <neikos@neikos.email>
2026-03-09 13:14:58 +01:00
9 changed files with 276 additions and 10 deletions

View file

@ -264,16 +264,32 @@ 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(0.., any, peek(TokenKind::RightDelim)).map(|((), _)| ()), repeat_till(
0..,
any,
peek(preceded(
opt(TokenKind::TrimWhitespace),
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,
left_trim,
_wants_output,
(expression, right_trim, _right, post_whitespace),
) = (
opt(TokenKind::Whitespace), opt(TokenKind::Whitespace),
TokenKind::LeftDelim, TokenKind::LeftDelim,
opt(TokenKind::TrimWhitespace),
TokenKind::WantsOutput, TokenKind::WantsOutput,
cut_err(( cut_err((
surrounded(ws, expr_parser).map(Box::new), surrounded(ws, expr_parser).map(Box::new),
opt(TokenKind::TrimWhitespace),
TokenKind::RightDelim, TokenKind::RightDelim,
opt(TokenKind::Whitespace), opt(TokenKind::Whitespace),
)), )),
@ -281,9 +297,17 @@ fn parse_interpolation<'input>(
.parse_next(input)?; .parse_next(input)?;
Ok(TemplateAstExpr::Interpolation { Ok(TemplateAstExpr::Interpolation {
prev_whitespace_content: prev_whitespace, prev_whitespace_content: if left_trim.is_some() {
None
} else {
prev_whitespace
},
expression, expression,
post_whitespace_content: post_whitespace, post_whitespace_content: if right_trim.is_some() {
None
} else {
post_whitespace
},
}) })
} }
@ -439,19 +463,35 @@ where
( (
opt(TokenKind::Whitespace), opt(TokenKind::Whitespace),
TokenKind::LeftDelim, TokenKind::LeftDelim,
opt(TokenKind::TrimWhitespace),
not(TokenKind::WantsOutput), not(TokenKind::WantsOutput),
( (
surrounded(ws, expr_parser.map(Box::new)), surrounded(ws, expr_parser.map(Box::new)),
opt(TokenKind::TrimWhitespace),
TokenKind::RightDelim, TokenKind::RightDelim,
opt(TokenKind::Whitespace), opt(TokenKind::Whitespace),
), ),
) )
.map( .map(
|(prev_whitespace, _left, _not_token, (expression, _right, post_whitespace))| { |(
prev_whitespace,
_left,
left_trim,
_not_token,
(expression, right_trim, _right, post_whitespace),
)| {
TemplateAstExpr::Block { TemplateAstExpr::Block {
prev_whitespace_content: prev_whitespace, prev_whitespace_content: if left_trim.is_some() {
None
} else {
prev_whitespace
},
expression, expression,
post_whitespace_content: post_whitespace, post_whitespace_content: if right_trim.is_some() {
None
} else {
post_whitespace
},
} }
}, },
) )
@ -736,4 +776,15 @@ mod tests {
insta::assert_debug_snapshot!(ast); insta::assert_debug_snapshot!(ast);
} }
#[test]
fn check_trim_whitespace() {
let input = "{{ if foo -}} foo {{- else if bar -}} bar {{- end }}";
let parsed = crate::parser::parse(input.into()).unwrap();
let ast = panic_pretty(input, parse(parsed.tokens()));
insta::assert_debug_snapshot!(ast);
}
} }

View file

@ -0,0 +1,51 @@
---
source: src/ast/mod.rs
expression: ast
---
TemplateAst {
root: [
ConditionalChain {
chain: [
Block {
prev_whitespace_content: None,
expression: IfConditional {
expression: VariableAccess(
[Ident]"foo" (6..9),
),
},
post_whitespace_content: None,
},
ConditionalContent {
content: [
StaticContent(
[Content]"foo" (14..17),
),
],
},
Block {
prev_whitespace_content: None,
expression: ElseConditional {
expression: Some(
VariableAccess(
[Ident]"bar" (30..33),
),
),
},
post_whitespace_content: None,
},
ConditionalContent {
content: [
StaticContent(
[Content]"bar" (38..41),
),
],
},
Block {
prev_whitespace_content: None,
expression: EndBlock,
post_whitespace_content: None,
},
],
},
],
}

View file

@ -53,11 +53,12 @@ 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 } => { Instruction::JumpIfNotTrue { emit_slot, jump } => {
let jump = if *jump == 0 { 1 } else { *jump };
let dont_jump = scopes.get(emit_slot).unwrap().as_bool().unwrap(); let dont_jump = scopes.get(emit_slot).unwrap().as_bool().unwrap();
if dont_jump { if dont_jump {
// We are done // We are done
} else { } else {
let (new_ip, overflow) = ip.overflowing_add_signed(*jump); let (new_ip, overflow) = ip.overflowing_add_signed(jump);
if overflow { if overflow {
return Err(EvaluationError::InstructionPointerOverflow); return Err(EvaluationError::InstructionPointerOverflow);
@ -68,7 +69,8 @@ pub fn execute(
} }
} }
Instruction::Jump { jump } => { Instruction::Jump { jump } => {
let (new_ip, overflow) = ip.overflowing_add_signed(*jump); let jump = if *jump == 0 { 1 } else { *jump };
let (new_ip, overflow) = ip.overflowing_add_signed(jump);
if overflow { if overflow {
return Err(EvaluationError::InstructionPointerOverflow); return Err(EvaluationError::InstructionPointerOverflow);

View file

@ -16,6 +16,7 @@ use winnow::combinator::eof;
use winnow::combinator::not; use winnow::combinator::not;
use winnow::combinator::opt; use winnow::combinator::opt;
use winnow::combinator::peek; use winnow::combinator::peek;
use winnow::combinator::preceded;
use winnow::combinator::repeat_till; use winnow::combinator::repeat_till;
use winnow::combinator::terminated; use winnow::combinator::terminated;
use winnow::combinator::trace; use winnow::combinator::trace;
@ -201,6 +202,7 @@ pub enum TokenKind {
Content, Content,
LeftDelim, LeftDelim,
RightDelim, RightDelim,
TrimWhitespace,
WantsOutput, WantsOutput,
Ident, Ident,
Whitespace, Whitespace,
@ -286,6 +288,7 @@ impl TemplateToken {
content => TokenKind::Content, content => TokenKind::Content,
left_delim => TokenKind::LeftDelim, left_delim => TokenKind::LeftDelim,
right_delim => TokenKind::RightDelim, right_delim => TokenKind::RightDelim,
trim_whitespace => TokenKind::TrimWhitespace,
wants_output => TokenKind::WantsOutput, wants_output => TokenKind::WantsOutput,
ident => TokenKind::Ident, ident => TokenKind::Ident,
whitespace => TokenKind::Whitespace, whitespace => TokenKind::Whitespace,
@ -344,9 +347,10 @@ fn parse_content<'input>(input: &mut Input<'input>) -> PResult<'input, Vec<Templ
fn parse_interpolate<'input>(input: &mut Input<'input>) -> PResult<'input, Vec<TemplateToken>> { fn parse_interpolate<'input>(input: &mut Input<'input>) -> PResult<'input, Vec<TemplateToken>> {
let prev_whitespace = opt(parse_whitespace).parse_next(input)?; let prev_whitespace = opt(parse_whitespace).parse_next(input)?;
let left_delim = "{{".map(TemplateToken::left_delim).parse_next(input)?; let left_delim = "{{".map(TemplateToken::left_delim).parse_next(input)?;
let left_trim = opt("-".map(TemplateToken::trim_whitespace)).parse_next(input)?;
let wants_output = opt("=".map(TemplateToken::wants_output)).parse_next(input)?; let wants_output = opt("=".map(TemplateToken::wants_output)).parse_next(input)?;
let get_tokens = repeat_till(1.., parse_block_token, peek("}}")); let get_tokens = repeat_till(1.., parse_block_token, peek(preceded(opt("-"), "}}")));
let recover = take_until(0.., "}}").void(); let recover = take_until(0.., "}}").void();
let (inside_tokens, _): (Vec<_>, _) = get_tokens let (inside_tokens, _): (Vec<_>, _) = get_tokens
@ -357,14 +361,17 @@ fn parse_interpolate<'input>(input: &mut Input<'input>) -> PResult<'input, Vec<T
}) })
.parse_next(input)?; .parse_next(input)?;
let right_trim = opt("-".map(TemplateToken::trim_whitespace)).parse_next(input)?;
let right_delim = "}}".map(TemplateToken::right_delim).parse_next(input)?; let right_delim = "}}".map(TemplateToken::right_delim).parse_next(input)?;
let post_whitespace = opt(parse_whitespace).parse_next(input)?; let post_whitespace = opt(parse_whitespace).parse_next(input)?;
let mut tokens = vec![]; let mut tokens = vec![];
tokens.extend(prev_whitespace); tokens.extend(prev_whitespace);
tokens.push(left_delim); tokens.push(left_delim);
tokens.extend(left_trim);
tokens.extend(wants_output); tokens.extend(wants_output);
tokens.extend(inside_tokens); tokens.extend(inside_tokens);
tokens.extend(right_trim);
tokens.push(right_delim); tokens.push(right_delim);
tokens.extend(post_whitespace); tokens.extend(post_whitespace);
@ -595,4 +602,29 @@ mod tests {
) )
"#); "#);
} }
#[test]
fn parse_trim_whitespace() {
let input = "\n\n{{-= hello -}} \n\n";
let output = parse(input.into());
insta::assert_debug_snapshot!(output, @r#"
Ok(
ParsedTemplate {
tokens: [
[Whitespace]"\n\n" (0..2),
[LeftDelim]"{{" (2..4),
[TrimWhitespace]"-" (4..5),
[WantsOutput]"=" (5..6),
[Whitespace]" " (6..7),
[Ident]"hello" (7..12),
[Whitespace]" " (12..13),
[TrimWhitespace]"-" (13..14),
[RightDelim]"}}" (14..16),
[Whitespace]" \n\n" (16..19),
],
},
)
"#);
}
} }

View file

@ -0,0 +1,34 @@
---
source: tests/file_tests.rs
expression: parsed
input_file: tests/cases/trim_whitespace.nomo
---
ParsedTemplate {
tokens: [
[LeftDelim]"{{" (0..2),
[Whitespace]" " (2..3),
[ConditionalIf]"if" (3..5),
[Whitespace]" " (5..6),
[Ident]"test" (6..10),
[Whitespace]" " (10..11),
[TrimWhitespace]"-" (11..12),
[RightDelim]"}}" (12..14),
[Whitespace]"\n " (14..19),
[Content]"Hello" (19..24),
[Whitespace]" " (24..25),
[LeftDelim]"{{" (25..27),
[WantsOutput]"=" (27..28),
[Whitespace]" " (28..29),
[Ident]"stuff" (29..34),
[Whitespace]" " (34..35),
[TrimWhitespace]"-" (35..36),
[RightDelim]"}}" (36..38),
[Whitespace]"\n" (38..39),
[LeftDelim]"{{" (39..41),
[TrimWhitespace]"-" (41..42),
[Whitespace]" " (42..43),
[End]"end" (43..46),
[Whitespace]" " (46..47),
[RightDelim]"}}" (47..49),
],
}

View file

@ -0,0 +1,43 @@
---
source: tests/file_tests.rs
expression: ast
input_file: tests/cases/trim_whitespace.nomo
---
TemplateAst {
root: [
ConditionalChain {
chain: [
Block {
prev_whitespace_content: None,
expression: IfConditional {
expression: VariableAccess(
[Ident]"test" (6..10),
),
},
post_whitespace_content: None,
},
ConditionalContent {
content: [
StaticContent(
[Content]"Hello" (19..24),
),
Interpolation {
prev_whitespace_content: Some(
[Whitespace]" " (24..25),
),
expression: VariableAccess(
[Ident]"stuff" (29..34),
),
post_whitespace_content: None,
},
],
},
Block {
prev_whitespace_content: None,
expression: EndBlock,
post_whitespace_content: None,
},
],
},
],
}

View file

@ -0,0 +1,39 @@
---
source: tests/file_tests.rs
expression: emit
input_file: tests/cases/trim_whitespace.nomo
---
[
LoadFromContextToSlot {
name: "test" (6..10),
slot: VariableSlot {
index: 0,
},
},
JumpIfNotTrue {
emit_slot: VariableSlot {
index: 0,
},
jump: 5,
},
AppendContent {
content: "Hello" (19..24),
},
AppendContent {
content: " " (24..25),
},
LoadFromContextToSlot {
name: "stuff" (29..34),
slot: VariableSlot {
index: 1,
},
},
EmitFromSlot {
slot: VariableSlot {
index: 1,
},
},
Jump {
jump: 0,
},
]

View file

@ -0,0 +1,6 @@
---
source: tests/file_tests.rs
expression: output
input_file: tests/cases/trim_whitespace.nomo
---
"Hello Hemera"

View file

@ -0,0 +1,8 @@
{
"test": true,
"stuff": "Hemera"
}
---
{{ if test -}}
Hello {{= stuff -}}
{{- end }}