1use super::TemplateError;
28use super::node::{Align, Expr, Node, Pipe, Template};
29
30impl Template {
31 pub fn parse(source: &str) -> Result<Template, TemplateError> {
37 let mut tokens = lex(source)?;
38 trim_standalone(&mut tokens);
39 let mut builder = Builder {
40 tokens: &tokens,
41 pos: 0,
42 };
43 let nodes = builder.sequence()?;
44 if builder.pos != tokens.len() {
45 return Err(TemplateError::new("unexpected control directive"));
46 }
47 Ok(Template { nodes })
48 }
49}
50
51#[derive(Debug, Clone)]
53enum Token {
54 Text(String),
55 Var(Expr),
56 Partial {
57 name: String,
58 map_over: Option<Expr>,
59 sep: Option<String>,
60 },
61 If(Expr),
62 ElseIf(Expr),
63 Else,
64 EndIf,
65 For(Expr),
66 Sep,
67 EndFor,
68}
69
70fn is_blank(c: char) -> bool {
72 c == ' ' || c == '\t' || c == '\r'
73}
74
75fn lex(source: &str) -> Result<Vec<Token>, TemplateError> {
76 let chars: Vec<char> = source.chars().collect();
77 let mut tokens: Vec<Token> = Vec::new();
78 let mut text = String::new();
79 let mut i = 0;
80 let mut col_clean = true;
83
84 while let Some(&c) = chars.get(i) {
85 if c != '$' {
86 text.push(c);
87 col_clean = c == '\n';
88 i += 1;
89 continue;
90 }
91
92 match chars.get(i + 1) {
93 Some('$') => {
94 text.push('$');
95 col_clean = false;
96 i += 2;
97 }
98 Some('-') if chars.get(i + 2) == Some(&'-') => {
99 let mut j = i + 3;
100 while let Some(&d) = chars.get(j) {
101 if d == '\n' {
102 break;
103 }
104 j += 1;
105 }
106 if col_clean {
107 if chars.get(j) == Some(&'\n') {
109 j += 1;
110 }
111 } i = j;
113 }
114 _ => {
115 if !text.is_empty() {
116 tokens.push(Token::Text(std::mem::take(&mut text)));
117 }
118 let (token, next) = directive(&chars, i + 1)?;
119 tokens.push(token);
120 col_clean = false;
121 i = next;
122 }
123 }
124 }
125 if !text.is_empty() {
126 tokens.push(Token::Text(text));
127 }
128 Ok(tokens)
129}
130
131fn directive(chars: &[char], start: usize) -> Result<(Token, usize), TemplateError> {
134 let close = close_index(chars, start)
135 .ok_or_else(|| TemplateError::new("unterminated directive (missing closing `$`)"))?;
136 let interior: String = chars.get(start..close).unwrap_or_default().iter().collect();
137 Ok((interior_token(&interior)?, close + 1))
138}
139
140fn close_index(chars: &[char], start: usize) -> Option<usize> {
144 let mut i = start;
145 let mut in_bracket = false;
146 let mut in_quote = false;
147 while let Some(&c) = chars.get(i) {
148 match c {
149 '\n' => return None,
150 '"' if !in_bracket => in_quote = !in_quote,
151 '[' if !in_quote => in_bracket = true,
152 ']' if !in_quote => in_bracket = false,
153 '$' if !in_quote && !in_bracket => return Some(i),
154 _ => {}
155 }
156 i += 1;
157 }
158 None
159}
160
161fn interior_token(interior: &str) -> Result<Token, TemplateError> {
163 let trimmed = interior.trim();
164 match trimmed {
165 "else" => return Ok(Token::Else),
166 "endif" => return Ok(Token::EndIf),
167 "sep" => return Ok(Token::Sep),
168 "endfor" => return Ok(Token::EndFor),
169 _ => {}
170 }
171 if let Some(arg) = keyword_arg(trimmed, "if") {
172 return Ok(Token::If(parse_expr(arg)?));
173 }
174 if let Some(arg) = keyword_arg(trimmed, "elseif") {
175 return Ok(Token::ElseIf(parse_expr(arg)?));
176 }
177 if let Some(arg) = keyword_arg(trimmed, "for") {
178 return Ok(Token::For(parse_expr(arg)?));
179 }
180 value_token(trimmed)
181}
182
183fn keyword_arg<'a>(text: &'a str, keyword: &str) -> Option<&'a str> {
185 let rest = text.strip_prefix(keyword)?.trim_start();
186 let inner = rest.strip_prefix('(')?.strip_suffix(')')?;
187 Some(inner.trim())
188}
189
190fn value_token(text: &str) -> Result<Token, TemplateError> {
192 if let Some((target, rest)) = text.split_once(':') {
193 let (name, sep) = partial_parts(rest)?;
194 return Ok(Token::Partial {
195 name,
196 map_over: Some(parse_expr(target.trim())?),
197 sep,
198 });
199 }
200 if text.contains("()") {
201 let (name, sep) = partial_parts(text)?;
202 return Ok(Token::Partial {
203 name,
204 map_over: None,
205 sep,
206 });
207 }
208 Ok(Token::Var(parse_expr(text)?))
209}
210
211fn partial_parts(text: &str) -> Result<(String, Option<String>), TemplateError> {
213 let (name, after) = text
214 .trim()
215 .split_once("()")
216 .ok_or_else(|| TemplateError::new("malformed partial (expected `name()`)"))?;
217 let after = after.trim();
218 let sep = if after.is_empty() {
219 None
220 } else {
221 Some(
222 after
223 .strip_prefix('[')
224 .and_then(|s| s.strip_suffix(']'))
225 .ok_or_else(|| TemplateError::new("malformed partial separator (expected `[…]`)"))?
226 .to_string(),
227 )
228 };
229 Ok((name.trim().to_string(), sep))
230}
231
232fn parse_expr(text: &str) -> Result<Expr, TemplateError> {
234 let mut parts = text.split('/');
235 let head = parts.next().unwrap_or("").trim();
236 let path: Vec<String> = head
237 .split('.')
238 .map(|s| s.trim().to_string())
239 .filter(|s| !s.is_empty())
240 .collect();
241 let mut pipes = Vec::new();
242 for part in parts {
243 pipes.push(parse_pipe(part.trim())?);
244 }
245 Ok(Expr { path, pipes })
246}
247
248fn parse_pipe(text: &str) -> Result<Pipe, TemplateError> {
249 let args = pipe_args(text);
250 let name = args.first().map_or("", String::as_str);
251 let pipe = match name {
252 "uppercase" => Pipe::Uppercase,
253 "lowercase" => Pipe::Lowercase,
254 "length" => Pipe::Length,
255 "reverse" => Pipe::Reverse,
256 "first" => Pipe::First,
257 "last" => Pipe::Last,
258 "rest" => Pipe::Rest,
259 "allbutlast" => Pipe::AllButLast,
260 "pairs" => Pipe::Pairs,
261 "alpha" => Pipe::Alpha,
262 "roman" => Pipe::Roman,
263 "chomp" => Pipe::Chomp,
264 "nowrap" => Pipe::Nowrap,
265 "left" | "right" | "center" => {
266 let align = match name {
267 "right" => Align::Right,
268 "center" => Align::Center,
269 _ => Align::Left,
270 };
271 let width = args
272 .get(1)
273 .and_then(|w| w.parse::<usize>().ok())
274 .ok_or_else(|| TemplateError::new("block pipe requires a width"))?;
275 Pipe::Block {
276 align,
277 width,
278 left: args.get(2).cloned().unwrap_or_default(),
279 right: args.get(3).cloned().unwrap_or_default(),
280 }
281 }
282 other => return Err(TemplateError::new(format!("unknown pipe: {other}"))),
283 };
284 Ok(pipe)
285}
286
287fn pipe_args(text: &str) -> Vec<String> {
289 let mut out = Vec::new();
290 let mut chars = text.chars().peekable();
291 while let Some(&c) = chars.peek() {
292 if c.is_whitespace() {
293 chars.next();
294 continue;
295 }
296 let mut buf = String::new();
297 if c == '"' {
298 chars.next();
299 for d in chars.by_ref() {
300 if d == '"' {
301 break;
302 }
303 buf.push(d);
304 }
305 } else {
306 while let Some(&d) = chars.peek() {
307 if d.is_whitespace() {
308 break;
309 }
310 buf.push(d);
311 chars.next();
312 }
313 }
314 out.push(buf);
315 }
316 out
317}
318
319fn trim_standalone(tokens: &mut [Token]) {
336 let mut blocks: Vec<bool> = Vec::new();
340 let drop_newline: Vec<bool> = tokens
341 .iter()
342 .enumerate()
343 .map(|(i, token)| match token {
344 Token::If(_) | Token::For(_) => {
345 let block = forward_blank(tokens, i);
346 blocks.push(block);
347 block
348 }
349 Token::ElseIf(_) | Token::Else | Token::Sep => {
350 blocks.last().copied().unwrap_or(false) && forward_blank(tokens, i)
351 }
352 Token::EndIf | Token::EndFor => {
353 blocks.pop().unwrap_or(false) && forward_blank(tokens, i)
354 }
355 _ => false,
356 })
357 .collect();
358 let absorb_partial: Vec<bool> = tokens
359 .iter()
360 .enumerate()
361 .map(|(i, token)| {
362 matches!(token, Token::Partial { map_over: None, .. })
363 && blank_before(tokens, i)
364 && matches!(tokens.get(i + 1), Some(Token::Text(t)) if t.starts_with('\n'))
365 })
366 .collect();
367 for (i, (&drop_nl, &absorb)) in drop_newline.iter().zip(&absorb_partial).enumerate() {
370 if drop_nl {
371 if let Some(Token::Text(t)) = tokens.get_mut(i + 1) {
372 trim_leading_line(t);
373 }
374 } else if absorb
375 && let Some(Token::Text(t)) = tokens.get_mut(i + 1)
376 && let Some(rest) = t.strip_prefix('\n')
377 {
378 *t = rest.to_string();
379 }
380 }
381}
382
383fn blank_before(tokens: &[Token], i: usize) -> bool {
385 match i.checked_sub(1) {
386 None => true,
387 Some(prev) => match tokens.get(prev) {
388 Some(Token::Text(t)) => match t.rfind('\n') {
389 Some(k) => t.get(k + 1..).unwrap_or("").chars().all(is_blank),
390 None => prev == 0 && t.chars().all(is_blank),
391 },
392 _ => false,
393 },
394 }
395}
396
397fn forward_blank(tokens: &[Token], i: usize) -> bool {
399 match tokens.get(i + 1) {
400 None => true,
401 Some(Token::Text(t)) => match t.find('\n') {
402 Some(k) => t.get(..k).unwrap_or("").chars().all(is_blank),
403 None => i + 1 == tokens.len() - 1 && t.chars().all(is_blank),
404 },
405 _ => false,
406 }
407}
408
409fn trim_leading_line(text: &mut String) {
411 match text.find('\n') {
412 Some(k) => *text = text.get(k + 1..).unwrap_or("").to_string(),
413 None => text.clear(),
414 }
415}
416
417struct Builder<'a> {
419 tokens: &'a [Token],
420 pos: usize,
421}
422
423impl Builder<'_> {
424 fn peek(&self) -> Option<Token> {
425 self.tokens.get(self.pos).cloned()
426 }
427
428 fn sequence(&mut self) -> Result<Vec<Node>, TemplateError> {
431 let mut nodes = Vec::new();
432 while let Some(token) = self.peek() {
433 match token {
434 Token::Text(s) => {
435 self.pos += 1;
436 nodes.push(Node::Literal(s));
437 }
438 Token::Var(expr) => {
439 self.pos += 1;
440 nodes.push(Node::Var(expr));
441 }
442 Token::Partial {
443 name,
444 map_over,
445 sep,
446 } => {
447 self.pos += 1;
448 nodes.push(Node::Partial {
449 name,
450 map_over,
451 sep,
452 });
453 }
454 Token::If(_) => nodes.push(self.conditional()?),
455 Token::For(_) => nodes.push(self.loop_node()?),
456 Token::ElseIf(_) | Token::Else | Token::EndIf | Token::Sep | Token::EndFor => break,
457 }
458 }
459 Ok(nodes)
460 }
461
462 fn conditional(&mut self) -> Result<Node, TemplateError> {
463 let Some(Token::If(cond)) = self.peek() else {
464 return Err(TemplateError::new("expected `if`"));
465 };
466 self.pos += 1;
467 let mut branches = vec![(cond, self.sequence()?)];
468 loop {
469 match self.peek() {
470 Some(Token::ElseIf(cond)) => {
471 self.pos += 1;
472 branches.push((cond, self.sequence()?));
473 }
474 Some(Token::Else) => {
475 self.pos += 1;
476 let otherwise = self.sequence()?;
477 self.expect(&Token::EndIf, "endif")?;
478 return Ok(Node::If {
479 branches,
480 otherwise,
481 });
482 }
483 Some(Token::EndIf) => {
484 self.pos += 1;
485 return Ok(Node::If {
486 branches,
487 otherwise: Vec::new(),
488 });
489 }
490 _ => return Err(TemplateError::new("unterminated `if` (missing `endif`)")),
491 }
492 }
493 }
494
495 fn loop_node(&mut self) -> Result<Node, TemplateError> {
496 let Some(Token::For(expr)) = self.peek() else {
497 return Err(TemplateError::new("expected `for`"));
498 };
499 self.pos += 1;
500 let bind = match expr.path.as_slice() {
504 [only] => Some(only.clone()),
505 _ => None,
506 };
507 let body = self.sequence()?;
508 let mut sep = Vec::new();
509 match self.peek() {
510 Some(Token::Sep) => {
511 self.pos += 1;
512 sep = self.sequence()?;
513 self.expect(&Token::EndFor, "endfor")?;
514 }
515 Some(Token::EndFor) => {
516 self.pos += 1;
517 }
518 _ => return Err(TemplateError::new("unterminated `for` (missing `endfor`)")),
519 }
520 Ok(Node::For {
521 expr,
522 bind,
523 body,
524 sep,
525 })
526 }
527
528 fn expect(&mut self, want: &Token, label: &str) -> Result<(), TemplateError> {
529 match self.peek() {
530 Some(ref got) if std::mem::discriminant(got) == std::mem::discriminant(want) => {
531 self.pos += 1;
532 Ok(())
533 }
534 _ => Err(TemplateError::new(format!("expected `{label}`"))),
535 }
536 }
537}