8000 Adds `break` and `continue` tags. by tcsc · Pull Request #26 · cobalt-org/liquid-rust · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Adds break and continue tags. #26

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

Merged
merged 1 commit into from
Mar 29, 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
27 changes: 27 additions & 0 deletions src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,23 @@ use std::collections::HashMap;
use token::Token::{self, Identifier, StringLiteral, NumberLiteral, BooleanLiteral};
use value::Value;

#[derive(Clone)]
pub enum Interrupt { Continue, Break }

type ValueMap = HashMap<String, Value>;

#[derive(Default)]
pub struct Context {
stack: Vec<ValueMap>,
globals: ValueMap,

/// The current interrupt state. The interrupt state is used by
/// the `break` and `continue` tags to halt template rendering
/// at a given point and unwind the `render` call stack until
/// it reaches an enclosing `for_loop`. At that point the interrupt
/// is cleared, and the `for_loop` carries on processing as directed.
interrupt: Option<Interrupt>,

// Public for backwards compatability
pub filters: HashMap<String, Box<Filter>>
}
Expand Down Expand Up @@ -42,6 +52,7 @@ impl Context {
filters: HashMap<String, Box<Filter>>) -> Context {
Context {
stack: vec!(HashMap::new()),
interrupt: None,
globals: values,
filters: filters
}
Expand All @@ -55,6 +66,22 @@ impl Context {
self.filters.get(name)
}

pub fn interrupted(&self) -> bool {
self.interrupt.is_some()
}

/// Sets the interrupt state. Any previous state is obliterated.
pub fn set_interrupt(&mut self, interrupt: Interrupt) {
self.interrupt = Some(interrupt);
}

/// Fetches and clears the interrupt state.
pub fn pop_interrupt(&mut self) -> Option<Interrupt> {
let rval = self.interrupt.clone();
self.interrupt = None;
rval
}

/// Creates a new variable scope chained to a parent scope.
fn push_scope(&mut self) {
self.stack.push(HashMap::new());
Expand Down
5 changes: 4 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ extern crate regex;

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

Expand Down Expand Up @@ -162,6 +163,8 @@ pub fn parse(text: &str, options: LiquidOptions) -> Result<Template> {
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_block("raw", Box::new(raw_block));
options.register_block("if", Box::new(if_block));
Expand Down
36 changes: 29 additions & 7 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,22 +98,44 @@ fn parse_tag(iter: &mut Iter<Element>,
tokens: &[Token],
options: &LiquidOptions)
-> Result<Box<Renderable>> {
match tokens[0] {
let tag = &tokens[0];
match *tag {
// is a tag
Identifier(ref x) if options.tags.contains_key(x) => {
options.tags.get(x).unwrap()(&x, &tokens[1..], options)
}

// is a block
Identifier(ref x) if options.blocks.contains_key(x) => {
// Collect all the inner elements of this block until we find a
// matching "end<blockname>" tag. Note that there may be nested blocks
// of the same type (and hence have the same closing delimiter) *inside*
// the body of the block, which would premauturely stop the element
// collection early if we did a nesting-unaware search for the
// closing tag.
//
// The whole nesting count machinery below is to ensure we only stop
// collecting elements when we have an un-nested closing tag.

let end_tag = Identifier("end".to_owned() + &x);
let mut children = vec![];
loop {
children.push(match iter.next() {
Some(&Tag(ref tokens, _)) if tokens[0] == end_tag => break,
None => break,
Some(t) => t.clone(),
})
let mut nesting_depth = 0;
for t in iter {
if let &Tag(ref tokens, _) = t {
match tokens[0] {
ref n if n == tag => {
nesting_depth += 1;
},
ref n if n == &end_tag && nesting_depth > 0 => {
nesting_depth -= 1;
},
ref n if n == &end_tag && nesting_depth == 0 => {
break
},
_ => {}
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you quickly elaborate (and maybe write a short comment) why you need to collect the nesting_depth here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, clever indeed

children.push(t.clone())
}
options.blocks.get(x).unwrap()(&x, &tokens[1..], children, options)
}
Expand Down
34 changes: 33 additions & 1 deletion src/tags/for_block.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use Renderable;
use context::Context;
use context::{Context, Interrupt};
use LiquidOptions;
use lexer::Element;
use token::Token::{self, Identifier, OpenRound, CloseRound, NumberLiteral, DotDot, Colon};
Expand Down Expand Up @@ -95,6 +95,14 @@ impl Renderable for For {
scope.set_local_val(&self.var_name, v.clone());
let inner = try!(self.item_template.render(&mut scope)).unwrap_or("".to_owned());
ret = ret + &inner;

// given that we're at the end of the loop body
// already, dealing with a `continue` signal is just
// clearing the interrupt and carrying on as normal. A
// `break` requires some special handling, though.
if let Some(Interrupt::Break) = scope.pop_interrupt() {
break;
}
}

Ok(Some(ret))
Expand Down Expand Up @@ -281,6 +289,30 @@ mod test{
Some("#1 test 42, #2 test 43, #3 test 44, #4 test 45, ".to_string()));
}

#[test]
fn nested_for_loops() {
// test that nest nested for loops work, and that the
// variable scopes between the inner and outer variable
// scopes do not overlap.
let text = concat!(
"{% for outer in (1..5) %}",
">>{{for_loop.index0}}:{{outer}}>>",
"{% for inner in (6..10) %}",
"{{outer}}:{{for_loop.index0}}:{{inner}},",
"{% endfor %}",
">>{{outer}}>>\n",
"{% endfor %}");
let template = parse(text, LiquidOptions::default()).unwrap();
let mut context = Context::new();
let output = template.render(&mut context);
assert_eq!(output.unwrap(), Some(concat!(
">>0:1>>1:0:6,1:1:7,1:2:8,1:3:9,>>1>>\n",
">>1:2>>2:0:6,2:1:7,2:2:8,2:3:9,>>2>>\n",
">>2:3>>3:0:6,3:1:7,3:2:8,3:3:9,>>3>>\n",
">>3:4>>4:0:6,4:1:7,4:2:8,4:3:9,>>4>>\n").to_owned()
));
}

#[test]
fn degenerate_range_is_safe() {
// make sure that a degenerate range (i.e. where max < min)
Expand Down
141 changes: 141 additions & 0 deletions src/tags/interrupt_tags.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
use error::{Error, Result};
use context::{Context, Interrupt};
use Token;
use LiquidOptions;
use Renderable;

struct Break;

impl Renderable for Break {
fn render(&self, context: &mut Context) -> Result<Option<String>> {
context.set_interrupt(Interrupt::Break);
Ok(None)
}
}

pub fn break_tag(_tag_name: &str,
arguments: &[Token],
_options: &LiquidOptions) -> Result<Box<Renderable>> {

// no arguments should be supplied, trying to supply them is an error
if arguments.len() > 0 {
return Error::parser("%}", arguments.first());
}
return Ok(Box::new(Break));
}

struct Continue;

impl Renderable for Continue {
fn render(&self, context: &mut Context) -> Result<Option<String>> {
context.set_interrupt(Interrupt::Continue);
Ok(None)
}
}

pub fn continue_tag(_tag_name: &str,
arguments: &[Token],
_options: &LiquidOptions) -> Result<Box<Renderable>> {
// no arguments should be supplied, trying to supply them is an error
if arguments.len() > 0 {
return Error::parser("%}", arguments.first());
}
return Ok(Box::new(Continue));
}


#[cfg(test)]
mod test {
use Context;
use LiquidOptions;
use Renderable;
use parse;

#[test]
fn test_simple_break() {
let text = concat!(
"{% for i in (0..10) %}",
"enter-{{i}};",
"{% if i == 2 %}break-{{i}}\n{% break %}{% endif %}",
"exit-{{i}}\n",
"{% endfor %}");
let template = parse(text, LiquidOptions::default()).unwrap();

let mut ctx = Context::new();
let output = template.render(&mut ctx);
assert_eq!(output.unwrap(), Some(concat!(
"enter-0;exit-0\n",
"enter-1;exit-1\n",
"enter-2;break-2\n").to_owned()
));
}

#[test]
fn test_nested_break() {
// assert that a {% break %} only breaks out of the innermost loop
let text = concat!(
"{% for outer in (0..3) %}",
"enter-{{outer}}; ",
"{% for inner in (6..10) %}",
"{% if inner == 8 %}break, {% break %}{% endif %}",
"{{ inner }}, ",
"{% endfor %}",
"exit-{{outer}}\n",
"{% endfor %}");
let template = parse(text, LiquidOptions::default()).unwrap();

let mut ctx = Context::new();
let output = template.render(&mut ctx);
assert_eq!(output.unwrap(), Some(concat!(
"enter-0; 6, 7, break, exit-0\n",
"enter-1; 6, 7, break, exit-1\n",
"enter-2; 6, 7, break, exit-2\n").to_owned()
));
}

#[test]
fn test_simple_continue() {
let text = concat!(
"{% for i in (0..5) %}",
"enter-{{i}};",
"{% if i == 2 %}continue-{{i}}\n{% continue %}{% endif %}",
"exit-{{i}}\n",
"{% endfor %}");
let template = parse(text, LiquidOptions::default()).unwrap();

let mut ctx = Context::new();
let output = template.render(&mut ctx);
assert_eq!(output.unwrap(), Some(concat!(
"enter-0;exit-0\n",
"enter-1;exit-1\n",
"enter-2;continue-2\n",
"enter-3;exit-3\n",
"enter-4;exit-4\n").to_owned()
));
}

#[test]
fn test_nested_continue() {
// assert that a {% continue %} only jumps out of the innermost loop
let text = concat!(
"{% for outer in (0..3) %}",
"enter-{{outer}}; ",
"{% for inner in (6..10) %}",
"{% if inner == 8 %}continue, {% continue %}{% endif %}",
"{{ inner }}, ",
"{% endfor %}",
"exit-{{outer}}\n",
"{% endfor %}");
let template = parse(text, LiquidOptions::default()).unwrap();

let mut ctx = Context::new();
let output = template.render(&mut ctx);
assert_eq!(output.unwrap(), Some(concat!(
"enter-0; 6, 7, continue, 9, exit-0\n",
"enter-1; 6, 7, continue, 9, exit-1\n",
"enter-2; 6, 7, continue, 9, exit-2\n").to_owned()
));
}


}
3 changes: 3 additions & 0 deletions src/tags/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod assign_tag;
mod capture_block;
mod if_block;
mod interrupt_tags;
mod for_block;
mod raw_block;
mod comment_block;
Expand All @@ -11,3 +12,5 @@ pub use self::comment_block::comment_block;
pub use self::raw_block::raw_block;
pub use self::for_block::for_block;
pub use self::if_block::if_block;
pub use self::interrupt_tags::break_tag;
pub use self::interrupt_tags::continue_tag;
8 changes: 8 additions & 0 deletions src/template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ impl Renderable for Template {
if let Some(ref x) = try!(el.render(context)) {
buf = buf + x;
}

// Did the last element we processed set an interrupt? If so, we
// need to abandon the rest of our child elements and just
// return what we've got. This is usually in response to a
// `break` or `continue` tag being rendered.
if context.interrupted() {
break;
}
}
Ok(Some(buf))
}
Expand Down
0