diff --git a/liquid-compiler/src/lib.rs b/liquid-compiler/src/lib.rs index 518af3724..bb4014281 100644 --- a/liquid-compiler/src/lib.rs +++ b/liquid-compiler/src/lib.rs @@ -11,6 +11,7 @@ mod include; mod options; mod parser; mod tag; +mod text; pub mod error { pub use liquid_error::*; @@ -25,3 +26,5 @@ pub use include::*; pub use options::*; pub use parser::*; pub use tag::*; + +use text::Text; diff --git a/liquid-compiler/src/parser.rs b/liquid-compiler/src/parser.rs index ef0e5bad5..cc28653c9 100644 --- a/liquid-compiler/src/parser.rs +++ b/liquid-compiler/src/parser.rs @@ -7,7 +7,6 @@ use std; use liquid_interpreter::Expression; use liquid_interpreter::Renderable; -use liquid_interpreter::Text; use liquid_interpreter::Variable; use liquid_interpreter::{FilterCall, FilterChain}; use liquid_value::Scalar; @@ -17,6 +16,7 @@ use super::error::{Error, Result}; use super::LiquidOptions; use super::ParseBlock; use super::ParseTag; +use super::Text; use pest::Parser; diff --git a/liquid-interpreter/src/text.rs b/liquid-compiler/src/text.rs similarity index 75% rename from liquid-interpreter/src/text.rs rename to liquid-compiler/src/text.rs index 5f4b86bc5..501302fe4 100644 --- a/liquid-interpreter/src/text.rs +++ b/liquid-compiler/src/text.rs @@ -1,18 +1,18 @@ use std::io::Write; -use super::Context; -use super::Renderable; use error::{Result, ResultLiquidChainExt}; +use liquid_interpreter::Context; +use liquid_interpreter::Renderable; /// A raw template expression. #[derive(Clone, Debug, Eq, PartialEq)] -pub struct Text { +pub(crate) struct Text { text: String, } impl Text { /// Create a raw template expression. - pub fn new>(text: S) -> Text { + pub(crate) fn new>(text: S) -> Text { Text { text: text.into() } } } diff --git a/liquid-interpreter/Cargo.toml b/liquid-interpreter/Cargo.toml index e915d8ae8..87444424f 100644 --- a/liquid-interpreter/Cargo.toml +++ b/liquid-interpreter/Cargo.toml @@ -16,6 +16,7 @@ appveyor = { repository = "johannhof/liquid-rust" } [dependencies] itertools = "0.7.0" +anymap = "0.12" # Exposed in API liquid-error = { version = "0.16", path = "../liquid-error" } liquid-value = { version = "0.17", path = "../liquid-value" } diff --git a/liquid-interpreter/src/context.rs b/liquid-interpreter/src/context.rs index 8d95a6208..1c1be27f5 100644 --- a/liquid-interpreter/src/context.rs +++ b/liquid-interpreter/src/context.rs @@ -1,27 +1,14 @@ -use std::borrow; -use std::collections::HashMap; use std::sync; -use error::{Error, Result}; +use anymap; use itertools; -use value::{Object, PathRef, Scalar, Value}; +use liquid_error::{Error, Result}; -use super::Expression; -use super::Globals; use super::PluginRegistry; +use super::Stack; +use super::ValueStore; use super::{BoxedValueFilter, FilterValue}; -/// Format an error for an unexpected value. -pub fn unexpected_value_error(expected: &str, actual: Option) -> Error { - let actual = actual.map(|x| x.to_string()); - unexpected_value_error_string(expected, actual) -} - -fn unexpected_value_error_string(expected: &str, actual: Option) -> Error { - let actual = actual.unwrap_or_else(|| "nothing".to_owned()); - Error::with_msg(format!("Expected {}, found `{}`", expected, actual)) -} - /// Block processing interrupt state. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Interrupt { @@ -60,239 +47,9 @@ impl InterruptState { } } -#[derive(Debug, Clone, PartialEq, Eq, Default)] -struct CycleStateInner { - // The indices of all the cycles encountered during rendering. - cycles: HashMap, -} - -impl CycleStateInner { - fn cycle_index(&mut self, name: &str, max: usize) -> usize { - let i = self.cycles.entry(name.to_owned()).or_insert(0); - let j = *i; - *i = (*i + 1) % max; - j - } -} - -/// See `cycle` tag. -pub struct CycleState<'a, 'g> -where - 'g: 'a, -{ - context: &'a mut Context<'g>, -} - -impl<'a, 'g> CycleState<'a, 'g> { - /// See `cycle` tag. - pub fn cycle_element<'c>( - &'c mut self, - name: &str, - values: &'c [Expression], - ) -> Result<&'c Value> { - let index = self.context.cycles.cycle_index(name, values.len()); - if index >= values.len() { - return Err(Error::with_msg( - "cycle index out of bounds, most likely from mismatched cycles", - ) - .context("index", format!("{}", index)) - .context("count", format!("{}", values.len()))); - } - - let val = values[index].evaluate(self.context)?; - Ok(val) - } -} - -/// Remembers the content of the last rendered `ifstate` block. -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct IfChangedState { - last_rendered: Option, -} - -impl IfChangedState { - /// Checks whether or not a new rendered `&str` is different from - /// `last_rendered` and updates `last_rendered` value to the new value. - pub fn has_changed(&mut self, rendered: &str) -> bool { - let has_changed = if let Some(last_rendered) = &self.last_rendered { - last_rendered != rendered - } else { - true - }; - self.last_rendered = Some(rendered.to_owned()); - - has_changed - } -} - -/// Stack of variables. -#[derive(Debug, Clone)] -pub struct Stack<'g> { - globals: Option<&'g Globals>, - stack: Vec, - // State of variables created through increment or decrement tags. - indexes: Object, -} - -impl<'g> Stack<'g> { - /// Create an empty stack - pub fn empty() -> Self { - Self { - globals: None, - indexes: Object::new(), - // Mutable frame for globals. - stack: vec![Object::new()], - } - } - - /// Create a stack initialized with read-only `Globals`. - pub fn with_globals(globals: &'g Globals) -> Self { - let mut stack = Self::empty(); - stack.globals = Some(globals); - stack - } - - /// Creates a new variable scope chained to a parent scope. - fn push_frame(&mut self) { - self.stack.push(Object::new()); - } - - /// Removes the topmost stack frame from the local variable stack. - /// - /// # Panics - /// - /// This method will panic if popping the topmost frame results in an - /// empty stack. Given that a context is created with a top-level stack - /// frame already in place, emptying the stack should never happen in a - /// well-formed program. - fn pop_frame(&mut self) { - if self.stack.pop().is_none() { - panic!("Unbalanced push/pop, leaving the stack empty.") - }; - } - - /// Recursively index into the stack. - pub fn try_get(&self, path: PathRef) -> Option<&Value> { - let frame = self.find_path_frame(path)?; - - frame.try_get_variable(path) - } - - /// Recursively index into the stack. - pub fn get(&self, path: PathRef) -> Result<&Value> { - let frame = self.find_path_frame(path).ok_or_else(|| { - let key = path - .iter() - .next() - .cloned() - .unwrap_or_else(|| Scalar::new("nil")); - let globals = itertools::join(self.globals().iter(), ", "); - Error::with_msg("Unknown variable") - .context("requested variable", key.to_str().into_owned()) - .context("available variables", globals) - })?; - - frame.get_variable(path) - } - - fn globals(&self) -> Vec<&str> { - let mut globals = self.globals.map(|g| g.globals()).unwrap_or_default(); - for frame in self.stack.iter() { - globals.extend(frame.globals()); - } - globals.sort(); - globals.dedup(); - globals - } - - fn find_path_frame<'a>(&'a self, path: PathRef) -> Option<&'a Globals> { - let key = path.iter().next()?; - let key = key.to_str(); - self.find_frame(key.as_ref()) - } - - fn find_frame<'a>(&'a self, name: &str) -> Option<&'a Globals> { - for frame in self.stack.iter().rev() { - if frame.contains_global(name) { - return Some(frame); - } - } - - if self - .globals - .map(|g| g.contains_global(name)) - .unwrap_or(false) - { - return self.globals; - } - - if self.indexes.contains_global(name) { - return Some(&self.indexes); - } - - None - } - - /// Used by increment and decrement tags - pub fn set_index(&mut self, name: S, val: Value) -> Option - where - S: Into>, - { - self.indexes.insert(name.into(), val) - } - - /// Used by increment and decrement tags - pub fn get_index<'a>(&'a self, name: &str) -> Option<&'a Value> { - self.indexes.get(name) - } - - /// Sets a value in the global context. - pub fn set_global(&mut self, name: S, val: Value) -> Option - where - S: Into>, - { - self.global_frame().insert(name.into(), val) - } - - /// Sets a value to the rendering context. - /// Note that it needs to be wrapped in a liquid::Value. - /// - /// # Panics - /// - /// Panics if there is no frame on the local values stack. Context - /// instances are created with a top-level stack frame in place, so - /// this should never happen in a well-formed program. - pub fn set(&mut self, name: S, val: Value) -> Option - where - S: Into>, - { - self.current_frame().insert(name.into(), val) - } - - fn current_frame(&mut self) -> &mut Object { - match self.stack.last_mut() { - Some(frame) => frame, - None => panic!("Global frame removed."), - } - } - - fn global_frame(&mut self) -> &mut Object { - match self.stack.first_mut() { - Some(frame) => frame, - None => panic!("Global frame removed."), - } - } -} - -impl<'g> Default for Stack<'g> { - fn default() -> Self { - Self::empty() - } -} - /// Create processing context for a template. pub struct ContextBuilder<'g> { - globals: Option<&'g Globals>, + globals: Option<&'g ValueStore>, filters: sync::Arc>, } @@ -306,7 +63,7 @@ impl<'g> ContextBuilder<'g> { } /// Initialize the stack with the given globals. - pub fn set_globals(mut self, values: &'g Globals) -> Self { + pub fn set_globals(mut self, values: &'g ValueStore) -> Self { self.globals = Some(values); self } @@ -325,9 +82,8 @@ impl<'g> ContextBuilder<'g> { }; Context { stack, + registers: anymap::AnyMap::new(), interrupt: InterruptState::default(), - cycles: CycleStateInner::default(), - ifchanged: IfChangedState::default(), filters: self.filters, } } @@ -340,13 +96,11 @@ impl<'g> Default for ContextBuilder<'g> { } /// Processing context for a template. -#[derive(Default)] pub struct Context<'g> { stack: Stack<'g>, + registers: anymap::AnyMap, interrupt: InterruptState, - cycles: CycleStateInner, - ifchanged: IfChangedState, filters: sync::Arc>, } @@ -385,17 +139,16 @@ impl<'g> Context<'g> { &mut self.interrupt } - /// See `cycle` tag. - pub fn cycles<'a>(&'a mut self) -> CycleState<'a, 'g> - where - 'g: 'a, - { - CycleState { context: self } - } - - /// Access the block's `IfChangedState`. - pub fn ifchanged(&mut self) -> &mut IfChangedState { - &mut self.ifchanged + /// Data store for stateful tags/blocks. + /// + /// If a plugin needs state, it creates a `struct State : Default` and accesses it via + /// `get_register_mut`. + pub fn get_register_mut + Default>( + &mut self, + ) -> &mut T { + self.registers + .entry::() + .or_insert_with(|| Default::default()) } /// Access the current `Stack`. @@ -425,37 +178,23 @@ impl<'g> Context<'g> { } } +impl<'g> Default for Context<'g> { + fn default() -> Self { + Self { + stack: Stack::empty(), + registers: anymap::AnyMap::new(), + interrupt: InterruptState::default(), + filters: Default::default(), + } + } +} + #[cfg(test)] mod test { use super::*; - use value::Scalar; - - #[test] - fn stack_find_frame() { - let mut ctx = Context::new(); - ctx.stack_mut().set_global("number", Value::scalar(42f64)); - assert!(ctx.stack().find_frame("number").is_some(),); - } - - #[test] - fn stack_find_frame_failure() { - let mut ctx = Context::new(); - let mut post = Object::new(); - post.insert("number".into(), Value::scalar(42f64)); - ctx.stack_mut().set_global("post", Value::Object(post)); - assert!(ctx.stack().find_frame("post.number").is_none()); - } - - #[test] - fn stack_get() { - let mut ctx = Context::new(); - let mut post = Object::new(); - post.insert("number".into(), Value::scalar(42f64)); - ctx.stack_mut().set_global("post", Value::Object(post)); - let indexes = [Scalar::new("post"), Scalar::new("number")]; - assert_eq!(ctx.stack().get(&indexes).unwrap(), &Value::scalar(42f64)); - } + use liquid_value::Scalar; + use liquid_value::Value; #[test] fn scoped_variables() { diff --git a/liquid-interpreter/src/expression.rs b/liquid-interpreter/src/expression.rs index 5cc2ef2b3..99dc6d57e 100644 --- a/liquid-interpreter/src/expression.rs +++ b/liquid-interpreter/src/expression.rs @@ -1,8 +1,8 @@ use std::fmt; -use error::Result; -use value::Scalar; -use value::Value; +use liquid_error::Result; +use liquid_value::Scalar; +use liquid_value::Value; use super::Context; use variable::Variable; diff --git a/liquid-interpreter/src/filter.rs b/liquid-interpreter/src/filter.rs index 0e982c1f5..51aad18bd 100644 --- a/liquid-interpreter/src/filter.rs +++ b/liquid-interpreter/src/filter.rs @@ -1,6 +1,5 @@ use liquid_error; - -use value::Value; +use liquid_value::Value; /// Expected return type of a `Filter`. pub type FilterResult = Result; diff --git a/liquid-interpreter/src/filter_chain.rs b/liquid-interpreter/src/filter_chain.rs index 9cca8abb3..d8a4bb165 100644 --- a/liquid-interpreter/src/filter_chain.rs +++ b/liquid-interpreter/src/filter_chain.rs @@ -3,8 +3,8 @@ use std::io::Write; use itertools; -use error::{Result, ResultLiquidChainExt, ResultLiquidExt}; -use value::Value; +use liquid_error::{Result, ResultLiquidChainExt, ResultLiquidExt}; +use liquid_value::Value; use super::Context; use super::Expression; diff --git a/liquid-interpreter/src/lib.rs b/liquid-interpreter/src/lib.rs index ceb96fcb1..6a7a8c946 100644 --- a/liquid-interpreter/src/lib.rs +++ b/liquid-interpreter/src/lib.rs @@ -3,6 +3,7 @@ #![warn(missing_docs)] #![warn(unused_extern_crates)] +extern crate anymap; extern crate itertools; extern crate liquid_error; extern crate liquid_value; @@ -10,33 +11,24 @@ extern crate liquid_value; #[cfg(test)] extern crate serde_yaml; -/// Liquid Processing Errors. -pub mod error { - pub use liquid_error::*; -} -/// Liquid value type. -pub mod value { - pub use liquid_value::*; -} - mod context; mod expression; mod filter; mod filter_chain; -mod globals; mod registry; mod renderable; +mod stack; +mod store; mod template; -mod text; mod variable; pub use self::context::*; pub use self::expression::*; pub use self::filter::*; pub use self::filter_chain::*; -pub use self::globals::*; pub use self::registry::*; pub use self::renderable::*; +pub use self::stack::*; +pub use self::store::*; pub use self::template::*; -pub use self::text::*; pub use self::variable::*; diff --git a/liquid-interpreter/src/renderable.rs b/liquid-interpreter/src/renderable.rs index 736e7d6ff..d5dbe4667 100644 --- a/liquid-interpreter/src/renderable.rs +++ b/liquid-interpreter/src/renderable.rs @@ -1,8 +1,9 @@ use std::fmt::Debug; use std::io::Write; +use liquid_error::Result; + use super::Context; -use error::Result; /// Any object (tag/block) that can be rendered by liquid must implement this trait. pub trait Renderable: Send + Sync + Debug { diff --git a/liquid-interpreter/src/stack.rs b/liquid-interpreter/src/stack.rs new file mode 100644 index 000000000..5c96fbe0f --- /dev/null +++ b/liquid-interpreter/src/stack.rs @@ -0,0 +1,200 @@ +use std::borrow; + +use itertools; +use liquid_error::{Error, Result}; +use liquid_value::{Object, PathRef, Scalar, Value}; + +use super::ValueStore; + +/// Stack of variables. +#[derive(Debug, Clone)] +pub struct Stack<'g> { + globals: Option<&'g ValueStore>, + stack: Vec, + // State of variables created through increment or decrement tags. + indexes: Object, +} + +impl<'g> Stack<'g> { + /// Create an empty stack + pub fn empty() -> Self { + Self { + globals: None, + indexes: Object::new(), + // Mutable frame for globals. + stack: vec![Object::new()], + } + } + + /// Create a stack initialized with read-only `ValueStore`. + pub fn with_globals(globals: &'g ValueStore) -> Self { + let mut stack = Self::empty(); + stack.globals = Some(globals); + stack + } + + /// Creates a new variable scope chained to a parent scope. + pub(crate) fn push_frame(&mut self) { + self.stack.push(Object::new()); + } + + /// Removes the topmost stack frame from the local variable stack. + /// + /// # Panics + /// + /// This method will panic if popping the topmost frame results in an + /// empty stack. Given that a context is created with a top-level stack + /// frame already in place, emptying the stack should never happen in a + /// well-formed program. + pub(crate) fn pop_frame(&mut self) { + if self.stack.pop().is_none() { + panic!("Unbalanced push/pop, leaving the stack empty.") + }; + } + + /// Recursively index into the stack. + pub fn try_get(&self, path: PathRef) -> Option<&Value> { + let frame = self.find_path_frame(path)?; + + frame.try_get_variable(path) + } + + /// Recursively index into the stack. + pub fn get(&self, path: PathRef) -> Result<&Value> { + let frame = self.find_path_frame(path).ok_or_else(|| { + let key = path + .iter() + .next() + .cloned() + .unwrap_or_else(|| Scalar::new("nil")); + let globals = itertools::join(self.globals().iter(), ", "); + Error::with_msg("Unknown variable") + .context("requested variable", key.to_str().into_owned()) + .context("available variables", globals) + })?; + + frame.get_variable(path) + } + + fn globals(&self) -> Vec<&str> { + let mut globals = self.globals.map(|g| g.roots()).unwrap_or_default(); + for frame in self.stack.iter() { + globals.extend(frame.roots()); + } + globals.sort(); + globals.dedup(); + globals + } + + fn find_path_frame<'a>(&'a self, path: PathRef) -> Option<&'a ValueStore> { + let key = path.iter().next()?; + let key = key.to_str(); + self.find_frame(key.as_ref()) + } + + fn find_frame<'a>(&'a self, name: &str) -> Option<&'a ValueStore> { + for frame in self.stack.iter().rev() { + if frame.contains_root(name) { + return Some(frame); + } + } + + if self.globals.map(|g| g.contains_root(name)).unwrap_or(false) { + return self.globals; + } + + if self.indexes.contains_root(name) { + return Some(&self.indexes); + } + + None + } + + /// Used by increment and decrement tags + pub fn set_index(&mut self, name: S, val: Value) -> Option + where + S: Into>, + { + self.indexes.insert(name.into(), val) + } + + /// Used by increment and decrement tags + pub fn get_index<'a>(&'a self, name: &str) -> Option<&'a Value> { + self.indexes.get(name) + } + + /// Sets a value in the global context. + pub fn set_global(&mut self, name: S, val: Value) -> Option + where + S: Into>, + { + self.global_frame().insert(name.into(), val) + } + + /// Sets a value to the rendering context. + /// Note that it needs to be wrapped in a liquid::Value. + /// + /// # Panics + /// + /// Panics if there is no frame on the local values stack. Context + /// instances are created with a top-level stack frame in place, so + /// this should never happen in a well-formed program. + pub fn set(&mut self, name: S, val: Value) -> Option + where + S: Into>, + { + self.current_frame().insert(name.into(), val) + } + + fn current_frame(&mut self) -> &mut Object { + match self.stack.last_mut() { + Some(frame) => frame, + None => panic!("Global frame removed."), + } + } + + fn global_frame(&mut self) -> &mut Object { + match self.stack.first_mut() { + Some(frame) => frame, + None => panic!("Global frame removed."), + } + } +} + +impl<'g> Default for Stack<'g> { + fn default() -> Self { + Self::empty() + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn stack_find_frame() { + let mut stack = Stack::empty(); + stack.set_global("number", Value::scalar(42f64)); + assert!(stack.find_frame("number").is_some(),); + } + + #[test] + fn stack_find_frame_failure() { + let mut stack = Stack::empty(); + let mut post = Object::new(); + post.insert("number".into(), Value::scalar(42f64)); + stack.set_global("post", Value::Object(post)); + assert!(stack.find_frame("post.number").is_none()); + } + + #[test] + fn stack_get() { + let mut stack = Stack::empty(); + let mut post = Object::new(); + post.insert("number".into(), Value::scalar(42f64)); + stack.set_global("post", Value::Object(post)); + let indexes = [Scalar::new("post"), Scalar::new("number")]; + assert_eq!(stack.get(&indexes).unwrap(), &Value::scalar(42f64)); + } + +} diff --git a/liquid-interpreter/src/globals.rs b/liquid-interpreter/src/store.rs similarity index 87% rename from liquid-interpreter/src/globals.rs rename to liquid-interpreter/src/store.rs index 12457ca22..d1e819356 100644 --- a/liquid-interpreter/src/globals.rs +++ b/liquid-interpreter/src/store.rs @@ -1,18 +1,18 @@ use std::fmt; -use error::{Error, Result}; use itertools; -use value::Object; -use value::PathRef; -use value::Value; +use liquid_error::{Error, Result}; +use liquid_value::Object; +use liquid_value::PathRef; +use liquid_value::Value; /// Immutable view into a template's global variables. -pub trait Globals: fmt::Debug { - /// Check if global variable exists. - fn contains_global(&self, name: &str) -> bool; +pub trait ValueStore: fmt::Debug { + /// Check if root variable exists. + fn contains_root(&self, name: &str) -> bool; - /// Enumerate all globals - fn globals(&self) -> Vec<&str>; + /// Enumerate all root variables. + fn roots(&self) -> Vec<&str>; /// Check if variable exists. /// @@ -36,12 +36,12 @@ pub trait Globals: fmt::Debug { fn get_variable<'a>(&'a self, path: PathRef) -> Result<&'a Value>; } -impl Globals for Object { - fn contains_global(&self, name: &str) -> bool { +impl ValueStore for Object { + fn contains_root(&self, name: &str) -> bool { self.contains_key(name) } - fn globals(&self) -> Vec<&str> { + fn roots(&self) -> Vec<&str> { self.keys().map(|s| s.as_ref()).collect() } diff --git a/liquid-interpreter/src/template.rs b/liquid-interpreter/src/template.rs index 1115c79c9..135881bf0 100644 --- a/liquid-interpreter/src/template.rs +++ b/liquid-interpreter/src/template.rs @@ -1,8 +1,9 @@ use std::io::Write; +use liquid_error::Result; + use super::Context; use super::Renderable; -use error::Result; /// An executable template block. #[derive(Debug)] diff --git a/liquid-interpreter/src/variable.rs b/liquid-interpreter/src/variable.rs index 64c21a473..3bf897aaf 100644 --- a/liquid-interpreter/src/variable.rs +++ b/liquid-interpreter/src/variable.rs @@ -1,13 +1,11 @@ use std::fmt; -use std::io::Write; -use error::{Error, Result, ResultLiquidChainExt}; -use value::Path; -use value::Scalar; +use liquid_error::{Error, Result}; +use liquid_value::Path; +use liquid_value::Scalar; use super::Context; use super::Expression; -use super::Renderable; /// A `Value` reference. #[derive(Clone, Debug, PartialEq)] @@ -83,22 +81,14 @@ impl fmt::Display for Variable { } } -impl Renderable for Variable { - fn render_to(&self, writer: &mut Write, context: &mut Context) -> Result<()> { - let path = self.evaluate(context)?; - let value = context.stack().get(&path)?; - write!(writer, "{}", value).chain("Failed to render")?; - Ok(()) - } -} - #[cfg(test)] mod test { + use super::*; + + use liquid_value::Object; use serde_yaml; use super::super::ContextBuilder; - use super::*; - use value::Object; #[test] fn identifier_path_array_index() { @@ -108,13 +98,14 @@ test_a: ["test"] "#, ) .unwrap(); - let mut actual = Variable::with_literal("test_a"); + let mut var = Variable::with_literal("test_a"); let index = vec![Scalar::new(0)]; - actual.extend(index); + var.extend(index); - let mut context = ContextBuilder::new().set_globals(&globals).build(); - let actual = actual.render(&mut context).unwrap(); - assert_eq!(actual, "test".to_owned()); + let context = ContextBuilder::new().set_globals(&globals).build(); + let actual = var.evaluate(&context).unwrap(); + let actual = context.stack().get(&actual).unwrap(); + assert_eq!(actual.to_string(), "test"); } #[test] @@ -125,13 +116,14 @@ test_a: ["test1", "test2"] "#, ) .unwrap(); - let mut actual = Variable::with_literal("test_a"); + let mut var = Variable::with_literal("test_a"); let index = vec![Scalar::new(-1)]; - actual.extend(index); + var.extend(index); - let mut context = ContextBuilder::new().set_globals(&globals).build(); - let actual = actual.render(&mut context).unwrap(); - assert_eq!(actual, "test2".to_owned()); + let context = ContextBuilder::new().set_globals(&globals).build(); + let actual = var.evaluate(&context).unwrap(); + let actual = context.stack().get(&actual).unwrap(); + assert_eq!(actual.to_string(), "test2"); } #[test] @@ -143,12 +135,13 @@ test_a: "#, ) .unwrap(); - let mut actual = Variable::with_literal("test_a"); + let mut var = Variable::with_literal("test_a"); let index = vec![Scalar::new(0), Scalar::new("test_h")]; - actual.extend(index); + var.extend(index); - let mut context = ContextBuilder::new().set_globals(&globals).build(); - let actual = actual.render(&mut context).unwrap(); - assert_eq!(actual, "5".to_owned()); + let context = ContextBuilder::new().set_globals(&globals).build(); + let actual = var.evaluate(&context).unwrap(); + let actual = context.stack().get(&actual).unwrap(); + assert_eq!(actual.to_string(), "5"); } } diff --git a/src/lib.rs b/src/lib.rs index b339810f9..f8360d5f0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,7 +54,7 @@ pub mod value { pub mod filters; pub mod tags; -pub use interpreter::Globals; +pub use interpreter::ValueStore; pub use liquid_error::Error; pub use parser::*; pub use template::*; diff --git a/src/tags/cycle_tag.rs b/src/tags/cycle_tag.rs index 32ba2d457..628e4d5f4 100644 --- a/src/tags/cycle_tag.rs +++ b/src/tags/cycle_tag.rs @@ -1,7 +1,8 @@ +use std::collections::HashMap; use std::io::Write; use itertools; -use liquid_error::{Result, ResultLiquidChainExt, ResultLiquidExt}; +use liquid_error::{Error, Result, ResultLiquidChainExt, ResultLiquidExt}; use compiler::LiquidOptions; use compiler::TagToken; @@ -28,10 +29,11 @@ impl Cycle { impl Renderable for Cycle { fn render_to(&self, writer: &mut Write, context: &mut Context) -> Result<()> { - let mut cycles = context.cycles(); - let value = cycles - .cycle_element(&self.name, &self.values) + let expr = context + .get_register_mut::() + .cycle(&self.name, &self.values) .trace_with(|| self.trace().into())?; + let value = expr.evaluate(context).trace_with(|| self.trace().into())?; write!(writer, "{}", value).chain("Failed to render")?; Ok(()) } @@ -100,6 +102,34 @@ pub fn cycle_tag( parse_cycle(arguments, options).map(|opt| Box::new(opt) as Box) } +#[derive(Debug, Clone, PartialEq, Eq, Default)] +struct State { + // The indices of all the cycles encountered during rendering. + cycles: HashMap, +} + +impl State { + fn cycle<'e>(&mut self, name: &str, values: &'e [Expression]) -> Result<&'e Expression> { + let index = self.cycle_index(name, values.len()); + if index >= values.len() { + return Err(Error::with_msg( + "cycle index out of bounds, most likely from mismatched cycles", + ) + .context("index", format!("{}", index)) + .context("count", format!("{}", values.len()))); + } + + Ok(&values[index]) + } + + fn cycle_index(&mut self, name: &str, max: usize) -> usize { + let i = self.cycles.entry(name.to_owned()).or_insert(0); + let j = *i; + *i = (*i + 1) % max; + j + } +} + #[cfg(test)] mod test { use super::*; diff --git a/src/tags/for_block.rs b/src/tags/for_block.rs index bf72a6f1a..97d7449b2 100644 --- a/src/tags/for_block.rs +++ b/src/tags/for_block.rs @@ -2,7 +2,7 @@ use std::fmt; use std::io::Write; use itertools; -use liquid_error::{Result, ResultLiquidChainExt, ResultLiquidExt}; +use liquid_error::{Error, Result, ResultLiquidChainExt, ResultLiquidExt}; use liquid_value::{Object, Scalar, Value}; use compiler::BlockElement; @@ -13,7 +13,7 @@ use compiler::TryMatchToken; use interpreter::Expression; use interpreter::Renderable; use interpreter::Template; -use interpreter::{unexpected_value_error, Context, Interrupt}; +use interpreter::{Context, Interrupt}; #[derive(Clone, Debug)] enum Range { @@ -483,6 +483,17 @@ pub fn tablerow_block( })) } +/// Format an error for an unexpected value. +pub fn unexpected_value_error(expected: &str, actual: Option) -> Error { + let actual = actual.map(|x| x.to_string()); + unexpected_value_error_string(expected, actual) +} + +fn unexpected_value_error_string(expected: &str, actual: Option) -> Error { + let actual = actual.unwrap_or_else(|| "nothing".to_owned()); + Error::with_msg(format!("Expected {}, found `{}`", expected, actual)) +} + #[cfg(test)] mod test { use std::sync; diff --git a/src/tags/if_block.rs b/src/tags/if_block.rs index 8af677d41..4674d1edc 100644 --- a/src/tags/if_block.rs +++ b/src/tags/if_block.rs @@ -1,7 +1,7 @@ use std::fmt; use std::io::Write; -use liquid_error::{Result, ResultLiquidExt}; +use liquid_error::{Error, Result, ResultLiquidExt}; use liquid_value::Value; use compiler::BlockElement; @@ -10,9 +10,9 @@ use compiler::TagBlock; use compiler::TagToken; use compiler::TagTokenIter; use interpreter::Context; +use interpreter::Expression; use interpreter::Renderable; use interpreter::Template; -use interpreter::{unexpected_value_error, Expression}; #[derive(Clone, Debug)] enum ComparisonOperator { @@ -386,6 +386,17 @@ pub fn if_block( Ok(conditional) } +/// Format an error for an unexpected value. +pub fn unexpected_value_error(expected: &str, actual: Option) -> Error { + let actual = actual.map(|x| x.to_string()); + unexpected_value_error_string(expected, actual) +} + +fn unexpected_value_error_string(expected: &str, actual: Option) -> Error { + let actual = actual.unwrap_or_else(|| "nothing".to_owned()); + Error::with_msg(format!("Expected {}, found `{}`", expected, actual)) +} + #[cfg(test)] mod test { use super::*; diff --git a/src/tags/ifchanged_block.rs b/src/tags/ifchanged_block.rs index 85e769dbd..344243670 100644 --- a/src/tags/ifchanged_block.rs +++ b/src/tags/ifchanged_block.rs @@ -28,7 +28,7 @@ impl Renderable for IfChanged { .trace_with(|| self.trace().into())?; let rendered = String::from_utf8(rendered).expect("render only writes UTF-8"); - if context.ifchanged().has_changed(&rendered) { + if context.get_register_mut::().has_changed(&rendered) { write!(writer, "{}", rendered).chain("Failed to render")?; } @@ -51,6 +51,27 @@ pub fn ifchanged_block( Ok(Box::new(IfChanged { if_changed })) } +/// Remembers the content of the last rendered `ifstate` block. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +struct State { + last_rendered: Option, +} + +impl State { + /// Checks whether or not a new rendered `&str` is different from + /// `last_rendered` and updates `last_rendered` value to the new value. + fn has_changed(&mut self, rendered: &str) -> bool { + let has_changed = if let Some(last_rendered) = &self.last_rendered { + last_rendered != rendered + } else { + true + }; + self.last_rendered = Some(rendered.to_owned()); + + has_changed + } +} + #[cfg(test)] mod test { use super::*; diff --git a/src/template.rs b/src/template.rs index 8cb1e3612..a71e8a647 100644 --- a/src/template.rs +++ b/src/template.rs @@ -12,7 +12,7 @@ pub struct Template { impl Template { /// Renders an instance of the Template, using the given globals. - pub fn render(&self, globals: &interpreter::Globals) -> Result { + pub fn render(&self, globals: &interpreter::ValueStore) -> Result { const BEST_GUESS: usize = 10_000; let mut data = Vec::with_capacity(BEST_GUESS); self.render_to(&mut data, globals)?; @@ -21,7 +21,7 @@ impl Template { } /// Renders an instance of the Template, using the given globals. - pub fn render_to(&self, writer: &mut Write, globals: &interpreter::Globals) -> Result<()> { + pub fn render_to(&self, writer: &mut Write, globals: &interpreter::ValueStore) -> Result<()> { let mut data = interpreter::ContextBuilder::new() .set_filters(&self.filters) .set_globals(globals)