tinytemplate_async/
compiler.rs

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.
7use error::Error::*;
8use error::{get_offset, Error, Result};
9use instruction::{Instruction, Path, PathStep};
10
11/// The end point of a branch or goto instruction is not known.
12const 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.
17enum 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.
24static 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.
29pub(crate) struct TemplateCompiler {
30    original_text: String,
31    remaining_text: String,
32    instructions: Vec<Instruction>,
33    block_stack: Vec<(String, 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}
39impl TemplateCompiler {
40    /// Create a new template compiler to parse and compile the given template.
41    pub fn new(text: String) -> TemplateCompiler {
42        TemplateCompiler {
43            original_text: text.to_string(),
44            remaining_text: text.to_string(),
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.
52    pub fn compile(mut self) -> Result<Vec<Instruction>> {
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 = match self.consume_tag("#}") {
59                    Ok(it) => it,
60                    Err(err) => {
61                        self.instructions
62                            .push(Instruction::Literal("{#".to_string()));
63                        self.remaining_text = self.remaining_text[2..].to_string();
64                        continue;
65                    }
66                };
67                let comment = tag[2..(tag.len() - 2)].trim().to_string();
68                if comment.starts_with('-') {
69                    self.trim_last_whitespace();
70                }
71                if comment.ends_with('-') {
72                    self.trim_next_whitespace();
73                }
74            // Block tag. Block tags are wrapped in {{ }} and always have one word at the start
75            // to identify which kind of tag it is. Depending on the tag type there may be more.
76            } else if self.remaining_text.starts_with("{{") {
77                self.trim_next = false;
78
79                let (discriminant, rest) = match self.consume_block() {
80                    Ok(it) => it,
81                    Err(err) => {
82                        self.instructions
83                            .push(Instruction::Literal("{#".to_string()));
84                        self.remaining_text = self.remaining_text[2..].to_string();
85                        continue;
86                    }
87                };
88                match discriminant.as_str() {
89                    "if" => {
90                        let (path, negated) = if rest.starts_with("not") {
91                            (self.parse_path(&rest[4..])?, true)
92                        } else {
93                            (self.parse_path(rest.as_str())?, false)
94                        };
95                        self.block_stack
96                            .push((discriminant, Block::Branch(self.instructions.len())));
97                        self.instructions
98                            .push(Instruction::Branch(path, !negated, UNKNOWN));
99                    }
100                    "else" => {
101                        self.expect_empty(rest.as_str())?;
102                        let num_instructions = self.instructions.len() + 1;
103                        self.close_branch(num_instructions, discriminant.as_str())?;
104                        self.block_stack
105                            .push((discriminant, Block::Branch(self.instructions.len())));
106                        self.instructions.push(Instruction::Goto(UNKNOWN))
107                    }
108                    "endif" => {
109                        self.expect_empty(rest.as_str())?;
110                        let num_instructions = self.instructions.len();
111                        self.close_branch(num_instructions, discriminant.as_str())?;
112                    }
113                    "with" => {
114                        let (path, name) = self.parse_with(rest.as_str())?;
115                        let instruction = Instruction::PushNamedContext(path, name);
116                        self.instructions.push(instruction);
117                        self.block_stack.push((discriminant, Block::With));
118                    }
119                    "endwith" => {
120                        self.expect_empty(rest.as_str())?;
121                        if let Some((_, Block::With)) = self.block_stack.pop() {
122                            self.instructions.push(Instruction::PopContext)
123                        } else {
124                            return Err(self.parse_error(
125                                discriminant.as_str(),
126                                "Found a closing endwith that doesn't match with a preceeding with.".to_string()
127                            ));
128                        }
129                    }
130                    "for" => {
131                        let (path, name) = self.parse_for(rest.as_str())?;
132                        self.instructions
133                            .push(Instruction::PushIterationContext(path, name));
134                        self.block_stack
135                            .push((discriminant, Block::For(self.instructions.len())));
136                        self.instructions.push(Instruction::Iterate(UNKNOWN));
137                    }
138                    "endfor" => {
139                        self.expect_empty(rest.as_str())?;
140                        let num_instructions = self.instructions.len() + 1;
141                        let goto_target =
142                            self.close_for(num_instructions, discriminant.as_str())?;
143                        self.instructions.push(Instruction::Goto(goto_target));
144                        self.instructions.push(Instruction::PopContext);
145                    }
146                    "call" => {
147                        let (name, path) = self.parse_call(rest.as_str())?;
148                        self.instructions.push(Instruction::Call(name, path));
149                    }
150                    _ => {
151                        return Err(self.parse_error(
152                            discriminant.as_str(),
153                            format!("Unknown block type '{}'", discriminant),
154                        ));
155                    }
156                }
157            // Check for an escaped curly brace and consume text until we find a braace that is not escaped
158            } else if self.remaining_text.starts_with("\\{") {
159                let mut escaped = false;
160                loop {
161                    let mut text = self.consume_text(escaped).clone();
162                    if self.trim_next {
163                        text = text.trim_left().to_string();
164                        self.trim_next = false;
165                    }
166                    escaped = text.ends_with('\\');
167                    if escaped {
168                        text = text[..text.len() - 1].to_string();
169                    }
170                    self.instructions.push(Instruction::Literal(text.clone()));
171
172                    if !escaped {
173                        break;
174                    }
175
176                    if escaped && self.remaining_text.is_empty() {
177                        return Err(self.parse_error(
178                            text.as_str(),
179                            "Found an escape that doesn't escape any character.".to_string(),
180                        ));
181                    }
182                }
183            // Values, of the form { dotted.path.to.value.in.context }
184            } else if self.remaining_text.starts_with('{') {
185                self.trim_next = false;
186
187                match self.consume_value() {
188                    Ok((path, name)) => {
189                        let instruction = match name {
190                            Some(name) => Instruction::FormattedValue(path, name),
191                            None => Instruction::Value(path),
192                        };
193                        self.instructions.push(instruction);
194                    }
195                    Err(err) => {
196                        if self.remaining_text.is_empty() {
197                            continue;
198                        }
199                        self.instructions
200                            .push(Instruction::Literal("{".to_string()));
201                        self.remaining_text = self.remaining_text[1..].to_string();
202                    }
203                };
204            // All other text - just consume characters until we see a {
205            } else {
206                let mut escaped = false;
207                loop {
208                    let mut text = self.consume_text(escaped).clone();
209                    if self.trim_next {
210                        text = text.trim_left().to_string();
211                        self.trim_next = false;
212                    }
213                    escaped = text.ends_with('\\');
214                    if escaped {
215                        text = text[..text.len() - 1].to_string();
216                    }
217                    self.instructions.push(Instruction::Literal(text.clone()));
218
219                    if !escaped {
220                        break;
221                    }
222
223                    if escaped && self.remaining_text.is_empty() {
224                        return Err(self.parse_error(
225                            text.as_str(),
226                            "Found an escape that doesn't escape any character.".to_string(),
227                        ));
228                    }
229                }
230            }
231        }
232
233        if let Some((text, _)) = self.block_stack.pop() {
234            return Err(self.parse_error(
235                text.as_str(),
236                "Expected block-closing tag, but reached the end of input.".to_string(),
237            ));
238        }
239
240        Ok(self.instructions)
241    }
242
243    /// Splits a string into a list of named segments which can later be used to look up values in the
244    /// context.
245    fn parse_path(&self, text: &str) -> Result<Path> {
246        if !text.starts_with('@') {
247            Ok(text
248                .split('.')
249                .map(|s| match s.parse::<usize>() {
250                    Ok(n) => PathStep::Index(s.to_string(), n),
251                    Err(_) => PathStep::Name(s.to_string()),
252                })
253                .collect::<Vec<_>>())
254        } else if KNOWN_KEYWORDS.iter().any(|k| *k == text) {
255            Ok(vec![PathStep::Name(text.to_string())])
256        } else {
257            Err(self.parse_error(text, format!("Invalid keyword name '{}'", text)))
258        }
259    }
260
261    /// Finds the line number and column where an error occurred. Location is the substring of
262    /// self.original_text where the error was found, and msg is the error message.
263    fn parse_error(&self, location: &str, msg: String) -> Error {
264        let (line, column) = get_offset(self.original_text.as_str(), location);
265        ParseError { msg, line, column }
266    }
267
268    /// Tags which should have no text after the discriminant use this to raise an error if
269    /// text is found.
270    fn expect_empty(&self, text: &str) -> Result<()> {
271        if text.is_empty() {
272            Ok(())
273        } else {
274            Err(self.parse_error(text, format!("Unexpected text '{}'", text)))
275        }
276    }
277
278    /// Close the branch that is on top of the block stack by setting its target instruction
279    /// and popping it from the stack. Returns an error if the top of the block stack is not a
280    /// branch.
281    fn close_branch(&mut self, new_target: usize, discriminant: &str) -> Result<()> {
282        let branch_block = self.block_stack.pop();
283        if let Some((_, Block::Branch(index))) = branch_block {
284            match &mut self.instructions[index] {
285                Instruction::Branch(_, _, target) => {
286                    *target = new_target;
287                    Ok(())
288                }
289                Instruction::Goto(target) => {
290                    *target = new_target;
291                    Ok(())
292                }
293                _ => panic!(),
294            }
295        } else {
296            Err(self.parse_error(
297                discriminant,
298                "Found a closing endif or else which doesn't match with a preceding if."
299                    .to_string(),
300            ))
301        }
302    }
303
304    /// Close the for loop that is on top of the block stack by setting its target instruction and
305    /// popping it from the stack. Returns an error if the top of the stack is not a for loop.
306    /// Returns the index of the loop's Iterate instruction for further processing.
307    fn close_for(&mut self, new_target: usize, discriminant: &str) -> Result<usize> {
308        let branch_block = self.block_stack.pop();
309        if let Some((_, Block::For(index))) = branch_block {
310            match &mut self.instructions[index] {
311                Instruction::Iterate(target) => {
312                    *target = new_target;
313                    Ok(index)
314                }
315                _ => panic!(),
316            }
317        } else {
318            Err(self.parse_error(
319                discriminant,
320                "Found a closing endfor which doesn't match with a preceding for.".to_string(),
321            ))
322        }
323    }
324
325    /// Advance the cursor to the next { and return the consumed text. If `escaped` is true, skips
326    /// a { at the start of the text.
327    fn consume_text(&mut self, escaped: bool) -> String {
328        let search_substr = if escaped {
329            &self.remaining_text[1..]
330        } else {
331            &self.remaining_text[..]
332        };
333
334        let mut position = search_substr
335            .find('{')
336            .unwrap_or_else(|| search_substr.len());
337        if escaped {
338            position += 1;
339        }
340
341        let remaining_text = self.remaining_text.clone();
342        let (text, remaining) = remaining_text.split_at(position);
343        self.remaining_text = remaining.to_string();
344        text.to_string()
345    }
346
347    /// Advance the cursor to the end of the value tag and return the value's path and optional
348    /// formatter name.
349    fn consume_value(&mut self) -> Result<(Path, Option<String>)> {
350        let tag = self.consume_tag("}")?.to_string();
351        let mut tag = tag[1..(tag.len() - 1)].trim();
352        if tag.starts_with('-') {
353            tag = tag[1..].trim();
354            self.trim_last_whitespace();
355        }
356        if tag.ends_with('-') {
357            tag = tag[0..tag.len() - 1].trim();
358            self.trim_next_whitespace();
359        }
360
361        if let Some(index) = tag.find('|') {
362            let (path_str, name_str) = tag.split_at(index);
363            let name = name_str[1..].trim();
364            let path = self.parse_path(path_str.trim())?;
365            Ok((path, Some(name.to_string())))
366        } else {
367            Ok((self.parse_path(tag)?, None))
368        }
369    }
370
371    /// Right-trim whitespace from the last text block we parsed.
372    fn trim_last_whitespace(&mut self) {
373        if let Some(Instruction::Literal(text)) = self.instructions.last_mut() {
374            *text = text.trim_right().to_string();
375        }
376    }
377
378    /// Make a note to left-trim whitespace from the next text block we parse.
379    fn trim_next_whitespace(&mut self) {
380        self.trim_next = true;
381    }
382
383    /// Advance the cursor to the end of the current block tag and return the discriminant substring
384    /// and the rest of the text in the tag. Also handles trimming whitespace where needed.
385    fn consume_block(&mut self) -> Result<(String, String)> {
386        let tag = self.consume_tag("}}")?.to_string();
387        let mut block = tag[2..(tag.len() - 2)].trim();
388        if block.starts_with('-') {
389            block = block[1..].trim();
390            self.trim_last_whitespace();
391        }
392        if block.ends_with('-') {
393            block = block[0..block.len() - 1].trim();
394            self.trim_next_whitespace();
395        }
396        let discriminant = block.split_whitespace().next().unwrap_or(block);
397        let rest = block[discriminant.len()..].trim();
398        Ok((discriminant.to_string(), rest.to_string()))
399    }
400
401    /// Advance the cursor to after the given expected_close string and return the text in between
402    /// (including the expected_close characters), or return an error message if we reach the end
403    /// of a line of text without finding it.
404    /// Assumes that there's a start token with the same length as the close token at the start of
405    /// currently remaining text.
406    fn consume_tag(&mut self, expected_close: &str) -> Result<String> {
407        // We skip over the matching start token for this tag, so that we do not accidentally match
408        // some suffix of it with the close token. We assume that the start token is as long as the
409        // end token.
410        let start_len = expected_close.len();
411        let end_len = expected_close.len();
412        if let Some(line) = self.remaining_text.lines().next() {
413            if let Some(pos) = line[start_len..].find(expected_close) {
414                let remaining_text = self.remaining_text.to_string();
415                let (tag, remaining) = remaining_text.split_at(pos + start_len + end_len);
416                self.remaining_text = remaining.to_string();
417                Ok(tag.to_string())
418            } else {
419                Err(self.parse_error(
420                    line,
421                    format!(
422                        "Expected a closing '{}' but found end-of-line instead.",
423                        expected_close
424                    ),
425                ))
426            }
427        } else {
428            Err(self.parse_error(
429                self.remaining_text.as_str(),
430                format!(
431                    "Expected a closing '{}' but found end-of-text instead.",
432                    expected_close
433                ),
434            ))
435        }
436    }
437
438    /// Parse a with tag to separate the value path from the (optional) name.
439    fn parse_with(&self, with_text: &str) -> Result<(Path, String)> {
440        if let Some(index) = with_text.find(" as ") {
441            let (path_str, name_str) = with_text.split_at(index);
442            let path = self.parse_path(path_str.trim())?;
443            let name = name_str[" as ".len()..].trim();
444            Ok((path, name.to_string()))
445        } else {
446            Err(self.parse_error(
447                with_text,
448                format!(
449                    "Expected 'as <path>' in with block, but found \"{}\" instead",
450                    with_text
451                ),
452            ))
453        }
454    }
455
456    /// Parse a for tag to separate the value path from the name.
457    fn parse_for(&self, for_text: &str) -> Result<(Path, String)> {
458        if let Some(index) = for_text.find(" in ") {
459            let (name_str, path_str) = for_text.split_at(index);
460            let name = name_str.trim();
461            let path = self.parse_path(path_str[" in ".len()..].trim())?;
462            Ok((path, name.to_string()))
463        } else {
464            Err(self.parse_error(
465                for_text,
466                format!("Unable to parse for block text '{}'", for_text),
467            ))
468        }
469    }
470
471    /// Parse a call tag to separate the template name and context value.
472    fn parse_call(&self, call_text: &str) -> Result<(String, Path)> {
473        if let Some(index) = call_text.find(" with ") {
474            let (name_str, path_str) = call_text.split_at(index);
475            let name = name_str.trim();
476            let path = self.parse_path(path_str[" with ".len()..].trim())?;
477            Ok((name.to_string(), path))
478        } else {
479            Err(self.parse_error(
480                call_text,
481                format!("Unable to parse call block text '{}'", call_text),
482            ))
483        }
484    }
485}
486
487#[cfg(test)]
488mod test {
489    use super::*;
490    use instruction::Instruction::*;
491    use std::io::Write;
492
493    fn compile(text: &'static str) -> Result<Vec<Instruction>> {
494        TemplateCompiler::new(text.to_string()).compile()
495    }
496
497    #[test]
498    fn test_compile_literal() {
499        let text = "Test String";
500        let instructions = compile(text).unwrap();
501        assert_eq!(1, instructions.len());
502        assert_eq!(&Literal(text.to_string()), &instructions[0]);
503    }
504
505    #[test]
506    fn test_compile_value() {
507        let text = "{ foobar }";
508        let instructions = compile(text).unwrap();
509        assert_eq!(1, instructions.len());
510        assert_eq!(
511            &Value(vec![PathStep::Name("foobar".to_string())]),
512            &instructions[0]
513        );
514    }
515
516    #[test]
517    fn test_compile_value_with_formatter() {
518        let text = "{ foobar | my_formatter }";
519        let instructions = compile(text).unwrap();
520        assert_eq!(1, instructions.len());
521        assert_eq!(
522            &FormattedValue(
523                vec![PathStep::Name("foobar".to_string())],
524                "my_formatter".to_string()
525            ),
526            &instructions[0]
527        );
528    }
529
530    #[test]
531    fn test_dotted_path() {
532        let text = "{ foo.bar }";
533        let instructions = compile(text).unwrap();
534        assert_eq!(1, instructions.len());
535        assert_eq!(
536            &Value(vec![
537                PathStep::Name("foo".to_string()),
538                PathStep::Name("bar".to_string())
539            ]),
540            &instructions[0]
541        );
542    }
543
544    #[test]
545    fn test_indexed_path() {
546        let text = "{ foo.0.bar }";
547        let instructions = compile(text).unwrap();
548        assert_eq!(1, instructions.len());
549        assert_eq!(
550            &Value(vec![
551                PathStep::Name("foo".to_string()),
552                PathStep::Index("0".to_string(), 0),
553                PathStep::Name("bar".to_string())
554            ]),
555            &instructions[0]
556        );
557    }
558
559    #[test]
560    fn test_mixture() {
561        let text = "Hello { name }, how are you?";
562        let instructions = compile(text).unwrap();
563        assert_eq!(3, instructions.len());
564        assert_eq!(&Literal("Hello ".to_string()), &instructions[0]);
565        assert_eq!(
566            &Value(vec![PathStep::Name("name".to_string())]),
567            &instructions[1]
568        );
569        assert_eq!(&Literal(", how are you?".to_string()), &instructions[2]);
570    }
571
572    #[test]
573    fn test_if_endif() {
574        let text = "{{ if foo }}Hello!{{ endif }}";
575        let instructions = compile(text).unwrap();
576        assert_eq!(2, instructions.len());
577        assert_eq!(
578            &Branch(vec![PathStep::Name("foo".to_string())], true, 2),
579            &instructions[0]
580        );
581        assert_eq!(&Literal("Hello!".to_string()), &instructions[1]);
582    }
583
584    #[test]
585    fn test_if_not_endif() {
586        let text = "{{ if not foo }}Hello!{{ endif }}";
587        let instructions = compile(text).unwrap();
588        assert_eq!(2, instructions.len());
589        assert_eq!(
590            &Branch(vec![PathStep::Name("foo".to_string())], false, 2),
591            &instructions[0]
592        );
593        assert_eq!(&Literal("Hello!".to_string()), &instructions[1]);
594    }
595
596    #[test]
597    fn test_if_else_endif() {
598        let text = "{{ if foo }}Hello!{{ else }}Goodbye!{{ endif }}";
599        let instructions = compile(text).unwrap();
600        assert_eq!(4, instructions.len());
601        assert_eq!(
602            &Branch(vec![PathStep::Name("foo".to_string())], true, 3),
603            &instructions[0]
604        );
605        assert_eq!(&Literal("Hello!".to_string()), &instructions[1]);
606        assert_eq!(&Goto(4), &instructions[2]);
607        assert_eq!(&Literal("Goodbye!".to_string()), &instructions[3]);
608    }
609
610    #[test]
611    fn test_with() {
612        let text = "{{ with foo as bar }}Hello!{{ endwith }}";
613        let instructions = compile(text).unwrap();
614        assert_eq!(3, instructions.len());
615        assert_eq!(
616            &PushNamedContext(vec![PathStep::Name("foo".to_string())], "bar".to_string()),
617            &instructions[0]
618        );
619        assert_eq!(&Literal("Hello!".to_string()), &instructions[1]);
620        assert_eq!(&PopContext, &instructions[2]);
621    }
622
623    #[test]
624    fn test_foreach() {
625        let text = "{{ for foo in bar.baz }}{ foo }{{ endfor }}";
626        let instructions = compile(text).unwrap();
627        assert_eq!(5, instructions.len());
628        assert_eq!(
629            &PushIterationContext(
630                vec![
631                    PathStep::Name("bar".to_string()),
632                    PathStep::Name("baz".to_string())
633                ],
634                "foo".to_string()
635            ),
636            &instructions[0]
637        );
638        assert_eq!(&Iterate(4), &instructions[1]);
639        assert_eq!(
640            &Value(vec![PathStep::Name("foo".to_string())]),
641            &instructions[2]
642        );
643        assert_eq!(&Goto(1), &instructions[3]);
644        assert_eq!(&PopContext, &instructions[4]);
645    }
646
647    #[test]
648    fn test_strip_whitespace_value() {
649        let text = "Hello,     {- name -}   , how are you?";
650        let instructions = compile(text).unwrap();
651        assert_eq!(3, instructions.len());
652        assert_eq!(&Literal("Hello,".to_string()), &instructions[0]);
653        assert_eq!(
654            &Value(vec![PathStep::Name("name".to_string())]),
655            &instructions[1]
656        );
657        assert_eq!(&Literal(", how are you?".to_string()), &instructions[2]);
658    }
659
660    #[test]
661    fn test_strip_whitespace_block() {
662        let text = "Hello,     {{- if name -}}    {name}    {{- endif -}}   , how are you?";
663        let instructions = compile(text).unwrap();
664        assert_eq!(6, instructions.len());
665        assert_eq!(&Literal("Hello,".to_string()), &instructions[0]);
666        assert_eq!(
667            &Branch(vec![PathStep::Name("name".to_string())], true, 5),
668            &instructions[1]
669        );
670        assert_eq!(&Literal("".to_string()), &instructions[2]);
671        assert_eq!(
672            &Value(vec![PathStep::Name("name".to_string())]),
673            &instructions[3]
674        );
675        assert_eq!(&Literal("".to_string()), &instructions[4]);
676        assert_eq!(&Literal(", how are you?".to_string()), &instructions[5]);
677    }
678
679    #[test]
680    fn test_comment() {
681        let text = "Hello, {# foo bar baz #} there!";
682        let instructions = compile(text).unwrap();
683        assert_eq!(2, instructions.len());
684        assert_eq!(&Literal("Hello, ".to_string()), &instructions[0]);
685        assert_eq!(&Literal(" there!".to_string()), &instructions[1]);
686    }
687
688    #[test]
689    fn test_strip_whitespace_comment() {
690        let text = "Hello, \t\n    {#- foo bar baz -#} \t  there!";
691        let instructions = compile(text).unwrap();
692        assert_eq!(2, instructions.len());
693        assert_eq!(&Literal("Hello,".to_string()), &instructions[0]);
694        assert_eq!(&Literal("there!".to_string()), &instructions[1]);
695    }
696
697    #[test]
698    fn test_strip_whitespace_followed_by_another_tag() {
699        let text = "{value -}{value} Hello";
700        let instructions = compile(text).unwrap();
701        assert_eq!(3, instructions.len());
702        assert_eq!(
703            &Value(vec![PathStep::Name("value".to_string())]),
704            &instructions[0]
705        );
706        assert_eq!(
707            &Value(vec![PathStep::Name("value".to_string())]),
708            &instructions[1]
709        );
710        assert_eq!(&Literal(" Hello".to_string()), &instructions[2]);
711    }
712
713    #[test]
714    fn test_call() {
715        let text = "{{ call my_macro with foo.bar }}";
716        let instructions = compile(text).unwrap();
717        assert_eq!(1, instructions.len());
718        assert_eq!(
719            &Call(
720                "my_macro".to_string(),
721                vec![
722                    PathStep::Name("foo".to_string()),
723                    PathStep::Name("bar".to_string())
724                ]
725            ),
726            &instructions[0]
727        );
728    }
729
730    #[test]
731    fn test_curly_brace_escaping() {
732        let text = "body \\{ \nfont-size: {fontsize} \n}";
733        let instructions = compile(text).unwrap();
734        assert_eq!(4, instructions.len());
735        assert_eq!(&Literal("body ".to_string()), &instructions[0]);
736        assert_eq!(&Literal("{ \nfont-size: ".to_string()), &instructions[1]);
737        assert_eq!(
738            &Value(vec![PathStep::Name("fontsize".to_string())]),
739            &instructions[2]
740        );
741        assert_eq!(&Literal(" \n}".to_string()), &instructions[3]);
742    }
743
744    #[test]
745    fn test_unclosed_tags() {
746        let tags = vec![
747            "{",
748            "{ foo.bar",
749            "{ foo.bar\n }",
750            "{{",
751            "{{ if foo.bar",
752            "{{ if foo.bar \n}}",
753            "{#",
754            "{# if foo.bar",
755            "{# if foo.bar \n#}",
756        ];
757        for tag in tags {
758            compile(tag).unwrap();
759        }
760    }
761
762    #[test]
763    fn test_mismatched_blocks() {
764        let text = "{{ if foo }}{{ with bar }}{{ endif }} {{ endwith }}";
765        compile(text).unwrap_err();
766    }
767
768    #[test]
769    fn test_disallows_invalid_keywords() {
770        let text = "{ @foo }";
771        compile(text).unwrap();
772    }
773
774    #[test]
775    fn test_diallows_unknown_block_type() {
776        let text = "{{ foobar }}";
777        compile(text).unwrap_err();
778    }
779
780    #[test]
781    fn test_parse_error_line_column_num() {
782        let text = "\n\n\n{{ foobar }}";
783        let err = compile(text).unwrap_err();
784        if let ParseError { line, column, .. } = err {
785            assert_eq!(4, line);
786            assert_eq!(3, column);
787        } else {
788            panic!("Should have returned a parse error");
789        }
790    }
791
792    #[test]
793    fn test_parse_error_on_unclosed_if() {
794        let text = "{{ if foo }}";
795        compile(text).unwrap_err();
796    }
797
798    #[test]
799    fn test_parse_escaped_open_curly_brace() {
800        let text: &str = r"hello \{world}";
801        let instructions = compile(text).unwrap();
802        assert_eq!(2, instructions.len());
803        assert_eq!(&Literal("hello ".to_string()), &instructions[0]);
804        assert_eq!(&Literal("{world}".to_string()), &instructions[1]);
805    }
806
807    #[test]
808    fn test_unmatched_escape() {
809        let text = r#"0\"#;
810        compile(text).unwrap_err();
811    }
812
813    #[test]
814    fn test_mismatched_closing_tag() {
815        let text = "{#}";
816        compile(text).unwrap();
817    }
818}