Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds unless and elsif support #27

Merged
merged 1 commit into from
Apr 6, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 28 additions & 14 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ extern crate regex;

use std::collections::HashMap;
use lexer::Element;
use tags::{assign_tag, include_tag, break_tag, continue_tag, comment_block, raw_block, for_block,
if_block, capture_block};
use tags::{assign_tag, include_tag, break_tag, continue_tag,
comment_block, raw_block, for_block, if_block, unless_block, capture_block};
use std::default::Default;
use error::Result;

Expand Down Expand Up @@ -136,6 +136,30 @@ pub struct LiquidOptions {
}

impl LiquidOptions {
/// Creates a LiquidOptions instance, pre-seeded with all known
/// tags and blocks.
pub fn with_known_blocks() -> LiquidOptions {
let mut options = LiquidOptions::default();
options.register_known_blocks();
options
}

/// Registers all known tags and blocks in an existing options
/// struct
pub fn register_known_blocks(&mut self) {
self.register_tag("assign", Box::new(assign_tag));
self.register_tag("break", Box::new(break_tag));
self.register_tag("continue", Box::new(continue_tag));
self.register_tag("include", Box::new(include_tag));

self.register_block("raw", Box::new(raw_block));
self.register_block("if", Box::new(if_block));
self.register_block("unless", Box::new(unless_block));
self.register_block("for", Box::new(for_block));
self.register_block("comment", Box::new(comment_block));
self.register_block("capture", Box::new(capture_block));
}

pub fn register_block(&mut self, name: &str, block: Box<Block>) {
self.blocks.insert(name.to_owned(), block);
}
Expand All @@ -161,18 +185,8 @@ impl LiquidOptions {
///
pub fn parse(text: &str, options: LiquidOptions) -> Result<Template> {
let mut options = options;
let tokens = try!(lexer::tokenize(&text));

options.register_tag("assign", Box::new(assign_tag));
options.register_tag("break", Box::new(break_tag));
options.register_tag("continue", Box::new(continue_tag));
options.register_tag("include", Box::new(include_tag));

options.register_block("raw", Box::new(raw_block));
options.register_block("if", Box::new(if_block));
options.register_block("for", Box::new(for_block));
options.register_block("comment", Box::new(comment_block));
options.register_block("capture", Box::new(capture_block));
options.register_known_blocks();

let tokens = try!(lexer::tokenize(&text));
parser::parse(&tokens, &options).map(Template::new)
}
121 changes: 119 additions & 2 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ use LiquidOptions;
use value::Value;
use variable::Variable;
use text::Text;
use std::slice::Iter;
use output::{Output, FilterPrototype, VarOrVal};
use token::Token::{self, Identifier, Colon, Comma, Pipe, StringLiteral, NumberLiteral};
use lexer::Element::{self, Expression, Tag, Raw};
use error::{Error, Result};

use std::slice::Iter;
use std::collections::HashSet;
use std::iter::FromIterator;

pub fn parse(elements: &[Element], options: &LiquidOptions) -> Result<Vec<Box<Renderable>>> {
let mut ret = vec![];
let mut iter = elements.iter();
Expand Down Expand Up @@ -153,6 +156,58 @@ pub fn expect(tokens: &mut Iter<Token>, expected: Token) -> Result<()> {
}
}

/// Describes the optional trailing part of a block split.
pub struct BlockSplit<'a> {
pub delimiter: String,
pub args: &'a [Token],
pub trailing: &'a [Element]
}

/// A sub-block aware splitter that will only split the token stream
/// when it finds a delimter at the top level of the token stream,
/// ignoring any it finds in nested blocks.
///
/// Returns a slice contaiing all elements before the delimiter, and
/// an optional BlockSplit struct describing the delimiter and
/// trailing elements.
pub fn split_block<'a>(tokens: &'a[Element],
delimiters: &[&str],
options: &LiquidOptions) ->
(&'a[Element], Option<BlockSplit<'a>>) {
// construct a fast-lookup cache of the delimiters, as we're going to be
// consulting the delimiter list a *lot*.
let delims : HashSet<&str> = HashSet::from_iter(delimiters.iter().map(|x|*x));
let mut stack : Vec<String> = Vec::new();

for (i, t) in tokens.iter().enumerate() {
if let Tag(ref args, _) = *t {
match args[0] {
Identifier(ref name) if options.blocks.contains_key(name) => {
stack.push("end".to_owned() + name);
},

Identifier(ref name) if Some(name) == stack.last() => {
stack.pop();
},

Identifier(ref name) if stack.is_empty() &&
delims.contains(name.as_str()) => {
let leading = &tokens[0..i];
let split = BlockSplit {
delimiter: name.clone(),
args: args,
trailing: &tokens[i..]
};
return (leading, Some(split));
},
_ => {}
}
}
}

(&tokens[..], None)
}

