tinytemplate/
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};
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<'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}
39impl<'template> TemplateCompiler<'template> {
40    /// Create a new template compiler to parse and compile the given 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.
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.
186    fn parse_path(&self, text: &'template str) -> Result<Path<'template>> {
187        if !text.starts_with('@') {
188            Ok(text.split('.').collect::<Vec<_>>())
189        } else if KNOWN_KEYWORDS.iter().any(|k| *k == text) {
190            Ok(vec![text])
191        } else {
192            Err(self.parse_error(text, format!("Invalid keyword name '{}'", text)))
193        }
194    }
195
196    /// Finds the line number and column where an error occurred. Location is the substring of
197    /// self.original_text where the error was found, and msg is the error message.
198    fn parse_error(&self, location: &str, msg: String) -> Error {
199        let (line, column) = get_offset(self.original_text, location);
200        ParseError { msg, line, column }
201    }
202
203    /// Tags which should have no text after the discriminant use this to raise an error if
204    /// text is found.
205    fn expect_empty(&self, text: &str) -> Result<()> {
206        if text.is_empty() {
207            Ok(())
208        } else {
209            Err(self.parse_error(text, format!("Unexpected text '{}'", text)))
210        }
211    }
212
213    /// Close the branch that is on top of the block stack by setting its target instruction
214    /// and popping it from the stack. Returns an error if the top of the block stack is not a
215    /// branch.
216    fn close_branch(&mut self, new_target: usize, discriminant: &str) -> Result<()> {
217        let branch_block = self.block_stack.pop();
218        if let Some((_, Block::Branch(index))) = branch_block {
219            match &mut self.instructions[index] {
220                Instruction::Branch(_, _, target) => {
221                    *target = new_target;
222                    Ok(())
223                }
224                Instruction::Goto(target) => {
225                    *target = new_target;
226                    Ok(())
227                }
228                _ => panic!(),
229            }
230        } else {
231            Err(self.parse_error(
232                discriminant,
233                "Found a closing endif or else which doesn't match with a preceding if."
234                    .to_string(),
235            ))
236        }
237    }
238
239    /// Close the for loop that is on top of the block stack by setting its target instruction and
240    /// popping it from the stack. Returns an error if the top of the stack is not a for loop.
241    /// Returns the index of the loop's Iterate instruction for further processing.
242    fn close_for(&mut self, new_target: usize, discriminant: &str) -> Result<usize> {
243        let branch_block = self.block_stack.pop();
244        if let Some((_, Block::For(index))) = branch_block {
245            match &mut self.instructions[index] {
246                Instruction::Iterate(target) => {
247                    *target = new_target;
248                    Ok(index)
249                }
250                _ => panic!(),
251            }
252        } else {
253            Err(self.parse_error(
254                discriminant,
255                "Found a closing endfor which doesn't match with a preceding for.".to_string(),
256            ))
257        }
258    }
259
260    /// Advance the cursor to the next { and return the consumed text. If `escaped` is true, skips
261    /// a { at the start of the text.
262    fn consume_text(&mut self, escaped: bool) -> &'template str {
263        let search_substr = if escaped {
264            &self.remaining_text[1..]
265        } else {
266            self.remaining_text
267        };
268
269        let mut position = search_substr
270            .find('{')
271            .unwrap_or_else(|| search_substr.len());
272        if escaped {
273            position += 1;
274        }
275
276        let (text, remaining) = self.remaining_text.split_at(position);
277        self.remaining_text = remaining;
278        text
279    }
280
281    /// Advance the cursor to the end of the value tag and return the value's path and optional
282    /// formatter name.
283    fn consume_value(&mut self) -> Result<(Path<'template>, Option<&'template str>)> {
284        let tag = self.consume_tag("}")?;
285        let mut tag = tag[1..(tag.len() - 1)].trim();
286        if tag.starts_with('-') {
287            tag = tag[1..].trim();
288            self.trim_last_whitespace();
289        }
290        if tag.ends_with('-') {
291            tag = tag[0..tag.len() - 1].trim();
292            self.trim_next_whitespace();
293        }
294
295        if let Some(index) = tag.find('|') {
296            let (path_str, name_str) = tag.split_at(index);
297            let name = name_str[1..].trim();
298            let path = self.parse_path(path_str.trim())?;
299            Ok((path, Some(name)))
300        } else {
301            Ok((self.parse_path(tag)?, None))
302        }
303    }
304
305    /// Right-trim whitespace from the last text block we parsed.
306    fn trim_last_whitespace(&mut self) {
307        if let Some(Instruction::Literal(text)) = self.instructions.last_mut() {
308            *text = text.trim_right();
309        }
310    }
311
312    /// Make a note to left-trim whitespace from the next text block we parse.
313    fn trim_next_whitespace(&mut self) {
314        self.trim_next = true;
315    }
316
317    /// Advance the cursor to the end of the current block tag and return the discriminant substring
318    /// and the rest of the text in the tag. Also handles trimming whitespace where needed.
319    fn consume_block(&mut self) -> Result<(&'template str, &'template str)> {
320        let tag = self.consume_tag("}}")?;
321        let mut block = tag[2..(tag.len() - 2)].trim();
322        if block.starts_with('-') {
323            block = block[1..].trim();
324            self.trim_last_whitespace();
325        }
326        if block.ends_with('-') {
327            block = block[0..block.len() - 1].trim();
328            self.trim_next_whitespace();
329        }
330        let discriminant = block.split_whitespace().next().unwrap_or(block);
331        let rest = block[discriminant.len()..].trim();
332        Ok((discriminant, rest))
333    }
334
335    /// Advance the cursor to after the given expected_close string and return the text in between
336    /// (including the expected_close characters), or return an error message if we reach the end
337    /// of a line of text without finding it.
338    fn consume_tag(&mut self, expected_close: &str) -> Result<&'template str> {
339        if let Some(line) = self.remaining_text.lines().next() {
340            if let Some(pos) = line.find(expected_close) {
341                let (tag, remaining) = self.remaining_text.split_at(pos + expected_close.len());
342                self.remaining_text = remaining;
343                Ok(tag)
344            } else {
345                Err(self.parse_error(
346                    line,
347                    format!(
348                        "Expected a closing '{}' but found end-of-line instead.",
349                        expected_close
350                    ),
351                ))
352            }
353        } else {
354            Err(self.parse_error(
355                self.remaining_text,
356                format!(
357                    "Expected a closing '{}' but found end-of-text instead.",
358                    expected_close
359                ),
360            ))
361        }
362    }
363
364    /// Parse a with tag to separate the value path from the (optional) name.
365    fn parse_with(&self, with_text: &'template str) -> Result<(Path<'template>, &'template str)> {
366        if let Some(index) = with_text.find(" as ") {
367            let (path_str, name_str) = with_text.split_at(index);
368            let path = self.parse_path(path_str.trim())?;
369            let name = name_str[" as ".len()..].trim();
370            Ok((path, name))
371        } else {
372            Err(self.parse_error(
373                with_text,
374                format!(
375                    "Expected 'as <path>' in with block, but found \"{}\" instead",
376                    with_text
377                ),
378            ))
379        }
380    }
381
382    /// Parse a for tag to separate the value path from the name.
383    fn parse_for(&self, for_text: &'template str) -> Result<(Path<'template>, &'template str)> {
384        if let Some(index) = for_text.find(" in ") {
385            let (name_str, path_str) = for_text.split_at(index);
386            let name = name_str.trim();
387            let path = self.parse_path(path_str[" in ".len()..].trim())?;
388            Ok((path, name))
389        } else {
390            Err(self.parse_error(
391                for_text,
392                format!("Unable to parse for block text '{}'", for_text),
393            ))
394        }
395    }
396
397    /// Parse a call tag to separate the template name and context value.
398    fn parse_call(&self, call_text: &'template str) -> Result<(&'template str, Path<'template>)> {
399        if let Some(index) = call_text.find(" with ") {
400            let (name_str, path_str) = call_text.split_at(index);
401            let name = name_str.trim();
402            let path = self.parse_path(path_str[" with ".len()..].trim())?;
403            Ok((name, path))
404        } else {
405            Err(self.parse_error(
406                call_text,
407                format!("Unable to parse call block text '{}'", call_text),
408            ))
409        }
410    }
411}
412
413#[cfg(test)]
414mod test {
415    use super::*;
416    use instruction::Instruction::*;
417
418    fn compile(text: &'static str) -> Result<Vec<Instruction<'static>>> {
419        TemplateCompiler::new(text).compile()
420    }
421
422    #[test]
423    fn test_compile_literal() {
424        let text = "Test String";
425        let instructions = compile(text).unwrap();
426        assert_eq!(1, instructions.len());
427        assert_eq!(&Literal(text), &instructions[0]);
428    }
429
430    #[test]
431    fn test_compile_value() {
432        let text = "{ foobar }";
433        let instructions = compile(text).unwrap();
434        assert_eq!(1, instructions.len());
435        assert_eq!(&Value(vec!["foobar"]), &instructions[0]);
436    }
437
438    #[test]
439    fn test_compile_value_with_formatter() {
440        let text = "{ foobar | my_formatter }";
441        let instructions = compile(text).unwrap();
442        assert_eq!(1, instructions.len());
443        assert_eq!(
444            &FormattedValue(vec!["foobar"], "my_formatter"),
445            &instructions[0]
446        );
447    }
448
449    #[test]
450    fn test_dotted_path() {
451        let text = "{ foo.bar }";
452        let instructions = compile(text).unwrap();
453        assert_eq!(1, instructions.len());
454        assert_eq!(&Value(vec!["foo", "bar"]), &instructions[0]);
455    }
456
457    #[test]
458    fn test_mixture() {
459        let text = "Hello { name }, how are you?";
460        let instructions = compile(text).unwrap();
461        assert_eq!(3, instructions.len());
462        assert_eq!(&Literal("Hello "), &instructions[0]);
463        assert_eq!(&Value(vec!["name"]), &instructions[1]);
464        assert_eq!(&Literal(", how are you?"), &instructions[2]);
465    }
466
467    #[test]
468    fn test_if_endif() {
469        let text = "{{ if foo }}Hello!{{ endif }}";
470        let instructions = compile(text).unwrap();
471        assert_eq!(2, instructions.len());
472        assert_eq!(&Branch(vec!["foo"], true, 2), &instructions[0]);
473        assert_eq!(&Literal("Hello!"), &instructions[1]);
474    }
475
476    #[test]
477    fn test_if_not_endif() {
478        let text = "{{ if not foo }}Hello!{{ endif }}";
479        let instructions = compile(text).unwrap();
480        assert_eq!(2, instructions.len());
481        assert_eq!(&Branch(vec!["foo"], false, 2), &instructions[0]);
482        assert_eq!(&Literal("Hello!"), &instructions[1]);
483    }
484
485    #[test]
486    fn test_if_else_endif() {
487        let text = "{{ if foo }}Hello!{{ else }}Goodbye!{{ endif }}";
488        let instructions = compile(text).unwrap();
489        assert_eq!(4, instructions.len());
490        assert_eq!(&Branch(vec!["foo"], true, 3), &instructions[0]);
491        assert_eq!(&Literal("Hello!"), &instructions[1]);
492        assert_eq!(&Goto(4), &instructions[2]);
493        assert_eq!(&Literal("Goodbye!"), &instructions[3]);
494    }
495
496    #[test]
497    fn test_with() {
498        let text = "{{ with foo as bar }}Hello!{{ endwith }}";
499        let instructions = compile(text).unwrap();
500        assert_eq!(3, instructions.len());
501        assert_eq!(&PushNamedContext(vec!["foo"], "bar"), &instructions[0]);
502        assert_eq!(&Literal("Hello!"), &instructions[1]);
503        assert_eq!(&PopContext, &instructions[2]);
504    }
505
506    #[test]
507    fn test_foreach() {
508        let text = "{{ for foo in bar.baz }}{ foo }{{ endfor }}";
509        let instructions = compile(text).unwrap();
510        assert_eq!(5, instructions.len());
511        assert_eq!(
512            &PushIterationContext(vec!["bar", "baz"], "foo"),
513            &instructions[0]
514        );
515        assert_eq!(&Iterate(4), &instructions[1]);
516        assert_eq!(&Value(vec!["foo"]), &instructions[2]);
517        assert_eq!(&Goto(1), &instructions[3]);
518        assert_eq!(&PopContext, &instructions[4]);
519    }
520
521    #[test]
522    fn test_strip_whitespace_value() {
523        let text = "Hello,     {- name -}   , how are you?";
524        let instructions = compile(text).unwrap();
525        assert_eq!(3, instructions.len());
526        assert_eq!(&Literal("Hello,"), &instructions[0]);
527        assert_eq!(&Value(vec!["name"]), &instructions[1]);
528        assert_eq!(&Literal(", how are you?"), &instructions[2]);
529    }
530
531    #[test]
532    fn test_strip_whitespace_block() {
533        let text = "Hello,     {{- if name -}}    {name}    {{- endif -}}   , how are you?";
534        let instructions = compile(text).unwrap();
535        assert_eq!(6, instructions.len());
536        assert_eq!(&Literal("Hello,"), &instructions[0]);
537        assert_eq!(&Branch(vec!["name"], true, 5), &instructions[1]);
538        assert_eq!(&Literal(""), &instructions[2]);
539        assert_eq!(&Value(vec!["name"]), &instructions[3]);
540        assert_eq!(&Literal(""), &instructions[4]);
541        assert_eq!(&Literal(", how are you?"), &instructions[5]);
542    }
543
544    #[test]
545    fn test_comment() {
546        let text = "Hello, {# foo bar baz #} there!";
547        let instructions = compile(text).unwrap();
548        assert_eq!(2, instructions.len());
549        assert_eq!(&Literal("Hello, "), &instructions[0]);
550        assert_eq!(&Literal(" there!"), &instructions[1]);
551    }
552
553    #[test]
554    fn test_strip_whitespace_comment() {
555        let text = "Hello, \t\n    {#- foo bar baz -#} \t  there!";
556        let instructions = compile(text).unwrap();
557        assert_eq!(2, instructions.len());
558        assert_eq!(&Literal("Hello,"), &instructions[0]);
559        assert_eq!(&Literal("there!"), &instructions[1]);
560    }
561
562    #[test]
563    fn test_strip_whitespace_followed_by_another_tag() {
564        let text = "{value -}{value} Hello";
565        let instructions = compile(text).unwrap();
566        assert_eq!(3, instructions.len());
567        assert_eq!(&Value(vec!["value"]), &instructions[0]);
568        assert_eq!(&Value(vec!["value"]), &instructions[1]);
569        assert_eq!(&Literal(" Hello"), &instructions[2]);
570    }
571
572    #[test]
573    fn test_call() {
574        let text = "{{ call my_macro with foo.bar }}";
575        let instructions = compile(text).unwrap();
576        assert_eq!(1, instructions.len());
577        assert_eq!(&Call("my_macro", vec!["foo", "bar"]), &instructions[0]);
578    }
579
580    #[test]
581    fn test_curly_brace_escaping() {
582        let text = "body \\{ \nfont-size: {fontsize} \n}";
583        let instructions = compile(text).unwrap();
584        assert_eq!(4, instructions.len());
585        assert_eq!(&Literal("body "), &instructions[0]);
586        assert_eq!(&Literal("{ \nfont-size: "), &instructions[1]);
587        assert_eq!(&Value(vec!["fontsize"]), &instructions[2]);
588        assert_eq!(&Literal(" \n}"), &instructions[3]);
589    }
590
591    #[test]
592    fn test_unclosed_tags() {
593        let tags = vec![
594            "{",
595            "{ foo.bar",
596            "{ foo.bar\n }",
597            "{{",
598            "{{ if foo.bar",
599            "{{ if foo.bar \n}}",
600            "{#",
601            "{# if foo.bar",
602            "{# if foo.bar \n#}",
603        ];
604        for tag in tags {
605            compile(tag).unwrap_err();
606        }
607    }
608
609    #[test]
610    fn test_mismatched_blocks() {
611        let text = "{{ if foo }}{{ with bar }}{{ endif }} {{ endwith }}";
612        compile(text).unwrap_err();
613    }
614
615    #[test]
616    fn test_disallows_invalid_keywords() {
617        let text = "{ @foo }";
618        compile(text).unwrap_err();
619    }
620
621    #[test]
622    fn test_diallows_unknown_block_type() {
623        let text = "{{ foobar }}";
624        compile(text).unwrap_err();
625    }
626
627    #[test]
628    fn test_parse_error_line_column_num() {
629        let text = "\n\n\n{{ foobar }}";
630        let err = compile(text).unwrap_err();
631        if let ParseError { line, column, .. } = err {
632            assert_eq!(4, line);
633            assert_eq!(3, column);
634        } else {
635            panic!("Should have returned a parse error");
636        }
637    }
638
639    #[test]
640    fn test_parse_error_on_unclosed_if() {
641        let text = "{{ if foo }}";
642        compile(text).unwrap_err();
643    }
644
645    #[test]
646    fn test_parse_escaped_open_curly_brace() {
647        let text: &str = r"hello \{world}";
648        let instructions = compile(text).unwrap();
649        assert_eq!(2, instructions.len());
650        assert_eq!(&Literal("hello "), &instructions[0]);
651        assert_eq!(&Literal("{world}"), &instructions[1]);
652    }
653}