Repository: cobalt-org/liquid-rust
Pull request #93 (2017-05-07 04:24:47)

fix(filters): Align with shopify/liquid's filters
Either corrects filters to align with shopify/liquid or notes the differences.


Patch:
diff --git a/.travis.yml b/.travis.yml
index 589addd..068710c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,7 +7,7 @@ rust:
 
 install:
   # Install rustfmt
-  - curl -LSfs https://japaric.github.io/trust/install.sh | sh -s -- --force --crate rustfmt --git japaric/rustfmt-bin --tag v0.6.3-20161120
+  - cargo install rustfmt --force
   # Install clippy
   - if [ "$TRAVIS_RUST_VERSION" = "nightly" ]; then (cargo install clippy --force) fi
   - export PATH=$HOME/.cargo/bin:$PATH
@@ -32,9 +32,6 @@ addons:
 
 cache:
   apt: true
-  directories:
-    - $HOME/.cargo
-    - target/debug/build
 
 before_cache:
     # Travis can't cache files that are not readable by "others"
diff --git a/appveyor.yml b/appveyor.yml
index 886fc5d..975648e 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -32,8 +32,5 @@ test_script:
   - cargo check --verbose --features "cli serde_yaml serde_json"
   - cargo test --features "cli serde_yaml serde_json"
 
-cache:
-  - C:\Users\appveyor\.cargo\registry
-
 # disable automatic builds
 build: false
