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.
|
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
|
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.
|
204 | 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.
|
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.
|
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.
|
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.
|
268 | 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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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 |
|
424 | fn compile(text: &'static str) -> Result<Vec<Instruction<'static>>> {
|
425 | TemplateCompiler::new(text).compile()
|
426 | }
|
427 |
|
428 | #[test]
|
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]
|
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]
|
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]
|
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]
|
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]
|
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]
|
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]
|
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]
|
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]
|
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]
|
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]
|
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]
|
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]
|
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]
|
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]
|
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]
|
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]
|
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]
|
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]
|
655 | fn test_mismatched_blocks() {
|
656 | let text = "{{ if foo }}{{ with bar }}{{ endif }} {{ endwith }}" ;
|
657 | compile(text).unwrap_err();
|
658 | }
|
659 |
|
660 | #[test]
|
661 | fn test_disallows_invalid_keywords() {
|
662 | let text = "{ @foo }" ;
|
663 | compile(text).unwrap_err();
|
664 | }
|
665 |
|
666 | #[test]
|
667 | fn test_diallows_unknown_block_type() {
|
668 | let text = "{{ foobar }}" ;
|
669 | compile(text).unwrap_err();
|
670 | }
|
671 |
|
672 | #[test]
|
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]
|
685 | fn test_parse_error_on_unclosed_if() {
|
686 | let text = "{{ if foo }}" ;
|
687 | compile(text).unwrap_err();
|
688 | }
|
689 |
|
690 | #[test]
|
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 | |