1use crate::ast::{Branch, Node, Template, TemplateRef};
8use crate::expr::{self, Expr};
9use crate::lexer::{self, Token};
10use crate::source::{Span, line_column, syntax_error};
11use rustlavel_core::{Error, Result};
12use std::collections::BTreeMap;
13
14pub fn parse(name: &str, source: &str) -> Result<Template> {
16 let mut parser = Parser {
17 tokens: lexer::tokenize(name, source)?,
18 pos: 0,
19 file: name,
20 source,
21 extends: None,
22 sections: BTreeMap::new(),
23 };
24
25 let nodes = parser.block(&[])?;
27
28 Ok(Template {
29 name: name.to_string(),
30 nodes,
31 extends: parser.extends,
32 sections: parser.sections,
33 })
34}
35
36struct Parser<'a> {
37 tokens: Vec<Token>,
38 pos: usize,
39 file: &'a str,
40 source: &'a str,
41 extends: Option<TemplateRef>,
42 sections: BTreeMap<String, Vec<Node>>,
43}
44
45impl<'a> Parser<'a> {
46 fn block(&mut self, stop: &[&str]) -> Result<Vec<Node>> {
49 let mut nodes = Vec::new();
50
51 while let Some(token) = self.tokens.get(self.pos).cloned() {
52 match &token {
53 Token::Text(text) => {
54 self.pos += 1;
55 nodes.push(Node::Text(text.clone()));
56 }
57 Token::Echo { source, offset, escaped } => {
58 self.pos += 1;
59 let expr = expr::parse(self.span(*offset, source.len()))?;
60 nodes.push(Node::Echo { expr, escaped: *escaped });
61 }
62 Token::Directive { name, arguments, arguments_offset, offset } => {
63 if stop.contains(&name.as_str()) {
64 return Ok(nodes);
65 }
66 let span = self.span(*arguments_offset, arguments.len());
67 let offset = *offset;
68 self.pos += 1;
69 match name.as_str() {
70 "if" => nodes.push(self.conditional(span, offset)?),
71 "foreach" => nodes.push(self.loop_over(span, offset)?),
72 "section" => self.section(span, offset)?,
73 "extends" => self.extends(span, offset)?,
74 "yield" => nodes.push(self.yield_section(span)?),
75 "lang" => nodes.push(self.lang(span)?),
76 "route" => nodes.push(self.route(span)?),
77 "include" => {
78 let name = self.template_name(&span, "include")?;
79 nodes.push(Node::Include(self.reference(name, offset)));
80 }
81 _ => return Err(self.unexpected(&token)),
84 }
85 }
86 }
87 }
88
89 Ok(nodes)
90 }
91
92 fn conditional(&mut self, span: Span<'_>, offset: usize) -> Result<Node> {
94 const STOP: [&str; 3] = ["elseif", "else", "endif"];
95
96 let mut branches =
97 vec![Branch { condition: expr::parse(span)?, body: self.block(&STOP)? }];
98 let mut otherwise = None;
99
100 loop {
101 match self.tokens.get(self.pos).cloned() {
102 Some(Token::Directive { name, arguments, arguments_offset, .. })
103 if name == "elseif" =>
104 {
105 if otherwise.is_some() {
106 return Err(self.here("`@elseif` cannot follow `@else`"));
107 }
108 let span = self.span(arguments_offset, arguments.len());
109 self.pos += 1;
110 branches.push(Branch {
111 condition: expr::parse(span)?,
112 body: self.block(&STOP)?,
113 });
114 }
115 Some(Token::Directive { name, .. }) if name == "else" => {
116 if otherwise.is_some() {
117 return Err(self.here("`@else` appears twice in one `@if`"));
118 }
119 self.pos += 1;
120 otherwise = Some(self.block(&STOP)?);
121 }
122 _ => break,
123 }
124 }
125
126 self.close("if", "endif", offset)?;
127 Ok(Node::If { branches, otherwise })
128 }
129
130 fn loop_over(&mut self, span: Span<'_>, offset: usize) -> Result<Node> {
132 let (subject, binding) = expr::parse_foreach(span)?;
133 let body = self.block(&["endforeach"])?;
134 self.close("foreach", "endforeach", offset)?;
135 Ok(Node::Foreach { subject, binding, body })
136 }
137
138 fn section(&mut self, span: Span<'_>, offset: usize) -> Result<()> {
141 let arguments = expr::parse_arguments(span)?;
142 let name = self.literal(arguments.first(), &span, "section")?;
143
144 let body = match arguments.len() {
145 1 => {
146 let body = self.block(&["endsection"])?;
147 self.close("section", "endsection", offset)?;
148 body
149 }
150 2 => vec![Node::Echo { expr: arguments[1].clone(), escaped: true }],
151 _ => {
152 return Err(span.error(0, "`@section` takes a name and an optional value"));
153 }
154 };
155
156 if self.sections.insert(name.clone(), body).is_some() {
159 return Err(syntax_error(
160 self.file,
161 self.source,
162 offset,
163 format!("`{name}` is defined by two `@section` blocks in this template"),
164 ));
165 }
166 Ok(())
167 }
168
169 fn extends(&mut self, span: Span<'_>, offset: usize) -> Result<()> {
171 let name = self.template_name(&span, "extends")?;
172 if self.extends.is_some() {
173 return Err(span.error(0, "a template can only `@extends` one layout"));
174 }
175 self.extends = Some(self.reference(name, offset));
176 Ok(())
177 }
178
179 fn yield_section(&mut self, span: Span<'_>) -> Result<Node> {
181 let arguments = expr::parse_arguments(span)?;
182 if arguments.len() > 2 {
183 return Err(span.error(0, "`@yield` takes a name and an optional fallback"));
184 }
185 Ok(Node::Yield {
186 section: self.literal(arguments.first(), &span, "yield")?,
187 default: arguments.get(1).cloned(),
188 })
189 }
190
191 fn lang(&mut self, span: Span<'_>) -> Result<Node> {
200 let arguments = expr::parse_arguments(span)?;
201 let key = self.literal(arguments.first(), &span, "lang")?;
202
203 let rest = &arguments[1.min(arguments.len())..];
204 if rest.len() % 2 != 0 {
205 return Err(span.error(
206 0,
207 "`@lang` takes a key and then pairs of name and value, like \
208 `@lang(\"mail.greeting\", \"name\", user.name)` — one name here has no value",
209 ));
210 }
211
212 let mut replacements = Vec::with_capacity(rest.len() / 2);
213 for pair in rest.chunks(2) {
214 let name = match &pair[0] {
215 Expr::Str(name) => name.clone(),
216 _ => {
217 return Err(span.error(
218 0,
219 "a `@lang` placeholder name has to be quoted, like \
220 `@lang(\"mail.greeting\", \"name\", user.name)`",
221 ))
222 }
223 };
224 replacements.push((name, pair[1].clone()));
225 }
226
227 Ok(Node::Lang { key, replacements })
228 }
229
230 fn route(&mut self, span: Span<'_>) -> Result<Node> {
231 let arguments = expr::parse_arguments(span)?;
232 let name = self.literal(arguments.first(), &span, "route")?;
233
234 let rest = &arguments[1.min(arguments.len())..];
235 if rest.len() % 2 != 0 {
236 return Err(span.error(
237 0,
238 "`@route` takes a route name and then pairs of parameter and value, like \
239 `@route(\"users.show\", \"id\", user.id)` — one parameter here has no value",
240 ));
241 }
242
243 let mut params = Vec::with_capacity(rest.len() / 2);
244 for pair in rest.chunks(2) {
245 let name = match &pair[0] {
246 Expr::Str(name) => name.clone(),
247 _ => {
248 return Err(span.error(
249 0,
250 "a `@lang` placeholder name has to be quoted, like \
251 `@lang(\"mail.greeting\", \"name\", user.name)`",
252 ))
253 }
254 };
255 params.push((name, pair[1].clone()));
256 }
257
258 Ok(Node::Route { name, params })
259 }
260
261 fn template_name(&self, span: &Span<'_>, directive: &str) -> Result<String> {
263 let arguments = expr::parse_arguments(*span)?;
264 if arguments.len() != 1 {
265 return Err(span.error(0, format!("`@{directive}` takes exactly one view name")));
266 }
267 self.literal(arguments.first(), span, directive)
268 }
269
270 fn literal(&self, expr: Option<&Expr>, span: &Span<'_>, directive: &str) -> Result<String> {
273 match expr {
274 Some(Expr::Str(text)) => Ok(text.clone()),
275 _ => Err(span.error(
276 0,
277 format!("`@{directive}` needs a quoted name, like `@{directive}(\"layouts.app\")`"),
278 )),
279 }
280 }
281
282 fn reference(&self, name: String, offset: usize) -> TemplateRef {
283 let (line, column) = line_column(self.source, offset);
284 TemplateRef { name, file: self.file.to_string(), line, column }
285 }
286
287 fn span(&self, offset: usize, len: usize) -> Span<'a> {
290 Span::new(self.file, self.source, offset, len)
291 }
292
293 fn close(&mut self, opener: &str, closer: &str, offset: usize) -> Result<()> {
296 match self.tokens.get(self.pos) {
297 Some(Token::Directive { name, .. }) if name == closer => {
298 self.pos += 1;
299 Ok(())
300 }
301 _ => Err(syntax_error(
302 self.file,
303 self.source,
304 offset,
305 format!("`@{opener}` is never closed — add `@{closer}`"),
306 )),
307 }
308 }
309
310 fn here(&self, message: impl Into<String>) -> Error {
312 let offset = match self.tokens.get(self.pos) {
313 Some(Token::Directive { offset, .. }) | Some(Token::Echo { offset, .. }) => *offset,
314 _ => self.source.len(),
315 };
316 syntax_error(self.file, self.source, offset, message)
317 }
318
319 fn unexpected(&self, token: &Token) -> Error {
320 match token {
321 Token::Directive { name, offset, .. } => {
322 let opener = match name.as_str() {
323 "endif" | "elseif" | "else" => "@if",
324 "endforeach" => "@foreach",
325 "endsection" => "@section",
326 _ => "a block",
327 };
328 syntax_error(
329 self.file,
330 self.source,
331 *offset,
332 format!("`@{name}` has no matching `{opener}`"),
333 )
334 }
335 _ => self.here("unexpected token"),
336 }
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343
344 fn template(source: &str) -> Template {
345 parse("test", source).unwrap()
346 }
347
348 fn error(source: &str) -> (usize, usize, String) {
349 match parse("test", source).unwrap_err() {
350 Error::Template { line, column, message, .. } => (line, column, message),
351 other => panic!("expected a template error, got {other:?}"),
352 }
353 }
354
355 #[test]
356 fn builds_an_if_chain_with_every_arm() {
357 let nodes = template("@if(a)A@elseif(b)B@else C@endif").nodes;
358
359 match &nodes[0] {
360 Node::If { branches, otherwise } => {
361 assert_eq!(branches.len(), 2);
362 assert_eq!(branches[0].condition, Expr::Path("a".into()));
363 assert_eq!(branches[1].body, vec![Node::Text("B".into())]);
364 assert_eq!(otherwise.as_deref(), Some(&[Node::Text(" C".into())][..]));
365 }
366 other => panic!("expected an if, got {other:?}"),
367 }
368 }
369
370 #[test]
371 fn builds_a_foreach_with_its_binding() {
372 let nodes = template("@foreach(user.posts as post){{ post.title }}@endforeach").nodes;
373
374 match &nodes[0] {
375 Node::Foreach { subject, binding, body } => {
376 assert_eq!(subject, &Expr::Path("user.posts".into()));
377 assert_eq!(binding, "post");
378 assert_eq!(body.len(), 1);
379 }
380 other => panic!("expected a foreach, got {other:?}"),
381 }
382 }
383
384 #[test]
385 fn hoists_sections_out_of_the_node_stream() {
386 let parsed = template("@extends(\"layouts.app\")\n@section(\"body\")hi@endsection");
387
388 assert_eq!(parsed.extends.as_ref().map(|r| r.name.as_str()), Some("layouts.app"));
389 assert_eq!(parsed.sections["body"], vec![Node::Text("hi".into())]);
390 assert!(parsed.nodes.is_empty(), "{:?}", parsed.nodes);
391 }
392
393 #[test]
394 fn the_short_section_form_echoes_its_value() {
395 let parsed = template("@section(\"title\", page.name)");
396
397 assert_eq!(
398 parsed.sections["title"],
399 vec![Node::Echo { expr: Expr::Path("page.name".into()), escaped: true }]
400 );
401 }
402
403 #[test]
404 fn records_where_a_reference_was_written() {
405 let parsed = template("<p>\n @include(\"parts.nav\")\n");
406
407 match &parsed.nodes[1] {
408 Node::Include(reference) => {
409 assert_eq!(reference.name, "parts.nav");
410 assert_eq!((reference.line, reference.column), (2, 3));
411 }
412 other => panic!("expected an include, got {other:?}"),
413 }
414 }
415
416 #[test]
417 fn an_unclosed_block_is_reported_at_the_line_it_opened() {
418 let (line, column, message) = error("<ul>\n@if(ready)\n <li></li>\n</ul>\n");
419
420 assert_eq!((line, column), (2, 1));
421 assert!(message.contains("`@if` is never closed"), "{message}");
422 }
423
424 #[test]
425 fn a_stray_closing_directive_names_what_it_should_have_closed() {
426 let (line, _, message) = error("<p>ok</p>\n@endforeach\n");
427
428 assert_eq!(line, 2);
429 assert!(message.contains("@foreach"), "{message}");
430 }
431
432 #[test]
433 fn a_view_name_has_to_be_a_literal() {
434 let (_, _, message) = error("@include(page)");
435
436 assert!(message.contains("quoted name"), "{message}");
437 }
438
439 #[test]
440 fn two_sections_of_one_name_are_rejected() {
441 let (_, _, message) =
442 error("@section(\"a\")1@endsection\n@section(\"a\")2@endsection\n");
443
444 assert!(message.contains("two `@section`"), "{message}");
445 }
446
447 #[test]
450 fn a_lang_key_has_to_be_a_literal() {
451 let (_, _, message) = error(r#"@lang(key)"#);
452 assert!(message.contains("`@lang` needs a quoted name"), "{message}");
453 }
454
455 #[test]
458 fn a_lang_placeholder_needs_a_value() {
459 let (_, _, message) = error(r#"@lang("mail.greeting", "name")"#);
460 assert!(message.contains("no value"), "{message}");
461 }
462
463 #[test]
464 fn a_lang_placeholder_name_has_to_be_quoted() {
465 let (_, _, message) = error(r#"@lang("mail.greeting", name, user.name)"#);
466 assert!(message.contains("has to be quoted"), "{message}");
467 }
468
469 #[test]
470 fn a_lang_directive_keeps_its_key_and_pairs() {
471 let template = template(r#"@lang("auth.welcome", "name", user.name)"#);
472 match &template.nodes[0] {
473 Node::Lang { key, replacements } => {
474 assert_eq!(key, "auth.welcome");
475 assert_eq!(replacements.len(), 1);
476 assert_eq!(replacements[0].0, "name");
477 }
478 other => panic!("expected a Lang node, got {other:?}"),
479 }
480 }
481}