diff --git a/src/error.rs b/src/error.rs
index 00878d3..eb92e08 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -24,7 +24,8 @@ impl Error {
     pub fn parser<T>(expected: &str, actual: Option<&Token>) -> Result<T> {
         Err(Error::Parser(format!("Expected {}, found {}",
                                   expected,
-                                  actual.map(|x| x.to_string())
+                                  actual
+                                      .map(|x| x.to_string())
                                       .unwrap_or_else(|| "nothing".to_owned()))))
     }
 
diff --git a/src/filters.rs b/src/filters.rs
index 48de486..028b245 100644
--- a/src/filters.rs
+++ b/src/filters.rs
@@ -1,6 +1,6 @@
 use std::fmt;
 use std::error::Error;
-use std::cmp::Ordering;
+use std::cmp;
 
 use value::Value;
 use value::Value::{Array, Bool, Num, Object, Str};
@@ -80,7 +80,9 @@ fn nr_escaped(text: &str) -> usize {
 fn _escape(input: &Value, args: &[Value], once_p: bool) -> FilterResult {
     try!(check_args_len(args, 0));
 
-    let s = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?;
+    let s = input
+        .as_str()
+        .ok_or_else(|| InvalidType("String expected".to_owned()))?;
     let mut result = String::new();
     let mut last = 0;
     let mut skip = 0;
@@ -117,41 +119,35 @@ fn _escape(input: &Value, args: &[Value], once_p: bool) -> FilterResult {
     Ok(Str(result))
 }
 
-// Actual filters.
+// standardfilters.rb
 
-/// Returns the absolute value of a number.
-pub fn abs(input: &Value, args: &[Value]) -> FilterResult {
-    try!(check_args_len(args, 0));
+pub fn size(input: &Value, _args: &[Value]) -> FilterResult {
     match *input {
-        Str(ref s) => {
-            match s.parse::<f32>() {
-                Ok(n) => Ok(Num(n.abs())),
-                Err(e) => {
-                    Err(InvalidType(format!("Non-numeric-string, parse error ``{}'' occurred",
-                                            e.to_string())))
-                }
-            }
-        }
-        Num(n) => Ok(Num(n.abs())),
-        _ => Err(InvalidType("String or number expected".to_owned())),
+        Str(ref x) => Ok(Num(x.len() as f32)),
+        Array(ref x) => Ok(Num(x.len() as f32)),
+        Object(ref x) => Ok(Num(x.len() as f32)),
+        _ => Ok(Num(0f32)),
     }
 }
 
-pub fn append(input: &Value, args: &[Value]) -> FilterResult {
-    try!(check_args_len(args, 1));
+pub fn downcase(input: &Value, args: &[Value]) -> FilterResult {
+    try!(check_args_len(args, 0));
 
-    let x = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?;
+    let s = input.to_string();
+    Ok(Str(s.to_lowercase()))
+}
 
-    let a = args[0].as_str()
-        .ok_or_else(|| InvalidArgument(0, "String expected".to_owned()))?;
+pub fn upcase(input: &Value, args: &[Value]) -> FilterResult {
+    try!(check_args_len(args, 0));
 
-    Ok(Str(format!("{}{}", x, a)))
+    let s = input.to_string();
+    Ok(Str(s.to_uppercase()))
 }
 
 pub fn capitalize(input: &Value, args: &[Value]) -> FilterResult {
     try!(check_args_len(args, 0));
 
-    let s = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?;
+    let s = input.to_string();
     let mut chars = s.chars();
     let capitalized = match chars.next() {
         Some(first_char) => first_char.to_uppercase().chain(chars).collect(),
@@ -161,106 +157,192 @@ pub fn capitalize(input: &Value, args: &[Value]) -> FilterResult {
     Ok(Str(capitalized))
 }
 
-pub fn ceil(input: &Value, args: &[Value]) -> FilterResult {
-    try!(check_args_len(args, 0));
+pub fn escape(input: &Value, args: &[Value]) -> FilterResult {
+    _escape(input, args, false)
+}
 
-    let n = input.as_float().ok_or_else(|| InvalidType("Number expected".to_owned()))?;
-    Ok(Num(n.ceil()))
+pub fn escape_once(input: &Value, args: &[Value]) -> FilterResult {
+    _escape(input, args, true)
 }
 
-pub fn date(input: &Value, args: &[Value]) -> FilterResult {
-    try!(check_args_len(args, 1));
+// TODO url_encode
 
-    let s = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?;
-    let date = DateTime::parse_from_str(s, "%d %B %Y %H:%M:%S %z")
-                .map_err(|e| FilterError::InvalidType(format!("Invalid date format: {}", e)))?;
+// TODO url_decode
 
-    let format = args[0].as_str().ok_or_else(|| InvalidArgument(0, "String expected".to_owned()))?;
+fn canonicalize_slice(slice_offset: isize,
+                      slice_length: isize,
+                      vec_length: usize)
+                      -> (usize, usize) {
+    let vec_length = vec_length as isize;
 
-    Ok(Value::Str(date.format(format).to_string()))
-}
+    // Cap slice_offset
+    let slice_offset = cmp::min(slice_offset, vec_length);
+    // Reverse indexing
+    let slice_offset = if slice_offset < 0 {
+        slice_offset + vec_length
+    } else {
+        slice_offset
+    };
 
-#[cfg(feature = "extra-filters")]
-pub fn date_in_tz(input: &Value, args: &[Value]) -> FilterResult {
-    try!(check_args_len(args, 2));
+    // Cap slice_length
+    let slice_length = if slice_offset + slice_length > vec_length {
+        vec_length - slice_offset
+    } else {
+        slice_length
+    };
 
-    let s = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?;
-    let date = DateTime::parse_from_str(s, "%d %B %Y %H:%M:%S %z")
-                .map_err(|e| FilterError::InvalidType(format!("Invalid date format: {}", e)))?;
+    (slice_offset as usize, slice_length as usize)
+}
+
+pub fn slice(input: &Value, args: &[Value]) -> FilterResult {
+    if args.len() < 1 || args.len() > 2 {
+        return Err(InvalidArgumentCount(format!("expected one or two arguments, {} given",
+                                                args.len())));
+    }
 
-    let format = args[0].as_str().ok_or_else(|| InvalidArgument(0, "String expected".to_owned()))?;
+    let offset = args[0]
+        .as_float()
+        .ok_or_else(|| InvalidArgument(0, "Number expected".to_owned()))?;
+    let offset = offset as isize;
 
-    let n = args[1].as_float().ok_or_else(|| InvalidArgument(1, "Number expected".to_owned()))?;
-    let timezone = FixedOffset::east((n * 3600.0) as i32);
+    let length = match args.get(1) {
+        Some(&Num(x)) if x > 0f32 => x as isize,
+        Some(_) => return Err(InvalidArgument(1, "Positive number expected".to_owned())),
+        None => 1,
+    };
 
-    Ok(Value::Str(date.with_timezone(&timezone).format(format).to_string()))
+    if let Value::Array(ref input) = *input {
+        let (offset, length) = canonicalize_slice(offset, length, input.len());
+        Ok(Value::Array(input
+                            .iter()
+                            .skip(offset as usize)
+                            .take(length as usize)
+                            .cloned()
+                            .collect()))
+    } else {
+        let input = input.to_string();
+        let (offset, length) = canonicalize_slice(offset, length, input.len());
+        Ok(Value::Str(input
+                          .chars()
+                          .skip(offset as usize)
+                          .take(length as usize)
+                          .collect()))
+    }
 }
 
-pub fn divided_by(input: &Value, args: &[Value]) -> FilterResult {
-    try!(check_args_len(args, 1));
+/// `truncate` shortens a string down to the number of characters passed as a parameter.
+///
+/// Note that this function operates on [grapheme
+/// clusters](http://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries) (or *user-perceived
+/// character*), rather than Unicode code points.  Each grapheme cluster may be composed of more
+/// than one Unicode code point, and does not necessarily correspond to rust's conception of a
+/// character.
+///
+/// If the number of characters specified is less than the length of the string, an ellipsis
+/// (`...`) is appended to the string and is included in the character count.
+///
+/// ## Custom ellipsis
+///
+/// `truncate` takes an optional second parameter that specifies the sequence of characters to be
+/// appended to the truncated string. By default this is an ellipsis (`...`), but you can specify a
+/// different sequence.
+///
+/// The length of the second parameter counts against the number of characters specified by the
+/// first parameter. For example, if you want to truncate a string to exactly 10 characters, and
+/// use a 3-character ellipsis, use 13 for the first parameter of `truncate`, since the ellipsis
+/// counts as 3 characters.
+///
+/// ## No ellipsis
+///
+/// You can truncate to the exact number of characters specified by the first parameter and show no
+/// trailing characters by passing a blank string as the second parameter.
+pub fn truncate(input: &Value, args: &[Value]) -> FilterResult {
+    if 2 < args.len() {
+        return Err(InvalidArgumentCount(format!("expected one or two arguments, {} given",
+                                                args.len())));
+    }
 
-    let n = input.as_float().ok_or_else(|| InvalidType("Number expected".to_owned()))?;
+    let length = args.get(0)
+        .unwrap_or(&Value::Num(50f32))
+        .as_float()
+        .ok_or_else(|| InvalidArgument(0, "Number expected".to_owned()))?;
+    let length = length as usize;
 
-    let x = args[0].as_float().ok_or_else(|| InvalidArgument(0, "Number expected".to_owned()))?;
+    let truncate_string = args.get(1)
+        .map(|v| v.to_string())
+        .unwrap_or_else(|| "...".to_owned());
 
-    Ok(Num((n / x).floor()))
-}
+    let l = cmp::max(length - truncate_string.len(), 0);
 
-pub fn downcase(input: &Value, args: &[Value]) -> FilterResult {
-    try!(check_args_len(args, 0));
+    let input = input.to_string();
 
-    let s = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?;
-    Ok(Str(s.to_lowercase()))
+    let result = if length < input.len() {
+        UnicodeSegmentation::graphemes(input.as_str(), true)
+            .take(l)
+            .collect::<Vec<&str>>()
+            .join("")
+            .to_string() + truncate_string.as_str()
+    } else {
+        input
+    };
+    Ok(Str(result))
 }
 
-pub fn escape(input: &Value, args: &[Value]) -> FilterResult {
-    _escape(input, args, false)
-}
+pub fn truncatewords(input: &Value, args: &[Value]) -> FilterResult {
+    if 2 < args.len() {
+        return Err(InvalidArgumentCount(format!("expected one or two arguments, {} given",
+                                                args.len())));
+    }
 
-pub fn escape_once(input: &Value, args: &[Value]) -> FilterResult {
-    _escape(input, args, true)
-}
+    let words = args.get(0)
+        .unwrap_or(&Value::Num(15f32))
+        .as_float()
+        .ok_or_else(|| InvalidArgument(0, "Number expected".to_owned()))?;
+    let words = words as usize;
 
-pub fn first(input: &Value, _args: &[Value]) -> FilterResult {
-    match *input {
-        Str(ref x) => {
-            let c = x.chars().next().map(|c| c.to_string()).unwrap_or_else(|| "".to_owned());
-            Ok(Str(c))
-        }
-        Array(ref x) => Ok(x.first().unwrap_or(&Str("".to_owned())).to_owned()),
-        _ => Err(InvalidType("String or Array expected".to_owned())),
-    }
-}
+    let truncate_string = args.get(1)
+        .map(|v| v.to_string())
+        .unwrap_or_else(|| "...".to_owned());
 
-pub fn floor(input: &Value, args: &[Value]) -> FilterResult {
-    try!(check_args_len(args, 0));
+    let l = cmp::max(words, 0);
 
-    let n = input.as_float().ok_or_else(|| InvalidType("Number expected".to_owned()))?;
-    Ok(Num(n.floor()))
+    let input_string = input.to_string();
+    let word_list: Vec<&str> = input_string.split(' ').collect();
+    let result = if words < word_list.len() {
+        let result = itertools::join(word_list.iter().take(l), " ") + truncate_string.as_str();
+        Str(result)
+    } else {
+        input.clone()
+    };
+    Ok(result)
 }
 
-pub fn join(input: &Value, args: &[Value]) -> FilterResult {
+pub fn split(input: &Value, args: &[Value]) -> FilterResult {
     try!(check_args_len(args, 1));
 
-    let array = input.as_array()
-        .ok_or_else(|| InvalidType("Array of strings expected".to_owned()))?;
-    // use ToStr to stringify the values in case they aren't strings...
-    let strings_to_join = array.iter().map(|x| x.to_string());
+    let input = input.to_string();
 
-    let join_string = args[0].as_str()
-        .ok_or_else(|| InvalidArgument(0, "String expected".to_owned()))?;
-    Ok(Str(itertools::join(strings_to_join, join_string)))
+    let pattern = args[0].to_string();
+
+    // Split and construct resulting Array
+    Ok(Array(input
+                 .split(pattern.as_str())
+                 .map(|x| Str(x.to_owned()))
+                 .collect()))
 }
 
-pub fn last(input: &Value, _args: &[Value]) -> FilterResult {
-    match *input {
-        Str(ref x) => {
-            let c = x.chars().last().map(|c| c.to_string()).unwrap_or_else(|| "".to_owned());
-            Ok(Str(c))
-        }
-        Array(ref x) => Ok(x.last().unwrap_or(&Str("".to_owned())).to_owned()),
-        _ => Err(InvalidType("String or Array expected".to_owned())),
-    }
+/// Removes all whitespace (tabs, spaces, and newlines) from both the left and right side of a
+/// string.
+///
+/// It does not affect spaces between words.  Note that while this works for the case of tabs,
+/// spaces, and newlines, it also removes any other codepoints defined by the Unicode Derived Core
+/// Property `White_Space` (per [rust
+/// documentation](https://doc.rust-lang.org/std/primitive.str.html#method.trim_left).
+pub fn strip(input: &Value, args: &[Value]) -> FilterResult {
+    try!(check_args_len(args, 0));
+
+    let input = input.to_string();
+    Ok(Str(input.trim().to_string()))
 }
 
 /// Removes all whitespaces (tabs, spaces, and newlines) from the beginning of a string.
@@ -272,368 +354,397 @@ pub fn last(input: &Value, _args: &[Value]) -> FilterResult {
 pub fn lstrip(input: &Value, args: &[Value]) -> FilterResult {
     try!(check_args_len(args, 0));
 
-    let s = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?;
-    Ok(Str(s.trim_left().to_string()))
+    let input = input.to_string();
+    Ok(Str(input.trim_left().to_string()))
 }
 
-pub fn minus(input: &Value, args: &[Value]) -> FilterResult {
-    try!(check_args_len(args, 1));
+/// Removes all whitespace (tabs, spaces, and newlines) from the right side of a string.
+///
+/// The filter does not affect spaces between words.  Note that while this works for the case of
+/// tabs, spaces, and newlines, it also removes any other codepoints defined by the Unicode Derived
+/// Core Property `White_Space` (per [rust
+/// documentation](https://doc.rust-lang.org/std/primitive.str.html#method.trim_left).
+pub fn rstrip(input: &Value, args: &[Value]) -> FilterResult {
+    try!(check_args_len(args, 0));
 
-    let n = input.as_float().ok_or_else(|| InvalidType("Number expected".to_owned()))?;
+    let input = input.to_string();
+    Ok(Str(input.trim_right().to_string()))
+}
 
-    let x = args[0].as_float().ok_or_else(|| InvalidArgument(0, "Number expected".to_owned()))?;
+pub fn strip_html(input: &Value, args: &[Value]) -> FilterResult {
+    lazy_static! {
+        // regexps taken from https://git.io/vXbgS
+        static ref MATCHERS: [Regex; 4] = [Regex::new(r"(?is)<script.*?</script>").unwrap(),
+                                           Regex::new(r"(?is)<style.*?</style>").unwrap(),
+                                           Regex::new(r"(?is)<!--.*?-->").unwrap(),
+                                           Regex::new(r"(?is)<.*?>").unwrap()];
+    }
+    try!(check_args_len(args, 0));
 
-    Ok(Num(n - x))
+    let input = input.to_string();
+
+    let result = MATCHERS
+        .iter()
+        .fold(input,
+              |acc, &ref matcher| matcher.replace_all(&acc, "").into_owned());
+    Ok(Str(result))
 }
 
-pub fn modulo(input: &Value, args: &[Value]) -> FilterResult {
-    try!(check_args_len(args, 1));
+/// Removes any newline characters (line breaks) from a string.
+pub fn strip_newlines(input: &Value, args: &[Value]) -> FilterResult {
+    try!(check_args_len(args, 0));
 
-    let n = input.as_float().ok_or_else(|| InvalidType("Number expected".to_owned()))?;
+    let input = input.to_string();
+    Ok(Str(input
+               .chars()
+               .filter(|c| *c != '\n' && *c != '\r')
+               .collect()))
+}
 
-    let x = args[0].as_float().ok_or_else(|| InvalidArgument(0, "Number expected".to_owned()))?;
+pub fn join(input: &Value, args: &[Value]) -> FilterResult {
+    if 1 < args.len() {
+        return Err(InvalidArgumentCount(format!("expected one, {} given", args.len())));
+    }
+
+    let glue = args.get(0)
+        .map(|v| v.to_string())
+        .unwrap_or_else(|| " ".to_owned());
+
+    let input = input
+        .as_array()
+        .ok_or_else(|| InvalidType("Array of strings expected".to_owned()))?;
+    // use ToStr to stringify the values in case they aren't strings...
+    let input = input.iter().map(|x| x.to_string());
 
-    Ok(Num(n % x))
+    Ok(Str(itertools::join(input, glue.as_str())))
 }
 
-/// Replaces every newline (`\n`) with an HTML line break (`<br>`).
-pub fn newline_to_br(input: &Value, args: &[Value]) -> FilterResult {
+pub fn sort(input: &Value, args: &[Value]) -> FilterResult {
     try!(check_args_len(args, 0));
 
-    let s = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?;
-    Ok(Str(s.replace("\n", "<br />")))
+    // TODO optional property parameter
+
+    let array = input
+        .as_array()
+        .ok_or_else(|| InvalidType("Array expected".to_owned()))?;
+    let mut sorted = array.clone();
+    sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(cmp::Ordering::Equal));
+    Ok(Value::Array(sorted))
 }
 
-pub fn pluralize(input: &Value, args: &[Value]) -> FilterResult {
-    try!(check_args_len(args, 2));
+// TODO sort_natural
 
-    let n = input.as_float().ok_or_else(|| InvalidType("Number expected".to_owned()))?;
-    if (n as isize) == 1 {
-        Ok(args[0].clone())
-    } else {
-        Ok(args[1].clone())
+/// Removes any duplicate elements in an array.
+///
+/// This has an O(n^2) worst-case complexity.
+pub fn uniq(input: &Value, args: &[Value]) -> FilterResult {
+    try!(check_args_len(args, 0));
+
+    // TODO optional property parameter
+
+    let array = input
+        .as_array()
+        .ok_or_else(|| InvalidType("Array expected".to_owned()))?;
+    let mut deduped: Vec<Value> = Vec::new();
+    for x in array.iter() {
+        if !deduped.contains(x) {
+            deduped.push(x.clone())
+        }
     }
+    Ok(Value::Array(deduped))
 }
 
-pub fn plus(input: &Value, args: &[Value]) -> FilterResult {
-    try!(check_args_len(args, 1));
+/// Reverses the order of the items in an array. `reverse` cannot `reverse` a string.
+pub fn reverse(input: &Value, args: &[Value]) -> FilterResult {
+    try!(check_args_len(args, 0));
 
-    let n = input.as_float().ok_or_else(|| InvalidType("Number expected".to_owned()))?;
+    let array = input
+        .as_array()
+        .ok_or_else(|| InvalidType("Array expected".to_owned()))?;
+    let mut reversed = array.clone();
+    reversed.reverse();
+    Ok(Value::Array(reversed))
+}
 
-    let x = args[0].as_float().ok_or_else(|| InvalidArgument(0, "Number expected".to_owned()))?;
+// TODO map
 
-    Ok(Num(n + x))
-}
+// TODO compact
 
-pub fn prepend(input: &Value, args: &[Value]) -> FilterResult {
-    try!(check_args_len(args, 1));
+pub fn replace(input: &Value, args: &[Value]) -> FilterResult {
+    if args.len() < 1 || 2 < args.len() {
+        return Err(InvalidArgumentCount(format!("expected one or two, {} given", args.len())));
+    }
 
-    let x = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?;
+    let input = input.to_string();
 
-    let a = args[0].as_str()
-        .ok_or_else(|| InvalidArgument(0, "String expected".to_owned()))?;
+    let search = args[0].to_string();
+    let replace = args.get(1)
+        .map(|v| v.to_string())
+        .unwrap_or_else(|| "".to_owned());
 
-    Ok(Str(format!("{}{}", a, x)))
+    Ok(Str(input.replace(search.as_str(), replace.as_str())))
 }
 
-pub fn remove(input: &Value, args: &[Value]) -> FilterResult {
-    try!(check_args_len(args, 1));
+pub fn replace_first(input: &Value, args: &[Value]) -> FilterResult {
+    if args.len() < 1 || 2 < args.len() {
+        return Err(InvalidArgumentCount(format!("expected one or two, {} given", args.len())));
+    }
 
-    let x = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?;
+    let input = input.to_string();
 
-    let a = args[0].as_str()
-        .ok_or_else(|| InvalidArgument(0, "String expected".to_owned()))?;
+    let search = args[0].to_string();
+    let replace = args.get(1)
+        .map(|v| v.to_string())
+        .unwrap_or_else(|| "".to_owned());
 
-    Ok(Str(x.replace(a, "")))
+    {
+        let tokens: Vec<&str> = input.splitn(2, search.as_str()).collect();
+        if tokens.len() == 2 {
+            let result = [tokens[0], replace.as_str(), tokens[1]].join("");
+            return Ok(Str(result));
+        }
+    }
+    Ok(Str(input))
 }
 
-pub fn remove_first(input: &Value, args: &[Value]) -> FilterResult {
+pub fn remove(input: &Value, args: &[Value]) -> FilterResult {
     try!(check_args_len(args, 1));
 
-    let x = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?;
+    let input = input.to_string();
 
-    let a = args[0].as_str()
-        .ok_or_else(|| InvalidArgument(0, "String expected".to_owned()))?;
+    let string = args[0].to_string();
 
-    Ok(Str(x.splitn(2, a).collect()))
+    Ok(Str(input.replace(string.as_str(), "")))
 }
 
-pub fn replace(input: &Value, args: &[Value]) -> FilterResult {
-    try!(check_args_len(args, 2));
+pub fn remove_first(input: &Value, args: &[Value]) -> FilterResult {
+    try!(check_args_len(args, 1));
 
-    let x = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?;
+    let input = input.to_string();
 
-    let search = args[0].as_str()
-        .ok_or_else(|| InvalidArgument(0, "String expected".to_owned()))?;
-    let replace = args[1].as_str()
-        .ok_or_else(|| InvalidArgument(1, "String expected".to_owned()))?;
+    let string = args[0].to_string();
 
-    Ok(Str(x.replace(search, replace)))
+    Ok(Str(input.splitn(2, string.as_str()).collect()))
 }
 
-pub fn replace_first(input: &Value, args: &[Value]) -> FilterResult {
-    try!(check_args_len(args, 2));
-
-    let x = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?;
+pub fn append(input: &Value, args: &[Value]) -> FilterResult {
+    try!(check_args_len(args, 1));
 
-    let search = args[0].as_str()
-        .ok_or_else(|| InvalidArgument(0, "String expected".to_owned()))?;
-    let replace = args[1].as_str()
-        .ok_or_else(|| InvalidArgument(1, "String expected".to_owned()))?;
+    let mut input = input.to_string();
 
-    let tokens: Vec<&str> = x.splitn(2, search).collect();
-    if tokens.len() == 2 {
-        let result = tokens[0].to_string() + replace + tokens[1];
-        Ok(Str(result))
-    } else {
-        Ok(Str(x.to_string()))
-    }
-}
+    let string = args[0].to_string();
 
-/// Reverses the order of the items in an array. `reverse` cannot `reverse` a string.
-pub fn reverse(input: &Value, args: &[Value]) -> FilterResult {
-    try!(check_args_len(args, 0));
+    input.push_str(string.as_str());
 
-    let array = input.as_array().ok_or_else(|| InvalidType("Array expected".to_owned()))?;
-    let mut reversed = array.clone();
-    reversed.reverse();
-    Ok(Value::Array(reversed))
+    Ok(Str(input))
 }
 
-pub fn round(input: &Value, args: &[Value]) -> FilterResult {
-    try!(check_args_len(args, 0));
+// TODO concat
 
-    let n = input.as_float().ok_or_else(|| InvalidType("Number expected".to_owned()))?;
-    Ok(Num(n.round()))
-}
-
-/// Removes all whitespace (tabs, spaces, and newlines) from the right side of a string.
-///
-/// The filter does not affect spaces between words.  Note that while this works for the case of
-/// tabs, spaces, and newlines, it also removes any other codepoints defined by the Unicode Derived
-/// Core Property `White_Space` (per [rust
-/// documentation](https://doc.rust-lang.org/std/primitive.str.html#method.trim_left).
-pub fn rstrip(input: &Value, args: &[Value]) -> FilterResult {
-    try!(check_args_len(args, 0));
+pub fn prepend(input: &Value, args: &[Value]) -> FilterResult {
+    try!(check_args_len(args, 1));
 
-    let s = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?;
-    Ok(Str(s.trim_right().to_string()))
-}
+    let input = input.to_string();
 
-pub fn size(input: &Value, _args: &[Value]) -> FilterResult {
-    match *input {
-        Str(ref x) => Ok(Num(x.len() as f32)),
-        Array(ref x) => Ok(Num(x.len() as f32)),
-        Object(ref x) => Ok(Num(x.len() as f32)),
-        _ => Err(InvalidType("String, Array or Object expected".to_owned())),
-    }
-}
+    let mut string = args[0].to_string();
 
-pub fn slice(input: &Value, args: &[Value]) -> FilterResult {
-    if args.len() < 1 || args.len() > 2 {
-        return Err(InvalidArgumentCount(format!("expected one or two arguments, {} given",
-                                                args.len())));
-    }
-    let start = args[0].as_float()
-        .ok_or_else(|| InvalidArgument(0, "Number expected".to_owned()))?;
-    let mut start = start as isize;
-    let mut offset = match args.get(1) {
-        Some(&Num(x)) if x > 0f32 => x as isize,
-        Some(_) => return Err(InvalidArgument(1, "Positive number expected".to_owned())),
-        None => 1,
-    };
+    string.push_str(input.as_str());
 
-    let x = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?;
-    // this simplifies counting and conversions
-    let ilen = x.len() as isize;
-    if start > ilen {
-        start = ilen;
-    }
-    // Check for overflows over string length and fallback to allowed values
-    if start < 0 {
-        start += ilen;
-    }
-    // start is guaranteed to be positive at this point
-    if start + offset > ilen {
-        offset = ilen - start;
-    }
-    Ok(Value::Str(x.chars().skip(start.abs() as usize).take(offset as usize).collect()))
+    Ok(Str(string))
 }
 
-pub fn sort(input: &Value, args: &[Value]) -> FilterResult {
+/// Replaces every newline (`\n`) with an HTML line break (`<br>`).
+pub fn newline_to_br(input: &Value, args: &[Value]) -> FilterResult {
     try!(check_args_len(args, 0));
 
-    let array = input.as_array().ok_or_else(|| InvalidType("Array expected".to_owned()))?;
-    let mut sorted = array.clone();
-    sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
-    Ok(Value::Array(sorted))
+    // TODO handle windows line endings
+    let input = input.to_string();
+    Ok(Str(input.replace("\n", "<br />")))
 }
 
-pub fn split(input: &Value, args: &[Value]) -> FilterResult {
+pub fn date(input: &Value, args: &[Value]) -> FilterResult {
     try!(check_args_len(args, 1));
 
-    let string_to_split = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?;
+    let format = args[0].to_string();
+    if format.is_empty() {
+        return Ok(input.clone());
+    }
 
-    let split_string = args[0].as_str()
-        .ok_or_else(|| InvalidArgument(0, "String expected".to_owned()))?;
+    let input_string = input
+        .as_str()
+        .ok_or_else(|| InvalidType("String expected".to_owned()))?;
+    let date = DateTime::parse_from_str(input_string, "%d %B %Y %H:%M:%S %z");
+    let date = match date {
+        Ok(d) => d,
+        Err(_) => {
+            return Ok(input.clone());
+        }
+    };
 
-    // Split and construct resulting Array
-    Ok(Array(string_to_split.split(split_string)
-        .map(|x| Str(String::from(x)))
-        .collect()))
+    Ok(Value::Str(date.format(format.as_str()).to_string()))
 }
 
-/// Removes all whitespace (tabs, spaces, and newlines) from both the left and right side of a
-/// string.
-///
-/// It does not affect spaces between words.  Note that while this works for the case of tabs,
-/// spaces, and newlines, it also removes any other codepoints defined by the Unicode Derived Core
-/// Property `White_Space` (per [rust
-/// documentation](https://doc.rust-lang.org/std/primitive.str.html#method.trim_left).
-pub fn strip(input: &Value, args: &[Value]) -> FilterResult {
+pub fn first(input: &Value, args: &[Value]) -> FilterResult {
     try!(check_args_len(args, 0));
 
-    let s = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?;
-    Ok(Str(s.trim().to_string()))
+    match *input {
+        Str(ref x) => {
+            let c = x.chars()
+                .next()
+                .map(|c| c.to_string())
+                .unwrap_or_else(|| "".to_owned());
+            Ok(Str(c))
+        }
+        Array(ref x) => Ok(x.first().unwrap_or(&Str("".to_owned())).to_owned()),
+        _ => Err(InvalidType("String or Array expected".to_owned())),
+    }
 }
 
-pub fn strip_html(input: &Value, args: &[Value]) -> FilterResult {
-    lazy_static! {
-        // regexps taken from https://git.io/vXbgS
-        static ref MATCHERS: [Regex; 4] = [Regex::new(r"(?is)<script.*?</script>").unwrap(),
-                                           Regex::new(r"(?is)<style.*?</style>").unwrap(),
-                                           Regex::new(r"(?is)<!--.*?-->").unwrap(),
-                                           Regex::new(r"(?is)<.*?>").unwrap()];
+pub fn last(input: &Value, args: &[Value]) -> FilterResult {
+    try!(check_args_len(args, 0));
+
+    match *input {
+        Str(ref x) => {
+            let c = x.chars()
+                .last()
+                .map(|c| c.to_string())
+                .unwrap_or_else(|| "".to_owned());
+            Ok(Str(c))
+        }
+        Array(ref x) => Ok(x.last().unwrap_or(&Str("".to_owned())).to_owned()),
+        _ => Err(InvalidType("String or Array expected".to_owned())),
     }
+}
+
+/// Returns the absolute value of a number.
+pub fn abs(input: &Value, args: &[Value]) -> FilterResult {
     try!(check_args_len(args, 0));
+    match *input {
+        Str(ref s) => {
+            match s.parse::<f32>() {
+                Ok(n) => Ok(Num(n.abs())),
+                Err(e) => {
+                    Err(InvalidType(format!("Non-numeric-string, parse error ``{}'' occurred",
+                                            e.to_string())))
+                }
+            }
+        }
+        Num(n) => Ok(Num(n.abs())),
+        _ => Err(InvalidType("String or number expected".to_owned())),
+    }
+}
+
+pub fn plus(input: &Value, args: &[Value]) -> FilterResult {
+    try!(check_args_len(args, 1));
 
-    let x = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?;
+    let input = input
+        .as_float()
+        .ok_or_else(|| InvalidType("Number expected".to_owned()))?;
 
-    let result = MATCHERS.iter()
-        .fold(x.to_string(),
-              |acc, &ref matcher| matcher.replace_all(&acc, "").into_owned());
-    Ok(Str(result))
+    let operand = args[0]
+        .as_float()
+        .ok_or_else(|| InvalidArgument(0, "Number expected".to_owned()))?;
+
+    Ok(Num(input + operand))
 }
 
-/// Removes any newline characters (line breaks) from a string.
-pub fn strip_newlines(input: &Value, args: &[Value]) -> FilterResult {
-    try!(check_args_len(args, 0));
+pub fn minus(input: &Value, args: &[Value]) -> FilterResult {
+    try!(check_args_len(args, 1));
+
+    let input = input
+        .as_float()
+        .ok_or_else(|| InvalidType("Number expected".to_owned()))?;
+
+    let operand = args[0]
+        .as_float()
+        .ok_or_else(|| InvalidArgument(0, "Number expected".to_owned()))?;
 
-    let s = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?;
-    Ok(Str(s.replace("\n", "")))
+    Ok(Num(input - operand))
 }
 
 pub fn times(input: &Value, args: &[Value]) -> FilterResult {
     try!(check_args_len(args, 1));
 
-    let n = input.as_float().ok_or_else(|| InvalidType("Number expected".to_owned()))?;
+    let input = input
+        .as_float()
+        .ok_or_else(|| InvalidType("Number expected".to_owned()))?;
 
-    let x = args[0].as_float().ok_or_else(|| InvalidArgument(0, "Number expected".to_owned()))?;
+    let operand = args[0]
+        .as_float()
+        .ok_or_else(|| InvalidArgument(0, "Number expected".to_owned()))?;
 
-    Ok(Num(n * x))
+    Ok(Num(input * operand))
 }
 
-/// `truncate` shortens a string down to the number of characters passed as a parameter.
-///
-/// Note that this function operates on [grapheme
-/// clusters](http://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries) (or *user-perceived
-/// character*), rather than Unicode code points.  Each grapheme cluster may be composed of more
-/// than one Unicode code point, and does not necessarily correspond to rust's conception of a
-/// character.
-///
-/// If the number of characters specified is less than the length of the string, an ellipsis
-/// (`...`) is appended to the string and is included in the character count.
-///
-/// ## Custom ellipsis
-///
-/// `truncate` takes an optional second parameter that specifies the sequence of characters to be
-/// appended to the truncated string. By default this is an ellipsis (`...`), but you can specify a
-/// different sequence.
-///
-/// The length of the second parameter counts against the number of characters specified by the
-/// first parameter. For example, if you want to truncate a string to exactly 10 characters, and
-/// use a 3-character ellipsis, use 13 for the first parameter of `truncate`, since the ellipsis
-/// counts as 3 characters.
-///
-/// ## No ellipsis
-///
-/// You can truncate to the exact number of characters specified by the first parameter and show no
-/// trailing characters by passing a blank string as the second parameter.
-pub fn truncate(input: &Value, args: &[Value]) -> FilterResult {
-    if args.len() < 1 || args.len() > 2 {
-        return Err(InvalidArgumentCount(format!("expected one or two arguments, {} given",
-                                                args.len())));
-    }
+pub fn divided_by(input: &Value, args: &[Value]) -> FilterResult {
+    try!(check_args_len(args, 1));
 
-    let num_chars = match args.first() {
-        Some(&Num(x)) if x > 0f32 => x as usize,
-        _ => return Err(InvalidArgument(0, "Positive number expected".to_string())),
-    };
+    let input = input
+        .as_float()
+        .ok_or_else(|| InvalidType("Number expected".to_owned()))?;
 
-    let ellipsis = Value::str("...");
-    let append = args.get(1)
-        .unwrap_or(&ellipsis)
-        .as_str()
-        .ok_or_else(|| InvalidArgument(1, "String expected".to_owned()))?;
+    let operand = args[0]
+        .as_float()
+        .ok_or_else(|| InvalidArgument(0, "Number expected".to_owned()))?;
+
+    // TODO only do `.floor` if its an integer
+    Ok(Num((input / operand).floor()))
+}
 
-    let s = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?;
+pub fn modulo(input: &Value, args: &[Value]) -> FilterResult {
+    try!(check_args_len(args, 1));
+
+    let input = input
+        .as_float()
+        .ok_or_else(|| InvalidType("Number expected".to_owned()))?;
+
+    let operand = args[0]
+        .as_float()
+        .ok_or_else(|| InvalidArgument(0, "Number expected".to_owned()))?;
 
-    Ok(Str(UnicodeSegmentation::graphemes(s, true)
-        .take(num_chars - append.len())
-        .collect::<Vec<&str>>()
-        .join("")
-        .to_string() + append))
+    Ok(Num(input % operand))
 }
 
-pub fn truncatewords(input: &Value, args: &[Value]) -> FilterResult {
-    if args.len() < 1 || args.len() > 2 {
-        return Err(InvalidArgumentCount(format!("expected one or two arguments, {} given",
-                                                args.len())));
+pub fn round(input: &Value, args: &[Value]) -> FilterResult {
+    if 1 < args.len() {
+        return Err(InvalidArgumentCount(format!("expected one, {} given", args.len())));
     }
 
-    let num_words = match args.first() {
-        Some(&Num(x)) if x > 0f32 => x as usize,
-        _ => return Err(InvalidArgument(0, "Positive number expected".to_owned())),
-    };
-
-    let empty = Value::str("");
-    let append = args.get(1)
-        .unwrap_or(&empty)
-        .as_str()
-        .ok_or_else(|| InvalidArgument(1, "String expected".to_owned()))?;
+    let n = args.get(0)
+        .unwrap_or(&Value::Num(0_f32))
+        .as_float()
+        .ok_or_else(|| InvalidArgument(0, "Number expected".to_owned()))?;
+    let n = n as usize;
 
-    let x = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?;
+    let input = input
+        .as_float()
+        .ok_or_else(|| InvalidType("Number expected".to_owned()))?;
 
-    let words: Vec<&str> = x.split(' ').take(num_words).collect();
-    let mut result = words.join(" ");
-    if *x != result {
-        result = result + append;
+    if n == 0 {
+        Ok(Num(input.round()))
+    } else {
+        // TODO support this
+        Err(InvalidArgument(0,
+                            "Rounding to additional places is not yet supported".to_owned()))
     }
-    Ok(Str(result))
 }
 
-/// Removes any duplicate elements in an array.
-///
-/// This has an O(n^2) worst-case complexity.
-pub fn uniq(input: &Value, args: &[Value]) -> FilterResult {
+pub fn ceil(input: &Value, args: &[Value]) -> FilterResult {
     try!(check_args_len(args, 0));
 
-    let array = input.as_array()
-        .ok_or_else(|| InvalidType("Array expected".to_owned()))?;
-    let mut deduped: Vec<Value> = Vec::new();
-    for x in array.iter() {
-        if !deduped.contains(x) {
-            deduped.push(x.clone())
-        }
-    }
-    Ok(Value::Array(deduped))
+    let n = input
+        .as_float()
+        .ok_or_else(|| InvalidType("Number expected".to_owned()))?;
+    Ok(Num(n.ceil()))
 }
 
-pub fn upcase(input: &Value, args: &[Value]) -> FilterResult {
+pub fn floor(input: &Value, args: &[Value]) -> FilterResult {
     try!(check_args_len(args, 0));
 
-    let s = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?;
-    Ok(Str(s.to_uppercase()))
+    let n = input
+        .as_float()
+        .ok_or_else(|| InvalidType("Number expected".to_owned()))?;
+    Ok(Num(n.floor()))
 }
 
 pub fn default(input: &Value, args: &[Value]) -> FilterResult {
@@ -654,6 +765,46 @@ pub fn default(input: &Value, args: &[Value]) -> FilterResult {
     }
 }
 
+// shopify
+
+#[cfg(feature = "extra-filters")]
+pub fn pluralize(input: &Value, args: &[Value]) -> FilterResult {
+    try!(check_args_len(args, 2));
+
+    let n = input
+        .as_float()
+        .ok_or_else(|| InvalidType("Number expected".to_owned()))?;
+    if (n as isize) == 1 {
+        Ok(args[0].clone())
+    } else {
+        Ok(args[1].clone())
+    }
+}
+
+// liquid-rust proprietary
+
+#[cfg(feature = "extra-filters")]
+pub fn date_in_tz(input: &Value, args: &[Value]) -> FilterResult {
+    try!(check_args_len(args, 2));
+
+    let s = input
+        .as_str()
+        .ok_or_else(|| InvalidType("String expected".to_owned()))?;
+    let date = DateTime::parse_from_str(s, "%d %B %Y %H:%M:%S %z")
+        .map_err(|e| FilterError::InvalidType(format!("Invalid date format: {}", e)))?;
+
+    let format = args[0]
+        .as_str()
+        .ok_or_else(|| InvalidArgument(0, "String expected".to_owned()))?;
+
+    let n = args[1]
+        .as_float()
+        .ok_or_else(|| InvalidArgument(1, "Number expected".to_owned()))?;
+    let timezone = FixedOffset::east((n * 3600.0) as i32);
+
+    Ok(Value::Str(date.with_timezone(&timezone).format(format).to_string()))
+}
+
 #[cfg(test)]
 mod tests {
 
@@ -760,21 +911,42 @@ mod tests {
                          tos!("13 Jun 2016 02:30:00 +0300"),
                          &[tos!("%Y-%m-%d")]),
                    tos!("2016-06-13"));
+    }
 
+    #[test]
+    fn unit_date_bad_input_type() {
         assert_eq!(failed!(date, Num(0f32), &[tos!("%Y-%m-%d")]),
                    FilterError::InvalidType("String expected".to_owned()));
+    }
+
+    #[test]
+    fn unit_date_bad_input_format() {
+        assert_eq!(unit!(date, tos!("blah blah blah"), &[tos!("%Y-%m-%d")]),
+                   tos!("blah blah blah"));
+    }
 
-        assert_eq!(failed!(date, tos!("blah blah blah"), &[tos!("%Y-%m-%d")]),
-                   FilterError::InvalidType("Invalid date format: input contains invalid \
-                                             characters"
-                       .to_owned()));
+    #[test]
+    fn unit_date_format_empty() {
+        assert_eq!(unit!(date,
+                         tos!("13 Jun 2016 02:30:00 +0300"),
+                         &[Str("".to_owned())]),
+                   tos!("13 Jun 2016 02:30:00 +0300"));
+    }
 
-        assert_eq!(failed!(date, tos!("13 Jun 2016 02:30:00 +0300"), &[Num(0f32)]),
-                   FilterError::InvalidArgument(0, "String expected".to_owned()));
+    #[test]
+    fn unit_date_bad_format_type() {
+        assert_eq!(unit!(date, tos!("13 Jun 2016 02:30:00 +0300"), &[Num(0f32)]),
+                   tos!("0"));
+    }
 
+    #[test]
+    fn unit_date_missing_format() {
         assert_eq!(failed!(date, tos!("13 Jun 2016 02:30:00 +0300")),
                    FilterError::InvalidArgumentCount("expected 1, 0 given".to_owned()));
+    }
 
+    #[test]
+    fn unit_date_extra_param() {
         assert_eq!(failed!(date,
                            tos!("13 Jun 2016 02:30:00 +0300"),
                            &[Num(0f32), Num(1f32)]),
@@ -824,7 +996,7 @@ mod tests {
         let args = &[tos!("%Y-%m-%d %H:%M:%S %z"), Num(0f32)];
         let desired_result = FilterError::InvalidType("Invalid date format: input contains \
                                                        invalid characters"
-            .to_owned());
+                                                              .to_owned());
         assert_eq!(failed!(date_in_tz, input, args), desired_result);
     }
 
@@ -945,7 +1117,7 @@ mod tests {
         let input = Array(vec![tos!("a"), tos!("b"), tos!("c")]);
         let args = &[Num(1f32)];
         let result = join(&input, args);
-        assert!(result.is_err());
+        assert_eq!(result.unwrap(), tos!("a1b1c"));
     }
 
     #[test]
@@ -953,7 +1125,7 @@ mod tests {
         let input = Array(vec![tos!("a"), tos!("b"), tos!("c")]);
         let args = &[];
         let result = join(&input, args);
-        assert!(result.is_err());
+        assert_eq!(result.unwrap(), tos!("a b c"));
     }
 
     #[test]
@@ -986,8 +1158,8 @@ mod tests {
     fn unit_lstrip_non_string() {
         let input = &Num(0f32);
         let args = &[];
-        let desired_result = FilterError::InvalidType("String expected".to_string());
-        assert_eq!(failed!(lstrip, input, args), desired_result);
+        let desired_result = tos!("0");
+        assert_eq!(unit!(lstrip, input, args), desired_result);
     }
 
     #[test]
@@ -1063,6 +1235,7 @@ mod tests {
     }
 
     #[test]
+    #[cfg(feature = "extra-filters")]
     fn unit_pluralize() {
         assert_eq!(unit!(pluralize, Num(1f32), &[tos!("one"), tos!("many")]),
                    tos!("one"));
@@ -1122,10 +1295,15 @@ mod tests {
     #[test]
     fn unit_reverse_apples_oranges_peaches_plums() {
         // First example from https://shopify.github.io/liquid/filters/reverse/
-        let input = &Array(vec![tos!("apples"), tos!("oranges"), tos!("peaches"), tos!("plums")]);
+        let input = &Array(vec![tos!("apples"),
+                                tos!("oranges"),
+                                tos!("peaches"),
+                                tos!("plums")]);
         let args = &[];
-        let desired_result =
-            Array(vec![tos!("plums"), tos!("peaches"), tos!("oranges"), tos!("apples")]);
+        let desired_result = Array(vec![tos!("plums"),
+                                        tos!("peaches"),
+                                        tos!("oranges"),
+                                        tos!("apples")]);
         assert_eq!(unit!(reverse, input, args), desired_result);
     }
 
@@ -1199,8 +1377,8 @@ mod tests {
     fn unit_rstrip_non_string() {
         let input = &Num(0f32);
         let args = &[];
-        let desired_result = FilterError::InvalidType("String expected".to_string());
-        assert_eq!(failed!(rstrip, input, args), desired_result);
+        let desired_result = tos!("0");
+        assert_eq!(unit!(rstrip, input, args), desired_result);
     }
 
     #[test]
@@ -1245,20 +1423,12 @@ mod tests {
                    Array(vec![tos!("a"), tos!("b")]));
     }
 
-    #[test]
-    fn unit_split_bad_input() {
-        let input = Array(vec![tos!("a"), tos!("b"), tos!("c")]);
-        let args = &[tos!(",")];
-        let result = split(&input, args);
-        assert!(result.is_err());
-    }
-
     #[test]
     fn unit_split_bad_split_string() {
         let input = tos!("a,b,c");
         let args = &[Num(1f32)];
-        let result = split(&input, args);
-        assert!(result.is_err());
+        let desired_result = Array(vec![tos!("a,b,c")]);
+        assert_eq!(unit!(split, input, args), desired_result);
     }
 
     #[test]
@@ -1289,8 +1459,8 @@ mod tests {
     fn unit_strip_non_string() {
         let input = &Num(0f32);
         let args = &[];
-        let desired_result = FilterError::InvalidType("String expected".to_string());
-        assert_eq!(failed!(strip, input, args), desired_result);
+        let desired_result = tos!("0");
+        assert_eq!(unit!(strip, input, args), desired_result);
     }
 
     #[test]
@@ -1370,8 +1540,8 @@ mod tests {
     fn unit_strip_newlines_non_string() {
         let input = &Num(0f32);
         let args = &[];
-        let desired_result = FilterError::InvalidType("String expected".to_string());
-        assert_eq!(failed!(strip_newlines, input, args), desired_result);
+        let desired_result = tos!("0");
+        assert_eq!(unit!(strip_newlines, input, args), desired_result);
     }
 
     #[test]
@@ -1420,17 +1590,16 @@ mod tests {
     fn unit_truncate_negative_length() {
         let input = &tos!("I often quote myself.  It adds spice to my conversation.");
         let args = &[Num(-17f32)];
-        let desired_result = FilterError::InvalidArgument(0,
-                                                          "Positive number expected".to_string());
-        assert_eq!(failed!(truncate, input, args), desired_result);
+        let desired_result = tos!("I often quote myself.  It adds spice to my conversation.");
+        assert_eq!(unit!(truncate, input, args), desired_result);
     }
 
     #[test]
     fn unit_truncate_non_string() {
-        let input = &Num(0f32);
-        let args = &[Num(17f32)];
-        let desired_result = FilterError::InvalidType("String expected".to_string());
-        assert_eq!(failed!(truncate, input, args), desired_result);
+        let input = &Num(10000000f32);
+        let args = &[Num(5f32)];
+        let desired_result = tos!("10...");
+        assert_eq!(unit!(truncate, input, args), desired_result);
     }
 
     #[test]
@@ -1484,28 +1653,41 @@ mod tests {
     fn unit_truncate_zero_arguments() {
         let input = &tos!("I often quote myself.  It adds spice to my conversation.");
         let args = &[];
-        let desired_result =
-            FilterError::InvalidArgumentCount("expected one or two arguments, 0 given".to_string());
-        assert_eq!(failed!(truncate, input, args), desired_result);
+        let desired_result = tos!("I often quote myself.  It adds spice to my conv...");
+        assert_eq!(unit!(truncate, input, args), desired_result);
     }
 
     #[test]
-    fn unit_truncatewords() {
-        assert_eq!(failed!(truncatewords, tos!("bar bar"), &[Num(-1_f32)]),
-                   FilterError::InvalidArgument(0, "Positive number expected".to_owned()));
-        assert_eq!(failed!(truncatewords, tos!("bar bar"), &[Num(0_f32)]),
-                   FilterError::InvalidArgument(0, "Positive number expected".to_owned()));
-        assert_eq!(unit!(truncatewords, tos!("bar bar"), &[Num(1_f32)]),
-                   tos!("bar"));
-        assert_eq!(unit!(truncatewords, tos!("bar bar"), &[Num(2_f32)]),
-                   tos!("bar bar"));
-        assert_eq!(unit!(truncatewords, tos!("bar bar"), &[Num(3_f32)]),
-                   tos!("bar bar"));
+    fn unit_truncatewords_negative_length() {
+        assert_eq!(unit!(truncatewords, tos!("one two three"), &[Num(-1_f32)]),
+                   tos!("one two three"));
+    }
+
+    #[test]
+    fn unit_truncatewords_zero_length() {
+        assert_eq!(unit!(truncatewords, tos!("one two three"), &[Num(0_f32)]),
+                   tos!("..."));
+    }
+
+    #[test]
+    fn unit_truncatewords_no_truncation() {
+        assert_eq!(unit!(truncatewords, tos!("one two three"), &[Num(4_f32)]),
+                   tos!("one two three"));
+    }
+
+    #[test]
+    fn unit_truncatewords_truncate() {
+        assert_eq!(unit!(truncatewords, tos!("one two three"), &[Num(2_f32)]),
+                   tos!("one two..."));
+        assert_eq!(unit!(truncatewords,
+                         tos!("one two three"),
+                         &[Num(2_f32), Num(1_f32)]),
+                   tos!("one two1"));
+    }
+
+    #[test]
+    fn unit_truncatewords_empty_string() {
         assert_eq!(unit!(truncatewords, tos!(""), &[Num(1_f32)]), tos!(""));
-        assert_eq!(unit!(truncatewords, tos!("bar bar"), &[Num(1_f32), tos!("...")]),
-                   tos!("bar..."));
-        assert_eq!(unit!(truncatewords, tos!("bar bar"), &[Num(2_f32), tos!("...")]),
-                   tos!("bar bar"));
     }
 
     #[test]
@@ -1535,8 +1717,11 @@ mod tests {
     #[test]
     fn unit_uniq_shopify_liquid() {
         // Test from https://shopify.github.io/liquid/filters/uniq/
-        let input =
-            &Array(vec![tos!("ants"), tos!("bugs"), tos!("bees"), tos!("bugs"), tos!("ants")]);
+        let input = &Array(vec![tos!("ants"),
+                                tos!("bugs"),
+                                tos!("bees"),
+                                tos!("bugs"),
+                                tos!("ants")]);
         let args = &[];
         let desired_result = Array(vec![tos!("ants"), tos!("bugs"), tos!("bees")]);
         assert_eq!(unit!(uniq, input, args), desired_result);
diff --git a/src/lexer.rs b/src/lexer.rs
index 0b1443a..056a838 100644
--- a/src/lexer.rs
+++ b/src/lexer.rs
@@ -111,43 +111,47 @@ pub fn granularize(block: &str) -> Result<Vec<Token>> {
 
     for el in split_atom(block) {
         result.push(match &*el.trim() {
-            "" => continue,
+                        "" => continue,
 
-            "|" => Pipe,
-            "." => Dot,
-            ":" => Colon,
-            "," => Comma,
-            "[" => OpenSquare,
-            "]" => CloseSquare,
-            "(" => OpenRound,
-            ")" => CloseRound,
-            "?" => Question,
-            "-" => Dash,
-            "=" => Assignment,
-            "or" => Or,
+                        "|" => Pipe,
+                        "." => Dot,
+                        ":" => Colon,
+                        "," => Comma,
+                        "[" => OpenSquare,
+                        "]" => CloseSquare,
+                        "(" => OpenRound,
+                        ")" => CloseRound,
+                        "?" => Question,
+                        "-" => Dash,
+                        "=" => Assignment,
+                        "or" => Or,
 
-            "==" => Comparison(Equals),
-            "!=" => Comparison(NotEquals),
-            "<=" => Comparison(LessThanEquals),
-            ">=" => Comparison(GreaterThanEquals),
-            "<" => Comparison(LessThan),
-            ">" => Comparison(GreaterThan),
-            "contains" => Comparison(Contains),
-            ".." => DotDot,
+                        "==" => Comparison(Equals),
+                        "!=" => Comparison(NotEquals),
+                        "<=" => Comparison(LessThanEquals),
+                        ">=" => Comparison(GreaterThanEquals),
+                        "<" => Comparison(LessThan),
+                        ">" => Comparison(GreaterThan),
+                        "contains" => Comparison(Contains),
+                        ".." => DotDot,
 
-            x if SINGLE_STRING_LITERAL.is_match(x) || DOUBLE_STRING_LITERAL.is_match(x) => {
-                StringLiteral(x[1..x.len() - 1].to_owned())
-            }
-            x if NUMBER_LITERAL.is_match(x) => {
-                NumberLiteral(x.parse::<f32>().expect(&format!("Could not parse {:?} as float", x)))
-            }
-            x if BOOLEAN_LITERAL.is_match(x) => {
-                BooleanLiteral(x.parse::<bool>()
-                    .expect(&format!("Could not parse {:?} as bool", x)))
-            }
-            x if IDENTIFIER.is_match(x) => Identifier(x.to_owned()),
-            x => return Err(Error::Lexer(format!("{} is not a valid identifier", x))),
-        });
+                        x if SINGLE_STRING_LITERAL.is_match(x) ||
+                             DOUBLE_STRING_LITERAL.is_match(x) => {
+                            StringLiteral(x[1..x.len() - 1].to_owned())
+                        }
+                        x if NUMBER_LITERAL.is_match(x) => {
+                            NumberLiteral(x.parse::<f32>()
+                                              .expect(&format!("Could not parse {:?} as float",
+                                                               x)))
+                        }
+                        x if BOOLEAN_LITERAL.is_match(x) => {
+                            BooleanLiteral(x.parse::<bool>()
+                                               .expect(&format!("Could not parse {:?} as bool",
+                                                                x)))
+                        }
+                        x if IDENTIFIER.is_match(x) => Identifier(x.to_owned()),
+                        x => return Err(Error::Lexer(format!("{} is not a valid identifier", x))),
+                    });
     }
 
     Ok(result)
@@ -218,31 +222,54 @@ fn test_tokenize() {
 #[test]
 fn test_granularize() {
     assert_eq!(granularize("include my-file.html").unwrap(),
-               vec![Identifier("include".to_owned()), Identifier("my-file.html".to_owned())]);
+               vec![Identifier("include".to_owned()),
+                    Identifier("my-file.html".to_owned())]);
     assert_eq!(granularize("test | me").unwrap(),
-               vec![Identifier("test".to_owned()), Pipe, Identifier("me".to_owned())]);
+               vec![Identifier("test".to_owned()),
+                    Pipe,
+                    Identifier("me".to_owned())]);
     assert_eq!(granularize("test .. me").unwrap(),
-               vec![Identifier("test".to_owned()), DotDot, Identifier("me".to_owned())]);
+               vec![Identifier("test".to_owned()),
+                    DotDot,
+                    Identifier("me".to_owned())]);
     assert_eq!(granularize("test : me").unwrap(),
-               vec![Identifier("test".to_owned()), Colon, Identifier("me".to_owned())]);
+               vec![Identifier("test".to_owned()),
+                    Colon,
+                    Identifier("me".to_owned())]);
     assert_eq!(granularize("test , me").unwrap(),
-               vec![Identifier("test".to_owned()), Comma, Identifier("me".to_owned())]);
+               vec![Identifier("test".to_owned()),
+                    Comma,
+                    Identifier("me".to_owned())]);
     assert_eq!(granularize("test [ me").unwrap(),
-               vec![Identifier("test".to_owned()), OpenSquare, Identifier("me".to_owned())]);
+               vec![Identifier("test".to_owned()),
+                    OpenSquare,
+                    Identifier("me".to_owned())]);
     assert_eq!(granularize("test ] me").unwrap(),
-               vec![Identifier("test".to_owned()), CloseSquare, Identifier("me".to_owned())]);
+               vec![Identifier("test".to_owned()),
+                    CloseSquare,
+                    Identifier("me".to_owned())]);
     assert_eq!(granularize("test ( me").unwrap(),
-               vec![Identifier("test".to_owned()), OpenRound, Identifier("me".to_owned())]);
+               vec![Identifier("test".to_owned()),
+                    OpenRound,
+                    Identifier("me".to_owned())]);
     assert_eq!(granularize("test ) me").unwrap(),
-               vec![Identifier("test".to_owned()), CloseRound, Identifier("me".to_owned())]);
+               vec![Identifier("test".to_owned()),
+                    CloseRound,
+                    Identifier("me".to_owned())]);
     assert_eq!(granularize("test ? me").unwrap(),
-               vec![Identifier("test".to_owned()), Question, Identifier("me".to_owned())]);
+               vec![Identifier("test".to_owned()),
+                    Question,
+                    Identifier("me".to_owned())]);
     assert_eq!(granularize("test - me").unwrap(),
-               vec![Identifier("test".to_owned()), Dash, Identifier("me".to_owned())]);
+               vec![Identifier("test".to_owned()),
+                    Dash,
+                    Identifier("me".to_owned())]);
     assert_eq!(granularize("test me").unwrap(),
                vec![Identifier("test".to_owned()), Identifier("me".to_owned())]);
     assert_eq!(granularize("test = me").unwrap(),
-               vec![Identifier("test".to_owned()), Assignment, Identifier("me".to_owned())]);
+               vec![Identifier("test".to_owned()),
+                    Assignment,
+                    Identifier("me".to_owned())]);
     assert_eq!(granularize("test == me").unwrap(),
                vec![Identifier("test".to_owned()),
                     Comparison(Equals),
@@ -296,7 +323,9 @@ fn test_granularize() {
                     Comma,
                     Identifier("arg2".to_owned())]);
     assert_eq!(granularize("multiply 5 3").unwrap(),
-               vec![Identifier("multiply".to_owned()), NumberLiteral(5f32), NumberLiteral(3f32)]);
+               vec![Identifier("multiply".to_owned()),
+                    NumberLiteral(5f32),
+                    NumberLiteral(3f32)]);
     assert_eq!(granularize("for i in (1..5)").unwrap(),
                vec![Identifier("for".to_owned()),
                     Identifier("i".to_owned()),
@@ -314,5 +343,7 @@ fn test_granularize() {
                vec![StringLiteral("1, '2', 3, 4".to_owned()),
                     StringLiteral("1, '2', 3, 4".to_owned())]);
     assert_eq!(granularize("abc : \"1, '2', 3, 4\"").unwrap(),
-               vec![Identifier("abc".to_owned()), Colon, StringLiteral("1, '2', 3, 4".to_owned())]);
+               vec![Identifier("abc".to_owned()),
+                    Colon,
+                    StringLiteral("1, '2', 3, 4".to_owned())]);
 }
diff --git a/src/main.rs b/src/main.rs
index 56be172..0227708 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -77,7 +77,8 @@ fn build_context(path: &path::Path) -> Result<liquid::Context> {
 }
 
 fn run() -> Result<()> {
-    let matches = clap::App::new("liquidate").version(crate_version!())
+    let matches = clap::App::new("liquidate")
+        .version(crate_version!())
         .author(crate_authors!())
         .arg(option("input", "LIQUID").required(true))
         .arg(option("output", "TXT"))
@@ -88,18 +89,23 @@ fn run() -> Result<()> {
     let mut options = liquid::LiquidOptions::default();
     options.file_system = matches.value_of("include-root").map(path::PathBuf::from);
 
-    let mut data = matches.value_of("context")
+    let mut data = matches
+        .value_of("context")
         .map(|s| {
-            let p = path::PathBuf::from(s);
-            build_context(p.as_path())
-        })
+                 let p = path::PathBuf::from(s);
+                 build_context(p.as_path())
+             })
         .map_or(Ok(None), |r| r.map(Some))?
         .unwrap_or_else(liquid::Context::new);
 
-    let template_path =
-        matches.value_of("input").map(path::PathBuf::from).expect("Parameter was required");
+    let template_path = matches
+        .value_of("input")
+        .map(path::PathBuf::from)
+        .expect("Parameter was required");
     let template = liquid::parse_file(template_path, options)?;
-    let output = template.render(&mut data)?.unwrap_or_else(|| "".to_string());
+    let output = template
+        .render(&mut data)?
+        .unwrap_or_else(|| "".to_string());
     match matches.value_of("output") {
         Some(path) => {
             let mut out = fs::File::create(path::PathBuf::from(path))?;
diff --git a/src/output.rs b/src/output.rs
index e047066..6fa944f 100644
--- a/src/output.rs
+++ b/src/output.rs
@@ -51,14 +51,21 @@ impl Output {
         let mut entry = match self.entry {
             VarOrVal::Val(ref x) => x.clone(),
             VarOrVal::Var(ref x) => {
-                context.get_val(&*x.name()).cloned().unwrap_or_else(|| Value::Str("".to_owned()))
+                context
+                    .get_val(&*x.name())
+                    .cloned()
+                    .unwrap_or_else(|| Value::Str("".to_owned()))
             }
         };
 
         // apply all specified filters
         for filter in &self.filters {
-            let f = try!(context.get_filter(&filter.name)
-                .ok_or_else(|| Error::Render(format!("Filter {} not implemented", &filter.name))));
+            let f = try!(context
+                             .get_filter(&filter.name)
+                             .ok_or_else(|| {
+                                             Error::Render(format!("Filter {} not implemented",
+                                                                   &filter.name))
+                                         }));
 
             let mut arguments = Vec::new();
             for arg in &filter.arguments {
diff --git a/src/parser.rs b/src/parser.rs
index ebd153d..f7cc63e 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -331,7 +331,7 @@ mod test {
             // top level, which is where it should split.
             let tokens = tokenize("{% comment %}A{%endcomment%} bunch of {{text}} with {{no}} \
                                    else tag")
-                .unwrap();
+                    .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
@@ -360,7 +360,7 @@ mod test {
                                           "{% endfor %}",
                                           "{% else %}",
                                           "trailing tags"))
-                .unwrap();
+                    .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
diff --git a/src/tags/assign_tag.rs b/src/tags/assign_tag.rs
index 7326870..4efd312 100644
--- a/src/tags/assign_tag.rs
+++ b/src/tags/assign_tag.rs
@@ -33,10 +33,7 @@ pub fn assign_tag(_tag_name: &str,
 
     let src = try!(parse_output(&arguments[2..]));
 
-    Ok(Box::new(Assign {
-        dst: dst,
-        src: src,
-    }))
+    Ok(Box::new(Assign { dst: dst, src: src }))
 }
 
 #[cfg(test)]
diff --git a/src/tags/capture_block.rs b/src/tags/capture_block.rs
index 2588e57..19af498 100644
--- a/src/tags/capture_block.rs
+++ b/src/tags/capture_block.rs
@@ -45,9 +45,9 @@ pub fn capture_block(_tag_name: &str,
     let t = Template::new(try!(parse(tokens, options)));
 
     Ok(Box::new(Capture {
-        id: id,
-        template: t,
-    }))
+                    id: id,
+                    template: t,
+                }))
 }
 
 #[cfg(test)]
diff --git a/src/tags/comment_block.rs b/src/tags/comment_block.rs
index f4d9a1b..148d669 100644
--- a/src/tags/comment_block.rs
+++ b/src/tags/comment_block.rs
@@ -35,7 +35,10 @@ mod test {
                                     &[],
                                     &vec![Expression(vec![], "This is a test".to_string())],
                                     &options);
-        assert_eq!(comment.unwrap().render(&mut Default::default()).unwrap(),
+        assert_eq!(comment
+                       .unwrap()
+                       .render(&mut Default::default())
+                       .unwrap(),
                    None);
     }
 }
diff --git a/src/tags/cycle_tag.rs b/src/tags/cycle_tag.rs
index d6d50e2..a95d1d9 100644
--- a/src/tags/cycle_tag.rs
+++ b/src/tags/cycle_tag.rs
@@ -53,13 +53,15 @@ fn parse_cycle(arguments: &[Token], _options: &LiquidOptions) -> Result<Cycle> {
     }
 
     if name.is_empty() {
-        name = values.iter().fold(String::new(), |acc, n| acc + n.to_string().as_str())
+        name = values
+            .iter()
+            .fold(String::new(), |acc, n| acc + n.to_string().as_str())
     }
 
     Ok(Cycle {
-        name: name,
-        values: values,
-    })
+           name: name,
+           values: values,
+       })
 }
 
 pub fn cycle_tag(_tag_name: &str,
@@ -101,7 +103,7 @@ mod test {
                            "{% cycle 'a': 'one', 'two', 'three' %}\n",
                            "{% cycle 'b': 'one', 'two', 'three' %}\n",
                            "{% cycle 'b': 'one', 'two', 'three' %}\n")
-            .to_owned();
+                .to_owned();
 
         let t = parse(&text, Default::default()).unwrap();
         let mut context = Context::new();
@@ -120,7 +122,7 @@ mod test {
                            "{% cycle 'one', 'two', 'three' %}\n",
                            "{% cycle 'one', 'two', 'three' %}\n",
                            "{% cycle 'one', 'two', 'three' %}\n")
-            .to_owned();
+                .to_owned();
 
         let t = parse(&text, Default::default()).unwrap();
         let mut context = Context::new();
@@ -140,7 +142,7 @@ mod test {
                            "{% cycle alpha, beta, gamma %}\n",
                            "{% cycle alpha, beta, gamma %}\n",
                            "{% cycle alpha, beta, gamma %}\n")
-            .to_owned();
+                .to_owned();
 
         let t = parse(&text, Default::default()).unwrap();
         let mut context = Context::new();
diff --git a/src/tags/for_block.rs b/src/tags/for_block.rs
index 2afbf3b..84fb9fa 100644
--- a/src/tags/for_block.rs
+++ b/src/tags/for_block.rs
@@ -88,8 +88,8 @@ impl Renderable for For {
                     for (i, v) in slice.iter().enumerate() {
                         helper_vars.insert("index0".to_owned(), Value::Num(i as f32));
                         helper_vars.insert("index".to_owned(), Value::Num((i + 1) as f32));
-                        helper_vars.insert(
-                            "rindex0".to_owned(), Value::Num((range_len - i - 1) as f32));
+                        helper_vars.insert("rindex0".to_owned(),
+                                           Value::Num((range_len - i - 1) as f32));
                         helper_vars.insert("rindex".to_owned(), Value::Num((range_len - i) as f32));
                         helper_vars.insert("first".to_owned(), Value::Bool(i == 0));
                         helper_vars.insert("last".to_owned(), Value::Bool(i == (range_len - 1)));
@@ -196,14 +196,14 @@ pub fn for_block(_tag_name: &str,
     };
 
     Ok(Box::new(For {
-        var_name: var_name,
-        range: range,
-        item_template: item_template,
-        else_template: else_template,
-        limit: limit,
-        offset: offset,
-        reversed: reversed,
-    }))
+                    var_name: var_name,
+                    range: range,
+                    item_template: item_template,
+                    else_template: else_template,
+                    limit: limit,
+                    offset: offset,
+                    reversed: reversed,
+                }))
 }
 
 #[cfg(test)]
@@ -293,7 +293,7 @@ mod test {
                                 ">>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()));
+                                .to_owned()));
     }
 
     #[test]
@@ -428,7 +428,7 @@ mod test {
                                                   "value: {{v}}, ",
                                                   "first: {{forloop.first}}, ",
                                                   "last: {{forloop.last}}\n"))
-                                    .unwrap(),
+                                         .unwrap(),
                                 &options);
 
         let mut data: Context = Default::default();
@@ -438,7 +438,7 @@ mod test {
 "length: 3, index: 2, index0: 1, rindex: 2, rindex0: 1, value: 101, first: false, last: false\n",
 "length: 3, index: 3, index0: 2, rindex: 1, rindex0: 0, value: 102, first: false, last: true\n",
 )
-                       .to_owned()));
+                                .to_owned()));
     }
 
 
@@ -457,10 +457,10 @@ mod test {
         let mut data: Context = Default::default();
         data.add_filter("shout",
                         Box::new(|input, _args| if let &Value::Str(ref s) = input {
-                            Ok(Value::Str(s.to_uppercase()))
-                        } else {
-                            FilterError::invalid_type("Expected a string")
-                        }));
+                                     Ok(Value::Str(s.to_uppercase()))
+                                 } else {
+                                     FilterError::invalid_type("Expected a string")
+                                 }));
 
         data.set_val("array",
                      Value::Array(vec![Value::str("alpha"),
diff --git a/src/tags/if_block.rs b/src/tags/if_block.rs
index 6c3830a..9f332f0 100644
--- a/src/tags/if_block.rs
+++ b/src/tags/if_block.rs
@@ -77,10 +77,10 @@ fn condition(arguments: &[Token]) -> Result<Condition> {
     };
 
     Ok(Condition {
-        lh: lh,
-        comparison: comp,
-        rh: rh,
-    })
+           lh: lh,
+           comparison: comp,
+           rh: rh,
+       })
 }
 
 pub fn unless_block(_tag_name: &str,
@@ -90,11 +90,11 @@ pub fn unless_block(_tag_name: &str,
                     -> Result<Box<Renderable>> {
     let cond = try!(condition(arguments));
     Ok(Box::new(Conditional {
-        condition: cond,
-        mode: false,
-        if_true: Template::new(try!(parse(&tokens[..], options))),
-        if_false: None,
-    }))
+                    condition: cond,
+                    mode: false,
+                    if_true: Template::new(try!(parse(&tokens[..], options))),
+                    if_false: None,
+                }))
 }
 
 pub fn if_block(_tag_name: &str,
@@ -114,11 +114,7 @@ pub fn if_block(_tag_name: &str,
         }
 
         Some(ref split) if split.delimiter == "elsif" => {
-            let child_tokens: Vec<Element> = split.trailing
-                .iter()
-                .skip(1)
-                .cloned()
-                .collect();
+            let child_tokens: Vec<Element> = split.trailing.iter().skip(1).cloned().collect();
             let parsed = try!(if_block("if", &split.args[1..], &child_tokens, options));
             Some(Template::new(vec![parsed]))
         }
@@ -129,11 +125,11 @@ pub fn if_block(_tag_name: &str,
     let if_true = Template::new(try!(parse(leading_tokens, options)));
 
     Ok(Box::new(Conditional {
-        condition: cond,
-        mode: true,
-        if_true: if_true,
-        if_false: if_false,
-    }))
+                    condition: cond,
+                    mode: true,
+                    if_true: if_true,
+                    if_false: if_false,
+                }))
 }
 
 #[cfg(test)]
@@ -148,14 +144,14 @@ mod test {
     fn number_comparison() {
         let a = parse("{% if 6 < 7  %}if true{% endif %}",
                       LiquidOptions::default())
-            .unwrap()
-            .render(&mut Context::new());
+                .unwrap()
+                .render(&mut Context::new());
         assert_eq!(a.unwrap(), Some("if true".to_owned()));
 
         let b = parse("{% if 7 < 6  %}if true{% else %}if false{% endif %}",
                       LiquidOptions::default())
-            .unwrap()
-            .render(&mut Context::new());
+                .unwrap()
+                .render(&mut Context::new());
         assert_eq!(b.unwrap(), Some("if false".to_owned()));
     }
 
@@ -164,15 +160,15 @@ mod test {
         // "one" == "one" then "if true" else "if false"
         let a = parse("{% if \"one\" == \"one\" %}if true{% endif %}",
                       LiquidOptions::default())
-            .unwrap()
-            .render(&mut Context::new());
+                .unwrap()
+                .render(&mut Context::new());
         assert_eq!(a.unwrap(), Some("if true".to_owned()));
 
         // "one" == "two"
         let b = parse("{% if \"one\" == \"two\" %}if true{% endif %}",
                       LiquidOptions::default())
-            .unwrap()
-            .render(&mut Context::new());
+                .unwrap()
+                .render(&mut Context::new());
         assert_eq!(b.unwrap(), Some("".to_owned()));
     }
 
diff --git a/src/tags/interrupt_tags.rs b/src/tags/interrupt_tags.rs
index 65b34f9..78c5561 100644
--- a/src/tags/interrupt_tags.rs
+++ b/src/tags/interrupt_tags.rs
@@ -66,7 +66,7 @@ mod test {
         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()));
+                            .to_owned()));
     }
 
     #[test]
@@ -88,7 +88,7 @@ mod test {
                    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()));
+                                .to_owned()));
     }
 
     #[test]
@@ -108,7 +108,7 @@ mod test {
                                 "enter-2;continue-2\n",
                                 "enter-3;exit-3\n",
                                 "enter-4;exit-4\n")
-                       .to_owned()));
+                                .to_owned()));
     }
 
     #[test]
@@ -130,7 +130,7 @@ mod test {
                    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()));
+                                .to_owned()));
     }
 
 
diff --git a/src/tags/raw_block.rs b/src/tags/raw_block.rs
index c5876b4..208a923 100644
--- a/src/tags/raw_block.rs
+++ b/src/tags/raw_block.rs
@@ -20,14 +20,16 @@ pub fn raw_block(_tag_name: &str,
                  tokens: &[Element],
                  _options: &LiquidOptions)
                  -> Result<Box<Renderable>> {
-    let content = tokens.iter().fold("".to_owned(), |a, b| {
-        match *b {
-                Expression(_, ref text) |
-                Tag(_, ref text) |
-                Raw(ref text) => text,
-            }
-            .to_owned() + &a
-    });
+    let content = tokens
+        .iter()
+        .fold("".to_owned(), |a, b| {
+            match *b {
+                    Expression(_, ref text) |
+                    Tag(_, ref text) |
+                    Raw(ref text) => text,
+                }
+                .to_owned() + &a
+        });
     Ok(Box::new(RawT { content: content }))
 }
 
diff --git a/src/template.rs b/src/template.rs
index 3df4550..a0a8e94 100644
--- a/src/template.rs
+++ b/src/template.rs
@@ -1,12 +1,14 @@
 use Renderable;
 use context::Context;
 use filters::{abs, append, capitalize, ceil, date, default, divided_by, downcase, escape,
-              escape_once, first, floor, join, last, lstrip, minus, modulo, newline_to_br,
-              pluralize, plus, prepend, remove, remove_first, replace, replace_first, reverse,
-              round, rstrip, size, slice, sort, split, strip, strip_html, strip_newlines, times,
-              truncate, truncatewords, uniq, upcase};
+              escape_once, first, floor, join, last, lstrip, minus, modulo, newline_to_br, plus,
+              prepend, remove, remove_first, replace, replace_first, reverse, round, rstrip, size,
+              slice, sort, split, strip, strip_html, strip_newlines, times, truncate,
+              truncatewords, uniq, upcase};
 use error::Result;
 
+#[cfg(feature = "extra-filters")]
+use filters::pluralize;
 #[cfg(feature = "extra-filters")]
 use filters::date_in_tz;
 
@@ -35,7 +37,6 @@ impl Renderable for Template {
         context.maybe_add_filter("minus", Box::new(minus));
         context.maybe_add_filter("modulo", Box::new(modulo));
         context.maybe_add_filter("newline_to_br", Box::new(newline_to_br));
-        context.maybe_add_filter("pluralize", Box::new(pluralize));
         context.maybe_add_filter("plus", Box::new(plus));
         context.maybe_add_filter("prepend", Box::new(prepend));
         context.maybe_add_filter("remove", Box::new(remove));
@@ -58,7 +59,8 @@ impl Renderable for Template {
         context.maybe_add_filter("uniq", Box::new(uniq));
         context.maybe_add_filter("upcase", Box::new(upcase));
 
-
+        #[cfg(feature = "extra-filters")]
+        context.maybe_add_filter("pluralize", Box::new(pluralize));
         #[cfg(feature = "extra-filters")]
         context.maybe_add_filter("date_in_tz", Box::new(date_in_tz));
 
diff --git a/src/value.rs b/src/value.rs
index 9261e6e..6d6780f 100644
--- a/src/value.rs
+++ b/src/value.rs
@@ -169,8 +169,9 @@ impl ToString for Value {
                 arr.join(", ")
             }
             Value::Object(ref x) => {
-                let arr: Vec<String> =
-                    x.iter().map(|(k, v)| k.clone() + ": " + &v.to_string()).collect();
+                let arr: Vec<String> = x.iter()
+                    .map(|(k, v)| k.clone() + ": " + &v.to_string())
+                    .collect();
                 arr.join(", ")
             }
         }
@@ -217,8 +218,9 @@ mod test {
 
     #[test]
     fn test_array_to_string() {
-        let val =
-            Value::Array(vec![Value::Num(3f32), Value::Str("test".to_owned()), Value::Num(5.3)]);
+        let val = Value::Array(vec![Value::Num(3f32),
+                                    Value::Str("test".to_owned()),
+                                    Value::Num(5.3)]);
         assert_eq!(&val.to_string(), "3, test, 5.3");
     }
 


Test patch:
diff --git a/tests/custom_blocks.rs b/tests/custom_blocks.rs
index 9f8410f..c2c4cef 100644
--- a/tests/custom_blocks.rs
+++ b/tests/custom_blocks.rs
@@ -27,11 +27,12 @@ fn run() {
                     _options: &LiquidOptions)
                     -> Result<Box<Renderable>, Error> {
 
-        let numbers = arguments.iter()
+        let numbers = arguments
+            .iter()
             .filter_map(|x| match *x {
-                Token::NumberLiteral(ref num) => Some(*num),
-                _ => None,
-            })
+                            Token::NumberLiteral(ref num) => Some(*num),
+                            _ => None,
+                        })
             .collect();
         Ok(Box::new(Multiply { numbers: numbers }))
     }
@@ -45,7 +46,7 @@ fn run() {
 
     let template = parse("wat\n{{hello}}\n{{multiply 5 3}}{%raw%}{{multiply 5 3}}{%endraw%} test",
                          options)
-        .unwrap();
+            .unwrap();
 
     let mut data = Context::new();
     data.set_val("hello", Value::Str("world".to_string()));
diff --git a/tests/filters.rs b/tests/filters.rs
index 27b6994..0bddbd3 100644
--- a/tests/filters.rs
+++ b/tests/filters.rs
@@ -47,6 +47,7 @@ pub fn capitalize() {
 }
 
 #[test]
+#[cfg(feature = "extra-filters")]
 pub fn pluralize() {
     let text = "{{ count | pluralize: 'one', 'many'}}";
     let options: LiquidOptions = Default::default();
diff --git a/tests/fixtures.rs b/tests/fixtures.rs
index e66baf1..f382ac9 100644
--- a/tests/fixtures.rs
+++ b/tests/fixtures.rs
@@ -11,7 +11,10 @@ fn compare(name: &str, context: &mut Context) {
     let input_file = format!("tests/fixtures/input/{}.txt", name);
     let output_file = format!("tests/fixtures/output/{}.txt", name);
     let mut input = String::new();
-    File::open(Path::new(&input_file)).unwrap().read_to_string(&mut input).unwrap();
+    File::open(Path::new(&input_file))
+        .unwrap()
+        .read_to_string(&mut input)
+        .unwrap();
 
     let options: LiquidOptions = Default::default();
     let template = parse(&input, options).unwrap();
@@ -19,7 +22,10 @@ fn compare(name: &str, context: &mut Context) {
     let output = template.render(context).unwrap();
 
     let mut comp = String::new();
-    File::open(output_file).unwrap().read_to_string(&mut comp).unwrap();
+    File::open(output_file)
+        .unwrap()
+        .read_to_string(&mut comp)
+        .unwrap();
 
     assert_diff!(&comp, &output.unwrap(), " ", 0);
 }
diff --git a/tests/multithreading.rs b/tests/multithreading.rs
index 451059e..cbcc1a4 100644
--- a/tests/multithreading.rs
+++ b/tests/multithreading.rs
@@ -34,7 +34,10 @@ pub fn pass_between_threads() {
 
             let output_file = format!("tests/fixtures/output/example_mt{}.txt", counter);
             let mut comp = String::new();
-            File::open(output_file).unwrap().read_to_string(&mut comp).unwrap();
+            File::open(output_file)
+                .unwrap()
+                .read_to_string(&mut comp)
+                .unwrap();
 
             assert_diff!(&comp, &output.unwrap(), " ", 0);
         }));
diff --git a/tests/parse_file.rs b/tests/parse_file.rs
index 8c734aa..238dcae 100644
--- a/tests/parse_file.rs
+++ b/tests/parse_file.rs
@@ -20,7 +20,10 @@ fn compare_by_file(name: &str, context: &mut Context) {
     let output = template.render(context).unwrap();
 
     let mut comp = String::new();
-    File::open(output_file).unwrap().read_to_string(&mut comp).unwrap();
+    File::open(output_file)
+        .unwrap()
+        .read_to_string(&mut comp)
+        .unwrap();
 
     assert_diff!(&comp, &output.unwrap(), " ", 0);
 }
diff --git a/tests/value.rs b/tests/value.rs
index 130a34c..ac93945 100644
--- a/tests/value.rs
+++ b/tests/value.rs
@@ -95,8 +95,9 @@ pub fn deserialize_str() {
 
 #[test]
 pub fn serialize_array() {
-    let actual =
-        vec![liquid::Value::Num(1f32), liquid::Value::Bool(true), liquid::Value::str("true")];
+    let actual = vec![liquid::Value::Num(1f32),
+                      liquid::Value::Bool(true),
+                      liquid::Value::str("true")];
     let actual = liquid::Value::Array(actual);
     let actual = serde_yaml::to_string(&actual).unwrap();
     assert_diff!(&actual, "---\n- 1\n- true\n- \"true\"", "", 0);
@@ -105,8 +106,9 @@ pub fn serialize_array() {
 #[test]
 pub fn deserialize_array() {
     let actual: liquid::Value = serde_yaml::from_str("---\n- 1\n- true\n- \"true\"").unwrap();
-    let expected =
-        vec![liquid::Value::Num(1f32), liquid::Value::Bool(true), liquid::Value::str("true")];
+    let expected = vec![liquid::Value::Num(1f32),
+                        liquid::Value::Bool(true),
+                        liquid::Value::str("true")];
     let expected = liquid::Value::Array(expected);
     assert_eq!(actual, expected);
 }
@@ -123,9 +125,9 @@ pub fn deserialize_object() {
     let expected: liquid::Object = [("Num".to_owned(), liquid::Value::Num(1f32)),
                                     ("Bool".to_owned(), liquid::Value::Bool(true)),
                                     ("Str".to_owned(), liquid::Value::str("true"))]
-        .iter()
-        .cloned()
-        .collect();
+            .iter()
+            .cloned()
+            .collect();
     let expected = liquid::Value::Object(expected);
     assert_eq!(actual, expected);
 }