use super::TemplateError;
use super::node::{Align, Expr, Node, Pipe, Template};
impl Template {
pub fn parse(source: &str) -> Result<Template, TemplateError> {
let mut tokens = lex(source)?;
trim_standalone(&mut tokens);
let mut builder = Builder {
tokens: &tokens,
pos: 0,
};
let nodes = builder.sequence()?;
if builder.pos != tokens.len() {
return Err(TemplateError::new("unexpected control directive"));
}
Ok(Template { nodes })
}
}
#[derive(Debug, Clone)]
enum Token {
Text(String),
Var(Expr),
Partial {
name: String,
map_over: Option<Expr>,
sep: Option<String>,
},
If(Expr),
ElseIf(Expr),
Else,
EndIf,
For(Expr),
Sep,
EndFor,
}
fn is_blank(c: char) -> bool {
c == ' ' || c == '\t' || c == '\r'
}
fn lex(source: &str) -> Result<Vec<Token>, TemplateError> {
let chars: Vec<char> = source.chars().collect();
let mut tokens: Vec<Token> = Vec::new();
let mut text = String::new();
let mut i = 0;
let mut col_clean = true;
while let Some(&c) = chars.get(i) {
if c != '$' {
text.push(c);
col_clean = c == '\n';
i += 1;
continue;
}
match chars.get(i + 1) {
Some('$') => {
text.push('$');
col_clean = false;
i += 2;
}
Some('-') if chars.get(i + 2) == Some(&'-') => {
let mut j = i + 3;
while let Some(&d) = chars.get(j) {
if d == '\n' {
break;
}
j += 1;
}
if col_clean {
if chars.get(j) == Some(&'\n') {
j += 1;
}
} i = j;
}
_ => {
if !text.is_empty() {
tokens.push(Token::Text(std::mem::take(&mut text)));
}
let (token, next) = directive(&chars, i + 1)?;
tokens.push(token);
col_clean = false;
i = next;
}
}
}
if !text.is_empty() {
tokens.push(Token::Text(text));
}
Ok(tokens)
}
fn directive(chars: &[char], start: usize) -> Result<(Token, usize), TemplateError> {
let close = close_index(chars, start)
.ok_or_else(|| TemplateError::new("unterminated directive (missing closing `$`)"))?;
let interior: String = chars.get(start..close).unwrap_or_default().iter().collect();
Ok((interior_token(&interior)?, close + 1))
}
fn close_index(chars: &[char], start: usize) -> Option<usize> {
let mut i = start;
let mut in_bracket = false;
let mut in_quote = false;
while let Some(&c) = chars.get(i) {
match c {
'\n' => return None,
'"' if !in_bracket => in_quote = !in_quote,
'[' if !in_quote => in_bracket = true,
']' if !in_quote => in_bracket = false,
'$' if !in_quote && !in_bracket => return Some(i),
_ => {}
}
i += 1;
}
None
}
fn interior_token(interior: &str) -> Result<Token, TemplateError> {
let trimmed = interior.trim();
match trimmed {
"else" => return Ok(Token::Else),
"endif" => return Ok(Token::EndIf),
"sep" => return Ok(Token::Sep),
"endfor" => return Ok(Token::EndFor),
_ => {}
}
if let Some(arg) = keyword_arg(trimmed, "if") {
return Ok(Token::If(parse_expr(arg)?));
}
if let Some(arg) = keyword_arg(trimmed, "elseif") {
return Ok(Token::ElseIf(parse_expr(arg)?));
}
if let Some(arg) = keyword_arg(trimmed, "for") {
return Ok(Token::For(parse_expr(arg)?));
}
value_token(trimmed)
}
fn keyword_arg<'a>(text: &'a str, keyword: &str) -> Option<&'a str> {
let rest = text.strip_prefix(keyword)?.trim_start();
let inner = rest.strip_prefix('(')?.strip_suffix(')')?;
Some(inner.trim())
}
fn value_token(text: &str) -> Result<Token, TemplateError> {
if let Some((target, rest)) = text.split_once(':') {
let (name, sep) = partial_parts(rest)?;
return Ok(Token::Partial {
name,
map_over: Some(parse_expr(target.trim())?),
sep,
});
}
if text.contains("()") {
let (name, sep) = partial_parts(text)?;
return Ok(Token::Partial {
name,
map_over: None,
sep,
});
}
Ok(Token::Var(parse_expr(text)?))
}
fn partial_parts(text: &str) -> Result<(String, Option<String>), TemplateError> {
let (name, after) = text
.trim()
.split_once("()")
.ok_or_else(|| TemplateError::new("malformed partial (expected `name()`)"))?;
let after = after.trim();
let sep = if after.is_empty() {
None
} else {
Some(
after
.strip_prefix('[')
.and_then(|s| s.strip_suffix(']'))
.ok_or_else(|| TemplateError::new("malformed partial separator (expected `[…]`)"))?
.to_string(),
)
};
Ok((name.trim().to_string(), sep))
}
fn parse_expr(text: &str) -> Result<Expr, TemplateError> {
let mut parts = text.split('/');
let head = parts.next().unwrap_or("").trim();
let path: Vec<String> = head
.split('.')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
let mut pipes = Vec::new();
for part in parts {
pipes.push(parse_pipe(part.trim())?);
}
Ok(Expr { path, pipes })
}
fn parse_pipe(text: &str) -> Result<Pipe, TemplateError> {
let args = pipe_args(text);
let name = args.first().map_or("", String::as_str);
let pipe = match name {
"uppercase" => Pipe::Uppercase,
"lowercase" => Pipe::Lowercase,
"length" => Pipe::Length,
"reverse" => Pipe::Reverse,
"first" => Pipe::First,
"last" => Pipe::Last,
"rest" => Pipe::Rest,
"allbutlast" => Pipe::AllButLast,
"pairs" => Pipe::Pairs,
"alpha" => Pipe::Alpha,
"roman" => Pipe::Roman,
"chomp" => Pipe::Chomp,
"nowrap" => Pipe::Nowrap,
"left" | "right" | "center" => {
let align = match name {
"right" => Align::Right,
"center" => Align::Center,
_ => Align::Left,
};
let width = args
.get(1)
.and_then(|w| w.parse::<usize>().ok())
.ok_or_else(|| TemplateError::new("block pipe requires a width"))?;
Pipe::Block {
align,
width,
left: args.get(2).cloned().unwrap_or_default(),
right: args.get(3).cloned().unwrap_or_default(),
}
}
other => return Err(TemplateError::new(format!("unknown pipe: {other}"))),
};
Ok(pipe)
}
fn pipe_args(text: &str) -> Vec<String> {
let mut out = Vec::new();
let mut chars = text.chars().peekable();
while let Some(&c) = chars.peek() {
if c.is_whitespace() {
chars.next();
continue;
}
let mut buf = String::new();
if c == '"' {
chars.next();
for d in chars.by_ref() {
if d == '"' {
break;
}
buf.push(d);
}
} else {
while let Some(&d) = chars.peek() {
if d.is_whitespace() {
break;
}
buf.push(d);
chars.next();
}
}
out.push(buf);
}
out
}
fn trim_standalone(tokens: &mut [Token]) {
let mut blocks: Vec<bool> = Vec::new();
let drop_newline: Vec<bool> = tokens
.iter()
.enumerate()
.map(|(i, token)| match token {
Token::If(_) | Token::For(_) => {
let block = forward_blank(tokens, i);
blocks.push(block);
block
}
Token::ElseIf(_) | Token::Else | Token::Sep => {
blocks.last().copied().unwrap_or(false) && forward_blank(tokens, i)
}
Token::EndIf | Token::EndFor => {
blocks.pop().unwrap_or(false) && forward_blank(tokens, i)
}
_ => false,
})
.collect();
let absorb_partial: Vec<bool> = tokens
.iter()
.enumerate()
.map(|(i, token)| {
matches!(token, Token::Partial { map_over: None, .. })
&& blank_before(tokens, i)
&& matches!(tokens.get(i + 1), Some(Token::Text(t)) if t.starts_with('\n'))
})
.collect();
for (i, (&drop_nl, &absorb)) in drop_newline.iter().zip(&absorb_partial).enumerate() {
if drop_nl {
if let Some(Token::Text(t)) = tokens.get_mut(i + 1) {
trim_leading_line(t);
}
} else if absorb
&& let Some(Token::Text(t)) = tokens.get_mut(i + 1)
&& let Some(rest) = t.strip_prefix('\n')
{
*t = rest.to_string();
}
}
}
fn blank_before(tokens: &[Token], i: usize) -> bool {
match i.checked_sub(1) {
None => true,
Some(prev) => match tokens.get(prev) {
Some(Token::Text(t)) => match t.rfind('\n') {
Some(k) => t.get(k + 1..).unwrap_or("").chars().all(is_blank),
None => prev == 0 && t.chars().all(is_blank),
},
_ => false,
},
}
}
fn forward_blank(tokens: &[Token], i: usize) -> bool {
match tokens.get(i + 1) {
None => true,
Some(Token::Text(t)) => match t.find('\n') {
Some(k) => t.get(..k).unwrap_or("").chars().all(is_blank),
None => i + 1 == tokens.len() - 1 && t.chars().all(is_blank),
},
_ => false,
}
}
fn trim_leading_line(text: &mut String) {
match text.find('\n') {
Some(k) => *text = text.get(k + 1..).unwrap_or("").to_string(),
None => text.clear(),
}
}
struct Builder<'a> {
tokens: &'a [Token],
pos: usize,
}
impl Builder<'_> {
fn peek(&self) -> Option<Token> {
self.tokens.get(self.pos).cloned()
}
fn sequence(&mut self) -> Result<Vec<Node>, TemplateError> {
let mut nodes = Vec::new();
while let Some(token) = self.peek() {
match token {
Token::Text(s) => {
self.pos += 1;
nodes.push(Node::Literal(s));
}
Token::Var(expr) => {
self.pos += 1;
nodes.push(Node::Var(expr));
}
Token::Partial {
name,
map_over,
sep,
} => {
self.pos += 1;
nodes.push(Node::Partial {
name,
map_over,
sep,
});
}
Token::If(_) => nodes.push(self.conditional()?),
Token::For(_) => nodes.push(self.loop_node()?),
Token::ElseIf(_) | Token::Else | Token::EndIf | Token::Sep | Token::EndFor => break,
}
}
Ok(nodes)
}
fn conditional(&mut self) -> Result<Node, TemplateError> {
let Some(Token::If(cond)) = self.peek() else {
return Err(TemplateError::new("expected `if`"));
};
self.pos += 1;
let mut branches = vec![(cond, self.sequence()?)];
loop {
match self.peek() {
Some(Token::ElseIf(cond)) => {
self.pos += 1;
branches.push((cond, self.sequence()?));
}
Some(Token::Else) => {
self.pos += 1;
let otherwise = self.sequence()?;
self.expect(&Token::EndIf, "endif")?;
return Ok(Node::If {
branches,
otherwise,
});
}
Some(Token::EndIf) => {
self.pos += 1;
return Ok(Node::If {
branches,
otherwise: Vec::new(),
});
}
_ => return Err(TemplateError::new("unterminated `if` (missing `endif`)")),
}
}
}
fn loop_node(&mut self) -> Result<Node, TemplateError> {
let Some(Token::For(expr)) = self.peek() else {
return Err(TemplateError::new("expected `for`"));
};
self.pos += 1;
let bind = match expr.path.as_slice() {
[only] => Some(only.clone()),
_ => None,
};
let body = self.sequence()?;
let mut sep = Vec::new();
match self.peek() {
Some(Token::Sep) => {
self.pos += 1;
sep = self.sequence()?;
self.expect(&Token::EndFor, "endfor")?;
}
Some(Token::EndFor) => {
self.pos += 1;
}
_ => return Err(TemplateError::new("unterminated `for` (missing `endfor`)")),
}
Ok(Node::For {
expr,
bind,
body,
sep,
})
}
fn expect(&mut self, want: &Token, label: &str) -> Result<(), TemplateError> {
match self.peek() {
Some(ref got) if std::mem::discriminant(got) == std::mem::discriminant(want) => {
self.pos += 1;
Ok(())
}
_ => Err(TemplateError::new(format!("expected `{label}`"))),
}
}
}