| 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 | |