• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 #![allow(deprecated)]
2 
3 /// The compiler module houses the code which parses and compiles templates. TinyTemplate implements
4 /// a simple bytecode interpreter (see the [instruction] module for more details) to render templates.
5 /// The [`TemplateCompiler`](struct.TemplateCompiler.html) struct is responsible for parsing the
6 /// template strings and generating the appropriate bytecode instructions.
7 use error::Error::*;
8 use error::{get_offset, Error, Result};
9 use instruction::{Instruction, Path, PathStep};
10 
11 /// The end point of a branch or goto instruction is not known.
12 const UNKNOWN: usize = ::std::usize::MAX;
13 
14 /// The compiler keeps a stack of the open blocks so that it can ensure that blocks are closed in
15 /// the right order. The Block type is a simple enumeration of the kinds of blocks that could be
16 /// open. It may contain the instruction index corresponding to the start of the block.
17 enum Block {
18     Branch(usize),
19     For(usize),
20     With,
21 }
22 
23 /// List of the known @-keywords so that we can error if the user spells them wrong.
24 static KNOWN_KEYWORDS: [&str; 4] = ["@index", "@first", "@last", "@root"];
25 
26 /// The TemplateCompiler struct is responsible for parsing a template string and generating bytecode
27 /// instructions based on it. The parser is a simple hand-written pattern-matching parser with no
28 /// recursion, which makes it relatively easy to read.
29 pub(crate) struct TemplateCompiler<'template> {
30     original_text: &'template str,
31     remaining_text: &'template str,
32     instructions: Vec<Instruction<'template>>,
33     block_stack: Vec<(&'template str, Block)>,
34 
35     /// When we see a `{foo -}` or similar, we need to remember to left-trim the next text block we
36     /// encounter.
37     trim_next: bool,
38 }
39 impl<'template> TemplateCompiler<'template> {
40     /// Create a new template compiler to parse and compile the given template.
new(text: &'template str) -> TemplateCompiler<'template>41     pub fn new(text: &'template str) -> TemplateCompiler<'template> {
42         TemplateCompiler {
43             original_text: text,
44             remaining_text: text,
45             instructions: vec![],
46             block_stack: vec![],
47             trim_next: false,
48         }
49     }
50 
51     /// Consume the template compiler to parse the template and return the generated bytecode.
compile(mut self) -> Result<Vec<Instruction<'template>>>52     pub fn compile(mut self) -> Result<Vec<Instruction<'template>>> {
53         while !self.remaining_text.is_empty() {
54             // Comment, denoted by {# comment text #}
55             if self.remaining_text.starts_with("{#") {
56                 self.trim_next = false;
57 
58                 let tag = self.consume_tag("#}")?;
59                 let comment = tag[2..(tag.len() - 2)].trim();
60                 if comment.starts_with('-') {
61                     self.trim_last_whitespace();
62                 }
63                 if comment.ends_with('-') {
64                     self.trim_next_whitespace();
65                 }
66             // Block tag. Block tags are wrapped in {{ }} and always have one word at the start
67             // to identify which kind of tag it is. Depending on the tag type there may be more.
68             } else if self.remaining_text.starts_with("{{") {
69                 self.trim_next = false;
70 
71                 let (discriminant, rest) = self.consume_block()?;
72                 match discriminant {
73                     "if" => {
74                         let (path, negated) = if rest.starts_with("not") {
75                             (self.parse_path(&rest[4..])?, true)
76                         } else {
77                             (self.parse_path(rest)?, false)
78                         };
79                         self.block_stack
80                             .push((discriminant, Block::Branch(self.instructions.len())));
81                         self.instructions
82                             .push(Instruction::Branch(path, !negated, UNKNOWN));
83                     }
84                     "else" => {
85                         self.expect_empty(rest)?;
86                         let num_instructions = self.instructions.len() + 1;
87                         self.close_branch(num_instructions, discriminant)?;
88                         self.block_stack
89                             .push((discriminant, Block::Branch(self.instructions.len())));
90                         self.instructions.push(Instruction::Goto(UNKNOWN))
91                     }
92                     "endif" => {
93                         self.expect_empty(rest)?;
94                         let num_instructions = self.instructions.len();
95                         self.close_branch(num_instructions, discriminant)?;
96                     }
97                     "with" => {
98                         let (path, name) = self.parse_with(rest)?;
99                         let instruction = Instruction::PushNamedContext(path, name);
100                         self.instructions.push(instruction);
101                         self.block_stack.push((discriminant, Block::With));
102                     }
103                     "endwith" => {
104                         self.expect_empty(rest)?;
105                         if let Some((_, Block::With)) = self.block_stack.pop() {
106                             self.instructions.push(Instruction::PopContext)
107                         } else {
108                             return Err(self.parse_error(
109                                 discriminant,
110                                 "Found a closing endwith that doesn't match with a preceeding with.".to_string()
111                             ));
112                         }
113                     }
114                     "for" => {
115                         let (path, name) = self.parse_for(rest)?;
116                         self.instructions
117                             .push(Instruction::PushIterationContext(path, name));
118                         self.block_stack
119                             .push((discriminant, Block::For(self.instructions.len())));
120                         self.instructions.push(Instruction::Iterate(UNKNOWN));
121                     }
122                     "endfor" => {
123                         self.expect_empty(rest)?;
124                         let num_instructions = self.instructions.len() + 1;
125                         let goto_target = self.close_for(num_instructions, discriminant)?;
126                         self.instructions.push(Instruction::Goto(goto_target));
127                         self.instructions.push(Instruction::PopContext);
128                     }
129                     "call" => {
130                         let (name, path) = self.parse_call(rest)?;
131                         self.instructions.push(Instruction::Call(name, path));
132                     }
133                     _ => {
134                         return Err(self.parse_error(
135                             discriminant,
136                             format!("Unknown block type '{}'", discriminant),
137                         ));
138                     }
139                 }
140             // Values, of the form { dotted.path.to.value.in.context }
141             // Note that it is not (currently) possible to escape curly braces in the templates to
142             // prevent them from being interpreted as values.
143             } else if self.remaining_text.starts_with('{') {
144                 self.trim_next = false;
145 
146                 let (path, name) = self.consume_value()?;
147                 let instruction = match name {
148                     Some(name) => Instruction::FormattedValue(path, name),
149                     None => Instruction::Value(path),
150                 };
151                 self.instructions.push(instruction);
152             // All other text - just consume characters until we see a {
153             } else {
154                 let mut escaped = false;
155                 loop {
156                     let mut text = self.consume_text(escaped);
157                     if self.trim_next {
158                         text = text.trim_left();
159                         self.trim_next = false;
160                     }
161                     escaped = text.ends_with('\\');
162                     if escaped {
163                         text = &text[0..(text.len() - 1)];
164                     }
165                     self.instructions.push(Instruction::Literal(text));
166 
167                     if !escaped {
168                         break;
169                     }
170                 }
171             }
172         }
173 
174         if let Some((text, _)) = self.block_stack.pop() {
175             return Err(self.parse_error(
176                 text,
177                 "Expected block-closing tag, but reached the end of input.".to_string(),
178             ));
179         }
180 
181         Ok(self.instructions)
182     }
183 
184     /// Splits a string into a list of named segments which can later be used to look up values in the
185     /// context.
parse_path(&self, text: &'template str) -> Result<Path<'template>>186     fn parse_path(&self, text: &'template str) -> Result<Path<'template>> {
187         if !text.starts_with('@') {
188             Ok(text
189                 .split('.')
190                 .map(|s| match s.parse::<usize>() {
191                     Ok(n) => PathStep::Index(s, n),
192                     Err(_) => PathStep::Name(s),
193                 })
194                 .collect::<Vec<_>>())
195         } else if KNOWN_KEYWORDS.iter().any(|k| *k == text) {
196             Ok(vec![PathStep::Name(text)])
197         } else {
198             Err(self.parse_error(text, format!("Invalid keyword name '{}'", text)))
199         }
200     }
201 
202     /// Finds the line number and column where an error occurred. Location is the substring of
203     /// self.original_text where the error was found, and msg is the error message.
parse_error(&self, location: &str, msg: String) -> Error204     fn parse_error(&self, location: &str, msg: String) -> Error {
205         let (line, column) = get_offset(self.original_text, location);
206         ParseError { msg, line, column }
207     }
208 
209     /// Tags which should have no text after the discriminant use this to raise an error if
210     /// text is found.
expect_empty(&self, text: &str) -> Result<()>211     fn expect_empty(&self, text: &str) -> Result<()> {
212         if text.is_empty() {
213             Ok(())
214         } else {
215             Err(self.parse_error(text, format!("Unexpected text '{}'", text)))
216         }
217     }
218 
219     /// Close the branch that is on top of the block stack by setting its target instruction
220     /// and popping it from the stack. Returns an error if the top of the block stack is not a
221     /// branch.
close_branch(&mut self, new_target: usize, discriminant: &str) -> Result<()>222     fn close_branch(&mut self, new_target: usize, discriminant: &str) -> Result<()> {
223         let branch_block = self.block_stack.pop();
224         if let Some((_, Block::Branch(index))) = branch_block {
225             match &mut self.instructions[index] {
226                 Instruction::Branch(_, _, target) => {
227                     *target = new_target;
228                     Ok(())
229                 }
230                 Instruction::Goto(target) => {
231                     *target = new_target;
232                     Ok(())
233                 }
234                 _ => panic!(),
235             }
236         } else {
237             Err(self.parse_error(
238                 discriminant,
239                 "Found a closing endif or else which doesn't match with a preceding if."
240                     .to_string(),
241             ))
242         }
243     }
244 
245     /// Close the for loop that is on top of the block stack by setting its target instruction and
246     /// popping it from the stack. Returns an error if the top of the stack is not a for loop.
247     /// Returns the index of the loop's Iterate instruction for further processing.
close_for(&mut self, new_target: usize, discriminant: &str) -> Result<usize>248     fn close_for(&mut self, new_target: usize, discriminant: &str) -> Result<usize> {
249         let branch_block = self.block_stack.pop();
250         if let Some((_, Block::For(index))) = branch_block {
251             match &mut self.instructions[index] {
252                 Instruction::Iterate(target) => {
253                     *target = new_target;
254                     Ok(index)
255                 }
256                 _ => panic!(),
257             }
258         } else {
259             Err(self.parse_error(
260                 discriminant,
261                 "Found a closing endfor which doesn't match with a preceding for.".to_string(),
262             ))
263         }
264     }
265 
266     /// Advance the cursor to the next { and return the consumed text. If `escaped` is true, skips
267     /// a { at the start of the text.
consume_text(&mut self, escaped: bool) -> &'template str268     fn consume_text(&mut self, escaped: bool) -> &'template str {
269         let search_substr = if escaped {
270             &self.remaining_text[1..]
271         } else {
272             self.remaining_text
273         };
274 
275         let mut position = search_substr
276             .find('{')
277             .unwrap_or_else(|| search_substr.len());
278         if escaped {
279             position += 1;
280         }
281 
282         let (text, remaining) = self.remaining_text.split_at(position);
283         self.remaining_text = remaining;
284         text
285     }
286 
287     /// Advance the cursor to the end of the value tag and return the value's path and optional
288     /// formatter name.
consume_value(&mut self) -> Result<(Path<'template>, Option<&'template str>)>289     fn consume_value(&mut self) -> Result<(Path<'template>, Option<&'template str>)> {
290         let tag = self.consume_tag("}")?;
291         let mut tag = tag[1..(tag.len() - 1)].trim();
292         if tag.starts_with('-') {
293             tag = tag[1..].trim();
294             self.trim_last_whitespace();
295         }
296         if tag.ends_with('-') {
297             tag = tag[0..tag.len() - 1].trim();
298             self.trim_next_whitespace();
299         }
300 
301         if let Some(index) = tag.find('|') {
302             let (path_str, name_str) = tag.split_at(index);
303             let name = name_str[1..].trim();
304             let path = self.parse_path(path_str.trim())?;
305             Ok((path, Some(name)))
306         } else {
307             Ok((self.parse_path(tag)?, None))
308         }
309     }
310 
311     /// Right-trim whitespace from the last text block we parsed.
trim_last_whitespace(&mut self)312     fn trim_last_whitespace(&mut self) {
313         if let Some(Instruction::Literal(text)) = self.instructions.last_mut() {
314             *text = text.trim_right();
315         }
316     }
317 
318     /// Make a note to left-trim whitespace from the next text block we parse.
trim_next_whitespace(&mut self)319     fn trim_next_whitespace(&mut self) {
320         self.trim_next = true;
321     }
322 
323     /// Advance the cursor to the end of the current block tag and return the discriminant substring
324     /// and the rest of the text in the tag. Also handles trimming whitespace where needed.
consume_block(&mut self) -> Result<(&'template str, &'template str)>325     fn consume_block(&mut self) -> Result<(&'template str, &'template str)> {
326         let tag = self.consume_tag("}}")?;
327         let mut block = tag[2..(tag.len() - 2)].trim();
328         if block.starts_with('-') {
329             block = block[1..].trim();
330             self.trim_last_whitespace();
331         }
332         if block.ends_with('-') {
333             block = block[0..block.len() - 1].trim();
334             self.trim_next_whitespace();
335         }
336         let discriminant = block.split_whitespace().next().unwrap_or(block);
337         let rest = block[discriminant.len()..].trim();
338         Ok((discriminant, rest))
339     }
340 
341     /// Advance the cursor to after the given expected_close string and return the text in between
342     /// (including the expected_close characters), or return an error message if we reach the end
343     /// of a line of text without finding it.
consume_tag(&mut self, expected_close: &str) -> Result<&'template str>344     fn consume_tag(&mut self, expected_close: &str) -> Result<&'template str> {
345         if let Some(line) = self.remaining_text.lines().next() {
346             if let Some(pos) = line.find(expected_close) {
347                 let (tag, remaining) = self.remaining_text.split_at(pos + expected_close.len());
348                 self.remaining_text = remaining;
349                 Ok(tag)
350             } else {
351                 Err(self.parse_error(
352                     line,
353                     format!(
354                         "Expected a closing '{}' but found end-of-line instead.",
355                         expected_close
356                     ),
357                 ))
358             }
359         } else {
360             Err(self.parse_error(
361                 self.remaining_text,
362                 format!(
363                     "Expected a closing '{}' but found end-of-text instead.",
364                     expected_close
365                 ),
366             ))
367         }
368     }
369 
370     /// Parse a with tag to separate the value path from the (optional) name.
parse_with(&self, with_text: &'template str) -> Result<(Path<'template>, &'template str)>371     fn parse_with(&self, with_text: &'template str) -> Result<(Path<'template>, &'template str)> {
372         if let Some(index) = with_text.find(" as ") {
373             let (path_str, name_str) = with_text.split_at(index);
374             let path = self.parse_path(path_str.trim())?;
375             let name = name_str[" as ".len()..].trim();
376             Ok((path, name))
377         } else {
378             Err(self.parse_error(
379                 with_text,
380                 format!(
381                     "Expected 'as <path>' in with block, but found \"{}\" instead",
382                     with_text
383                 ),
384             ))
385         }
386     }
387 
388     /// Parse a for tag to separate the value path from the name.
parse_for(&self, for_text: &'template str) -> Result<(Path<'template>, &'template str)>389     fn parse_for(&self, for_text: &'template str) -> Result<(Path<'template>, &'template str)> {
390         if let Some(index) = for_text.find(" in ") {
391             let (name_str, path_str) = for_text.split_at(index);
392             let name = name_str.trim();
393             let path = self.parse_path(path_str[" in ".len()..].trim())?;
394             Ok((path, name))
395         } else {
396             Err(self.parse_error(
397                 for_text,
398                 format!("Unable to parse for block text '{}'", for_text),
399             ))
400         }
401     }
402 
403     /// Parse a call tag to separate the template name and context value.
parse_call(&self, call_text: &'template str) -> Result<(&'template str, Path<'template>)>404     fn parse_call(&self, call_text: &'template str) -> Result<(&'template str, Path<'template>)> {
405         if let Some(index) = call_text.find(" with ") {
406             let (name_str, path_str) = call_text.split_at(index);
407             let name = name_str.trim();
408             let path = self.parse_path(path_str[" with ".len()..].trim())?;
409             Ok((name, path))
410         } else {
411             Err(self.parse_error(
412                 call_text,
413                 format!("Unable to parse call block text '{}'", call_text),
414             ))
415         }
416     }
417 }
418 
419 #[cfg(test)]
420 mod test {
421     use super::*;
422     use instruction::Instruction::*;
423 
compile(text: &'static str) -> Result<Vec<Instruction<'static>>>424     fn compile(text: &'static str) -> Result<Vec<Instruction<'static>>> {
425         TemplateCompiler::new(text).compile()
426     }
427 
428     #[test]
test_compile_literal()429     fn test_compile_literal() {
430         let text = "Test String";
431         let instructions = compile(text).unwrap();
432         assert_eq!(1, instructions.len());
433         assert_eq!(&Literal(text), &instructions[0]);
434     }
435 
436     #[test]
test_compile_value()437     fn test_compile_value() {
438         let text = "{ foobar }";
439         let instructions = compile(text).unwrap();
440         assert_eq!(1, instructions.len());
441         assert_eq!(&Value(vec![PathStep::Name("foobar")]), &instructions[0]);
442     }
443 
444     #[test]
test_compile_value_with_formatter()445     fn test_compile_value_with_formatter() {
446         let text = "{ foobar | my_formatter }";
447         let instructions = compile(text).unwrap();
448         assert_eq!(1, instructions.len());
449         assert_eq!(
450             &FormattedValue(vec![PathStep::Name("foobar")], "my_formatter"),
451             &instructions[0]
452         );
453     }
454 
455     #[test]
test_dotted_path()456     fn test_dotted_path() {
457         let text = "{ foo.bar }";
458         let instructions = compile(text).unwrap();
459         assert_eq!(1, instructions.len());
460         assert_eq!(
461             &Value(vec![PathStep::Name("foo"), PathStep::Name("bar")]),
462             &instructions[0]
463         );
464     }
465 
466     #[test]
test_indexed_path()467     fn test_indexed_path() {
468         let text = "{ foo.0.bar }";
469         let instructions = compile(text).unwrap();
470         assert_eq!(1, instructions.len());
471         assert_eq!(
472             &Value(vec![
473                 PathStep::Name("foo"),
474                 PathStep::Index("0", 0),
475                 PathStep::Name("bar")
476             ]),
477             &instructions[0]
478         );
479     }
480 
481     #[test]
test_mixture()482     fn test_mixture() {
483         let text = "Hello { name }, how are you?";
484         let instructions = compile(text).unwrap();
485         assert_eq!(3, instructions.len());
486         assert_eq!(&Literal("Hello "), &instructions[0]);
487         assert_eq!(&Value(vec![PathStep::Name("name")]), &instructions[1]);
488         assert_eq!(&Literal(", how are you?"), &instructions[2]);
489     }
490 
491     #[test]
test_if_endif()492     fn test_if_endif() {
493         let text = "{{ if foo }}Hello!{{ endif }}";
494         let instructions = compile(text).unwrap();
495         assert_eq!(2, instructions.len());
496         assert_eq!(
497             &Branch(vec![PathStep::Name("foo")], true, 2),
498             &instructions[0]
499         );
500         assert_eq!(&Literal("Hello!"), &instructions[1]);
501     }
502 
503     #[test]
test_if_not_endif()504     fn test_if_not_endif() {
505         let text = "{{ if not foo }}Hello!{{ endif }}";
506         let instructions = compile(text).unwrap();
507         assert_eq!(2, instructions.len());
508         assert_eq!(
509             &Branch(vec![PathStep::Name("foo")], false, 2),
510             &instructions[0]
511         );
512         assert_eq!(&Literal("Hello!"), &instructions[1]);
513     }
514 
515     #[test]
test_if_else_endif()516     fn test_if_else_endif() {
517         let text = "{{ if foo }}Hello!{{ else }}Goodbye!{{ endif }}";
518         let instructions = compile(text).unwrap();
519         assert_eq!(4, instructions.len());
520         assert_eq!(
521             &Branch(vec![PathStep::Name("foo")], true, 3),
522             &instructions[0]
523         );
524         assert_eq!(&Literal("Hello!"), &instructions[1]);
525         assert_eq!(&Goto(4), &instructions[2]);
526         assert_eq!(&Literal("Goodbye!"), &instructions[3]);
527     }
528 
529     #[test]
test_with()530     fn test_with() {
531         let text = "{{ with foo as bar }}Hello!{{ endwith }}";
532         let instructions = compile(text).unwrap();
533         assert_eq!(3, instructions.len());
534         assert_eq!(
535             &PushNamedContext(vec![PathStep::Name("foo")], "bar"),
536             &instructions[0]
537         );
538         assert_eq!(&Literal("Hello!"), &instructions[1]);
539         assert_eq!(&PopContext, &instructions[2]);
540     }
541 
542     #[test]
test_foreach()543     fn test_foreach() {
544         let text = "{{ for foo in bar.baz }}{ foo }{{ endfor }}";
545         let instructions = compile(text).unwrap();
546         assert_eq!(5, instructions.len());
547         assert_eq!(
548             &PushIterationContext(vec![PathStep::Name("bar"), PathStep::Name("baz")], "foo"),
549             &instructions[0]
550         );
551         assert_eq!(&Iterate(4), &instructions[1]);
552         assert_eq!(&Value(vec![PathStep::Name("foo")]), &instructions[2]);
553         assert_eq!(&Goto(1), &instructions[3]);
554         assert_eq!(&PopContext, &instructions[4]);
555     }
556 
557     #[test]
test_strip_whitespace_value()558     fn test_strip_whitespace_value() {
559         let text = "Hello,     {- name -}   , how are you?";
560         let instructions = compile(text).unwrap();
561         assert_eq!(3, instructions.len());
562         assert_eq!(&Literal("Hello,"), &instructions[0]);
563         assert_eq!(&Value(vec![PathStep::Name("name")]), &instructions[1]);
564         assert_eq!(&Literal(", how are you?"), &instructions[2]);
565     }
566 
567     #[test]
test_strip_whitespace_block()568     fn test_strip_whitespace_block() {
569         let text = "Hello,     {{- if name -}}    {name}    {{- endif -}}   , how are you?";
570         let instructions = compile(text).unwrap();
571         assert_eq!(6, instructions.len());
572         assert_eq!(&Literal("Hello,"), &instructions[0]);
573         assert_eq!(
574             &Branch(vec![PathStep::Name("name")], true, 5),
575             &instructions[1]
576         );
577         assert_eq!(&Literal(""), &instructions[2]);
578         assert_eq!(&Value(vec![PathStep::Name("name")]), &instructions[3]);
579         assert_eq!(&Literal(""), &instructions[4]);
580         assert_eq!(&Literal(", how are you?"), &instructions[5]);
581     }
582 
583     #[test]
test_comment()584     fn test_comment() {
585         let text = "Hello, {# foo bar baz #} there!";
586         let instructions = compile(text).unwrap();
587         assert_eq!(2, instructions.len());
588         assert_eq!(&Literal("Hello, "), &instructions[0]);
589         assert_eq!(&Literal(" there!"), &instructions[1]);
590     }
591 
592     #[test]
test_strip_whitespace_comment()593     fn test_strip_whitespace_comment() {
594         let text = "Hello, \t\n    {#- foo bar baz -#} \t  there!";
595         let instructions = compile(text).unwrap();
596         assert_eq!(2, instructions.len());
597         assert_eq!(&Literal("Hello,"), &instructions[0]);
598         assert_eq!(&Literal("there!"), &instructions[1]);
599     }
600 
601     #[test]
test_strip_whitespace_followed_by_another_tag()602     fn test_strip_whitespace_followed_by_another_tag() {
603         let text = "{value -}{value} Hello";
604         let instructions = compile(text).unwrap();
605         assert_eq!(3, instructions.len());
606         assert_eq!(&Value(vec![PathStep::Name("value")]), &instructions[0]);
607         assert_eq!(&Value(vec![PathStep::Name("value")]), &instructions[1]);
608         assert_eq!(&Literal(" Hello"), &instructions[2]);
609     }
610 
611     #[test]
test_call()612     fn test_call() {
613         let text = "{{ call my_macro with foo.bar }}";
614         let instructions = compile(text).unwrap();
615         assert_eq!(1, instructions.len());
616         assert_eq!(
617             &Call(
618                 "my_macro",
619                 vec![PathStep::Name("foo"), PathStep::Name("bar")]
620             ),
621             &instructions[0]
622         );
623     }
624 
625     #[test]
test_curly_brace_escaping()626     fn test_curly_brace_escaping() {
627         let text = "body \\{ \nfont-size: {fontsize} \n}";
628         let instructions = compile(text).unwrap();
629         assert_eq!(4, instructions.len());
630         assert_eq!(&Literal("body "), &instructions[0]);
631         assert_eq!(&Literal("{ \nfont-size: "), &instructions[1]);
632         assert_eq!(&Value(vec![PathStep::Name("fontsize")]), &instructions[2]);
633         assert_eq!(&Literal(" \n}"), &instructions[3]);
634     }
635 
636     #[test]
test_unclosed_tags()637     fn test_unclosed_tags() {
638         let tags = vec![
639             "{",
640             "{ foo.bar",
641             "{ foo.bar\n }",
642             "{{",
643             "{{ if foo.bar",
644             "{{ if foo.bar \n}}",
645             "{#",
646             "{# if foo.bar",
647             "{# if foo.bar \n#}",
648         ];
649         for tag in tags {
650             compile(tag).unwrap_err();
651         }
652     }
653 
654     #[test]
test_mismatched_blocks()655     fn test_mismatched_blocks() {
656         let text = "{{ if foo }}{{ with bar }}{{ endif }} {{ endwith }}";
657         compile(text).unwrap_err();
658     }
659 
660     #[test]
test_disallows_invalid_keywords()661     fn test_disallows_invalid_keywords() {
662         let text = "{ @foo }";
663         compile(text).unwrap_err();
664     }
665 
666     #[test]
test_diallows_unknown_block_type()667     fn test_diallows_unknown_block_type() {
668         let text = "{{ foobar }}";
669         compile(text).unwrap_err();
670     }
671 
672     #[test]
test_parse_error_line_column_num()673     fn test_parse_error_line_column_num() {
674         let text = "\n\n\n{{ foobar }}";
675         let err = compile(text).unwrap_err();
676         if let ParseError { line, column, .. } = err {
677             assert_eq!(4, line);
678             assert_eq!(3, column);
679         } else {
680             panic!("Should have returned a parse error");
681         }
682     }
683 
684     #[test]
test_parse_error_on_unclosed_if()685     fn test_parse_error_on_unclosed_if() {
686         let text = "{{ if foo }}";
687         compile(text).unwrap_err();
688     }
689 
690     #[test]
test_parse_escaped_open_curly_brace()691     fn test_parse_escaped_open_curly_brace() {
692         let text: &str = r"hello \{world}";
693         let instructions = compile(text).unwrap();
694         assert_eq!(2, instructions.len());
695         assert_eq!(&Literal("hello "), &instructions[0]);
696         assert_eq!(&Literal("{world}"), &instructions[1]);
697     }
698 }
699