#[cfg(test)]
mod test {
#[test]
Expand All @@ -166,4 +221,66 @@ mod test {
assert!(expect(&mut tokens, Dot).is_ok());
assert!(expect(&mut tokens, Comma).is_err());
}
}

#[test]
fn token_split_handles_nonmatching_stream() {
use lexer::tokenize;
use super::split_block;
use LiquidOptions;

// A stream of tokens with lots of `else`s in it, but only one at the
// top level, which is where it should split.
let tokens = tokenize(
"{% comment %}A{%endcomment%} bunch of {{text}} with {{no}} else tag"
).unwrap();

// note that we need an options block that has been initilaised with
// the supported block list; otherwise the split_tag function won't know
// which things start a nested block.
let options = LiquidOptions::with_known_blocks();
let (_, trailing) = split_block(&tokens[..], &["else"], &options);
assert!(trailing.is_none());
}


#[test]
fn token_split_honours_nesting() {
use lexer::tokenize;
use token::Token::Identifier;
use lexer::Element::{Tag, Raw};
use super::split_block;
use LiquidOptions;

// A stream of tokens with lots of `else`s in it, but only one at the
// top level, which is where it should split.
let tokens = tokenize(concat!(
"{% for x in (1..10) %}",
"{% if x == 2 %}",
"{% for y (2..10) %}{{y}}{% else %} zz {% endfor %}",
"{% else %}",
"c",
"{% endif %}",
"{% else %}",
"something",
"{% endfor %}",
"{% else %}",
"trailing tags"
)).unwrap();

// note that we need an options block that has been initilaised with
// the supported block list; otherwise the split_tag function won't know
// which things start a nested block.
let options = LiquidOptions::with_known_blocks();
let (_, trailing) = split_block(&tokens[..], &["else"], &options);
match trailing {
Some(split) => {
assert_eq!(split.delimiter, "else");
assert_eq!(split.args, &[Identifier("else".to_owned())]);
assert_eq!(split.trailing, &[
Tag(vec![Identifier("else".to_owned())], "{% else %}".to_owned()),
Raw("trailing tags".to_owned())]);
},
None => panic!("split failed")
}
}
}
58 changes: 33 additions & 25 deletions src/tags/for_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@ use context::{Context, Interrupt};
use LiquidOptions;
use lexer::Element;
use token::Token::{self, Identifier, OpenRound, CloseRound, NumberLiteral, DotDot, Colon};
use parser::{parse, expect};
use parser::{parse, expect, split_block};
use template::Template;
use value::Value;
use error::{Error, Result};
use lexer::Element::Tag;

use std::collections::HashMap;
use std::slice::Iter;
Expand Down Expand Up @@ -182,30 +181,15 @@ pub fn for_block(_tag_name: &str,
}
}

let else_tag = vec![Identifier("else".to_owned())];
let is_not_else = |x : &&Element| {
match *x {
&Tag(ref tokens, _) => *tokens != else_tag,
_ => true
}
};
let (leading, trailing) = split_block(&tokens, &["else"], options);
let item_template = Template::new(try!(parse(leading, options)));

// finally, collect the templates for the item, and the optional "else"
// block
let item_tokens : Vec<Element> = tokens.iter()
.take_while(&is_not_else)
.cloned()
.collect();
let item_template = Template::new(try!(parse(&item_tokens, options)));

let else_tokens : Vec<Element> = tokens.iter()
.skip_while(&is_not_else)
.skip(1)
.cloned()
.collect();
let else_template = match &else_tokens {
ts if ts.is_empty() => None,
ts => Some(Template::new(try!(parse(ts, options))))
let else_template = match trailing {
Some(split) => {
let parsed = try!(parse(&split.trailing[1..], options));
Some(Template::new(parsed))
},
None => None
};

Ok(Box::new(For {
Expand Down Expand Up @@ -313,6 +297,30 @@ mod test{
));
}

#[test]
fn nested_for_loops_with_else() {
// test that nested for loops parse their `else` blocks correctly
let text = concat!(
"{% for x in (0..i) %}",
"{% for y in (0..j) %}inner{% else %}empty inner{% endfor %}",
"{% else %}",
"empty outer",
"{% endfor %}");
let template = parse(text, LiquidOptions::default()).unwrap();
let mut context = Context::new();

context.set_val("i", Value::Num(0f32));
context.set_val("j", Value::Num(0f32));
assert_eq!(template.render(&mut context).unwrap(),
Some("empty outer".to_owned()));

context.set_val("i", Value::Num(1f32));
context.set_val("j", Value::Num(0f32));
assert_eq!(template.render(&mut context).unwrap(),
Some("empty inner".to_owned()));
}


#[test]
fn degenerate_range_is_safe() {
// make sure that a degenerate range (i.e. where max < min)
Expand Down
Loading