diff --git a/Cargo.toml b/Cargo.toml index abbc1d8a1..5fd6db5b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ lazy_static = "0.2" chrono = "0.3" unicode-segmentation = "1.1" itertools = "0.6.0" +url = "1.4" serde = "1.0" serde_derive = "1.0" diff --git a/src/filters.rs b/src/filters.rs index 1358276f5..31ffa75b8 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -1,6 +1,8 @@ use std::fmt; use std::error::Error; use std::cmp; +use url::percent_encoding::EncodeSet; +use url::percent_encoding; use value::Value; use value::Value::{Array, Bool, Num, Object, Str}; @@ -55,10 +57,15 @@ pub type FilterResult = Result; pub type Filter = Fn(&Value, &[Value]) -> FilterResult; // Helper functions for the filters. -fn check_args_len(args: &[Value], expected_len: usize) -> Result<(), FilterError> { - if args.len() != expected_len { - return Err(InvalidArgumentCount(format!("expected {}, {} given", - expected_len, +fn check_args_len(args: &[Value], required: usize, optional: usize) -> Result<(), FilterError> { + if args.len() < required { + return Err(InvalidArgumentCount(format!("expected at least {}, {} given", + required, + args.len()))); + } + if required + optional < args.len() { + return Err(InvalidArgumentCount(format!("expected at most {}, {} given", + required + optional, args.len()))); } Ok(()) @@ -78,7 +85,7 @@ fn nr_escaped(text: &str) -> usize { // https://github.com/rust-lang/rust/blob/master/src/librustdoc/html/escape.rs // Retrieved 2016-11-19. fn _escape(input: &Value, args: &[Value], once_p: bool) -> FilterResult { - try!(check_args_len(args, 0)); + try!(check_args_len(args, 0, 0)); let s = input .as_str() @@ -131,21 +138,21 @@ pub fn size(input: &Value, _args: &[Value]) -> FilterResult { } pub fn downcase(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 0)); + try!(check_args_len(args, 0, 0)); let s = input.to_string(); Ok(Str(s.to_lowercase())) } pub fn upcase(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 0)); + try!(check_args_len(args, 0, 0)); let s = input.to_string(); Ok(Str(s.to_uppercase())) } pub fn capitalize(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 0)); + try!(check_args_len(args, 0, 0)); let s = input.to_string(); let mut chars = s.chars(); @@ -165,9 +172,56 @@ pub fn escape_once(input: &Value, args: &[Value]) -> FilterResult { _escape(input, args, true) } -// TODO url_encode +#[derive(Clone)] +struct UrlEncodeSet(String); + +impl UrlEncodeSet { + fn safe_bytes(&self) -> &[u8] { + let &UrlEncodeSet(ref safe) = self; + safe.as_bytes() + } +} + +impl EncodeSet for UrlEncodeSet { + fn contains(&self, byte: u8) -> bool { + let is_digit = 48 <= byte && byte <= 57; + let is_upper = 65 <= byte && byte <= 90; + let is_lower = 97 <= byte && byte <= 122; + // -, . or _ + let is_special = byte == 45 || byte == 46 || byte == 95; + if is_digit || is_upper || is_lower || is_special { + false + } else { + !self.safe_bytes().contains(&byte) + } + } +} + +pub fn url_encode(input: &Value, args: &[Value]) -> FilterResult { + try!(check_args_len(args, 0, 0)); -// TODO url_decode + lazy_static! { + static ref URL_ENCODE_SET: UrlEncodeSet = UrlEncodeSet("".to_owned()); + } + + let s = input.to_string(); + + let result = percent_encoding::utf8_percent_encode(s.as_str(), URL_ENCODE_SET.clone()) + .collect(); + Ok(Str(result)) +} + +pub fn url_decode(input: &Value, args: &[Value]) -> FilterResult { + try!(check_args_len(args, 0, 0)); + + let s = input.to_string(); + + let result = percent_encoding::percent_decode(s.as_bytes()) + .decode_utf8() + .map_err(|_| InvalidType("Malformed UTF-8".to_owned()))? + .into_owned(); + Ok(Str(result)) +} fn canonicalize_slice(slice_offset: isize, slice_length: isize, @@ -195,10 +249,7 @@ fn canonicalize_slice(slice_offset: isize, } 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()))); - } + try!(check_args_len(args, 1, 1)); let offset = args[0] .as_float() @@ -257,10 +308,7 @@ pub fn slice(input: &Value, args: &[Value]) -> FilterResult { /// 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()))); - } + try!(check_args_len(args, 0, 2)); let length = args.get(0) .unwrap_or(&Value::Num(50f32)) @@ -289,10 +337,7 @@ pub fn truncate(input: &Value, args: &[Value]) -> FilterResult { } pub fn truncatewords(input: &Value, args: &[Value]) -> FilterResult { - if 2 < args.len() { - return Err(InvalidArgumentCount(format!("expected one or two arguments, {} given", - args.len()))); - } + try!(check_args_len(args, 0, 2)); let words = args.get(0) .unwrap_or(&Value::Num(15f32)) @@ -318,7 +363,7 @@ pub fn truncatewords(input: &Value, args: &[Value]) -> FilterResult { } pub fn split(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 1)); + try!(check_args_len(args, 1, 0)); let input = input.to_string(); @@ -339,7 +384,7 @@ pub fn split(input: &Value, args: &[Value]) -> FilterResult { /// 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)); + try!(check_args_len(args, 0, 0)); let input = input.to_string(); Ok(Str(input.trim().to_string())) @@ -352,7 +397,7 @@ pub fn strip(input: &Value, args: &[Value]) -> FilterResult { /// Core Property `White_Space` (per [rust /// documentation](https://doc.rust-lang.org/std/primitive.str.html#method.trim_left). pub fn lstrip(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 0)); + try!(check_args_len(args, 0, 0)); let input = input.to_string(); Ok(Str(input.trim_left().to_string())) @@ -365,7 +410,7 @@ pub fn lstrip(input: &Value, args: &[Value]) -> FilterResult { /// 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)); + try!(check_args_len(args, 0, 0)); let input = input.to_string(); Ok(Str(input.trim_right().to_string())) @@ -379,7 +424,7 @@ pub fn strip_html(input: &Value, args: &[Value]) -> FilterResult { Regex::new(r"(?is)").unwrap(), Regex::new(r"(?is)<.*?>").unwrap()]; } - try!(check_args_len(args, 0)); + try!(check_args_len(args, 0, 0)); let input = input.to_string(); @@ -392,7 +437,7 @@ pub fn strip_html(input: &Value, args: &[Value]) -> FilterResult { /// Removes any newline characters (line breaks) from a string. pub fn strip_newlines(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 0)); + try!(check_args_len(args, 0, 0)); let input = input.to_string(); Ok(Str(input @@ -402,9 +447,7 @@ pub fn strip_newlines(input: &Value, args: &[Value]) -> FilterResult { } pub fn join(input: &Value, args: &[Value]) -> FilterResult { - if 1 < args.len() { - return Err(InvalidArgumentCount(format!("expected one, {} given", args.len()))); - } + try!(check_args_len(args, 0, 1)); let glue = args.get(0) .map(|v| v.to_string()) @@ -420,7 +463,7 @@ pub fn join(input: &Value, args: &[Value]) -> FilterResult { } pub fn sort(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 0)); + try!(check_args_len(args, 0, 0)); // TODO optional property parameter @@ -438,7 +481,7 @@ pub fn sort(input: &Value, args: &[Value]) -> FilterResult { /// /// This has an O(n^2) worst-case complexity. pub fn uniq(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 0)); + try!(check_args_len(args, 0, 0)); // TODO optional property parameter @@ -456,7 +499,7 @@ pub fn uniq(input: &Value, args: &[Value]) -> FilterResult { /// 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)); + try!(check_args_len(args, 0, 0)); let array = input .as_array() @@ -466,14 +509,40 @@ pub fn reverse(input: &Value, args: &[Value]) -> FilterResult { Ok(Value::Array(reversed)) } -// TODO map +/// Extract `property` from the `Value::Object` elements of an array +pub fn map(input: &Value, args: &[Value]) -> FilterResult { + try!(check_args_len(args, 1, 0)); + + let array = input + .as_array() + .ok_or_else(|| InvalidType("Array expected".to_owned()))?; + + let property = args[0].to_string(); + + let result = array + .iter() + .filter_map(|v| { + v.as_object() + .map(|o| o.get(&property).cloned()) + .map_or(None, |o| o) + }) + .collect(); + Ok(Value::Array(result)) +} + +/// Remove nulls from an iterable. For hashes, you can specify which property you +/// want to filter out if it maps to Null. +pub fn compact(input: &Value, args: &[Value]) -> FilterResult { + try!(check_args_len(args, 0, 1)); -// TODO compact + // No-op. liquid-rust doesn't have Null but this is left in for compatibility with existing + // shopofy/liquid code. Should it be a no-op, error, or not exist? + + Ok(input.clone()) +} 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()))); - } + try!(check_args_len(args, 1, 1)); let input = input.to_string(); @@ -486,9 +555,7 @@ pub fn replace(input: &Value, args: &[Value]) -> FilterResult { } 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()))); - } + try!(check_args_len(args, 1, 1)); let input = input.to_string(); @@ -508,7 +575,7 @@ pub fn replace_first(input: &Value, args: &[Value]) -> FilterResult { } pub fn remove(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 1)); + try!(check_args_len(args, 1, 0)); let input = input.to_string(); @@ -518,7 +585,7 @@ pub fn remove(input: &Value, args: &[Value]) -> FilterResult { } pub fn remove_first(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 1)); + try!(check_args_len(args, 1, 0)); let input = input.to_string(); @@ -528,7 +595,7 @@ pub fn remove_first(input: &Value, args: &[Value]) -> FilterResult { } pub fn append(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 1)); + try!(check_args_len(args, 1, 0)); let mut input = input.to_string(); @@ -539,10 +606,26 @@ pub fn append(input: &Value, args: &[Value]) -> FilterResult { Ok(Str(input)) } -// TODO concat +pub fn concat(input: &Value, args: &[Value]) -> FilterResult { + try!(check_args_len(args, 1, 0)); + + let input = input + .as_array() + .ok_or_else(|| InvalidType("Array expected".to_owned()))?; + let input = input.iter().map(|v| v.to_owned()); + + let array = args[0] + .as_array() + .ok_or_else(|| InvalidArgument(0, "Array expected".to_owned()))?; + let array = array.iter().map(|v| v.to_owned()); + + let result = input.chain(array); + let result: Vec<_> = result.collect(); + Ok(Array(result)) +} pub fn prepend(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 1)); + try!(check_args_len(args, 1, 0)); let input = input.to_string(); @@ -555,7 +638,7 @@ pub fn prepend(input: &Value, args: &[Value]) -> FilterResult { /// Replaces every newline (`\n`) with an HTML line break (`
`). pub fn newline_to_br(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 0)); + try!(check_args_len(args, 0, 0)); // TODO handle windows line endings let input = input.to_string(); @@ -563,7 +646,7 @@ pub fn newline_to_br(input: &Value, args: &[Value]) -> FilterResult { } pub fn date(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 1)); + try!(check_args_len(args, 1, 0)); let format = args[0].to_string(); if format.is_empty() { @@ -585,7 +668,7 @@ pub fn date(input: &Value, args: &[Value]) -> FilterResult { } pub fn first(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 0)); + try!(check_args_len(args, 0, 0)); match *input { Str(ref x) => { @@ -601,7 +684,7 @@ pub fn first(input: &Value, args: &[Value]) -> FilterResult { } pub fn last(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 0)); + try!(check_args_len(args, 0, 0)); match *input { Str(ref x) => { @@ -618,7 +701,7 @@ pub fn last(input: &Value, args: &[Value]) -> FilterResult { /// Returns the absolute value of a number. pub fn abs(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 0)); + try!(check_args_len(args, 0, 0)); match *input { Str(ref s) => { match s.parse::() { @@ -635,7 +718,7 @@ pub fn abs(input: &Value, args: &[Value]) -> FilterResult { } pub fn plus(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 1)); + try!(check_args_len(args, 1, 0)); let input = input .as_float() @@ -649,7 +732,7 @@ pub fn plus(input: &Value, args: &[Value]) -> FilterResult { } pub fn minus(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 1)); + try!(check_args_len(args, 1, 0)); let input = input .as_float() @@ -663,7 +746,7 @@ pub fn minus(input: &Value, args: &[Value]) -> FilterResult { } pub fn times(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 1)); + try!(check_args_len(args, 1, 0)); let input = input .as_float() @@ -677,7 +760,7 @@ pub fn times(input: &Value, args: &[Value]) -> FilterResult { } pub fn divided_by(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 1)); + try!(check_args_len(args, 1, 0)); let input = input .as_float() @@ -692,7 +775,7 @@ pub fn divided_by(input: &Value, args: &[Value]) -> FilterResult { } pub fn modulo(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 1)); + try!(check_args_len(args, 1, 0)); let input = input .as_float() @@ -706,9 +789,7 @@ pub fn modulo(input: &Value, args: &[Value]) -> FilterResult { } pub fn round(input: &Value, args: &[Value]) -> FilterResult { - if 1 < args.len() { - return Err(InvalidArgumentCount(format!("expected one, {} given", args.len()))); - } + try!(check_args_len(args, 0, 1)); let n = args.get(0) .unwrap_or(&Value::Num(0_f32)) @@ -731,7 +812,7 @@ pub fn round(input: &Value, args: &[Value]) -> FilterResult { } pub fn ceil(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 0)); + try!(check_args_len(args, 0, 0)); let n = input .as_float() @@ -740,7 +821,7 @@ pub fn ceil(input: &Value, args: &[Value]) -> FilterResult { } pub fn floor(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 0)); + try!(check_args_len(args, 0, 0)); let n = input .as_float() @@ -749,7 +830,7 @@ pub fn floor(input: &Value, args: &[Value]) -> FilterResult { } pub fn default(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 1)); + try!(check_args_len(args, 1, 0)); let use_default = match *input { Str(ref s) => s.is_empty(), @@ -770,7 +851,7 @@ pub fn default(input: &Value, args: &[Value]) -> FilterResult { #[cfg(feature = "extra-filters")] pub fn pluralize(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 2)); + try!(check_args_len(args, 2, 0)); let n = input .as_float() @@ -786,7 +867,7 @@ pub fn pluralize(input: &Value, args: &[Value]) -> FilterResult { #[cfg(feature = "extra-filters")] pub fn date_in_tz(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 2)); + try!(check_args_len(args, 2, 0)); let s = input .as_str() @@ -864,7 +945,8 @@ mod tests { fn unit_abs_one_argument() { let input = &Num(-1f32); let args = &[Num(0f32)]; - let desired_result = FilterError::InvalidArgumentCount("expected 0, 1 given".to_owned()); + let desired_result = FilterError::InvalidArgumentCount("expected at most 0, 1 given" + .to_owned()); assert_eq!(failed!(abs, input, args), desired_result); } @@ -881,6 +963,54 @@ mod tests { assert_eq!(unit!(append, tos!("sam"), &[tos!("son")]), tos!("samson")); } + #[test] + fn unit_concat_nothing() { + let input = Array(vec![Num(1f32), Num(2f32)]); + let args = &[Array(vec![])]; + let result = Array(vec![Num(1f32), Num(2f32)]); + assert_eq!(unit!(concat, input, args), result); + } + + #[test] + fn unit_concat_something() { + let input = Array(vec![Num(1f32), Num(2f32)]); + let args = &[Array(vec![Num(3f32), Num(4f32)])]; + let result = Array(vec![Num(1f32), Num(2f32), Num(3f32), Num(4f32)]); + assert_eq!(unit!(concat, input, args), result); + } + + #[test] + fn unit_concat_mixed() { + let input = Array(vec![Num(1f32), Num(2f32)]); + let args = &[Array(vec![Num(3f32), Value::str("a")])]; + let result = Array(vec![Num(1f32), Num(2f32), Num(3f32), Value::str("a")]); + assert_eq!(unit!(concat, input, args), result); + } + + #[test] + fn unit_concat_wrong_type() { + let input = Array(vec![Num(1f32), Num(2f32)]); + let args = &[Num(1f32)]; + let result = FilterError::InvalidArgument(0, "Array expected".to_owned()); + assert_eq!(failed!(concat, input, args), result); + } + + #[test] + fn unit_concat_no_args() { + let input = Array(vec![Num(1f32), Num(2f32)]); + let args = &[]; + let result = FilterError::InvalidArgumentCount("expected at least 1, 0 given".to_owned()); + assert_eq!(failed!(concat, input, args), result); + } + + #[test] + fn unit_concat_extra_args() { + let input = Array(vec![Num(1f32), Num(2f32)]); + let args = &[Array(vec![Num(3f32), Value::str("a")]), Num(2f32)]; + let result = FilterError::InvalidArgumentCount("expected at most 1, 2 given".to_owned()); + assert_eq!(failed!(concat, input, args), result); + } + #[test] fn unit_capitalize() { assert_eq!(unit!(capitalize, tos!("abc")), tos!("Abc")); @@ -943,7 +1073,7 @@ mod tests { #[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())); + FilterError::InvalidArgumentCount("expected at least 1, 0 given".to_owned())); } #[test] @@ -951,7 +1081,7 @@ mod tests { assert_eq!(failed!(date, tos!("13 Jun 2016 02:30:00 +0300"), &[Num(0f32), Num(1f32)]), - FilterError::InvalidArgumentCount("expected 1, 2 given".to_owned())); + FilterError::InvalidArgumentCount("expected at most 1, 2 given".to_owned())); } #[test] @@ -1024,7 +1154,8 @@ mod tests { fn unit_date_in_tz_zero_arguments() { let input = &tos!("13 Jun 2016 12:00:00 +0000"); let args = &[]; - let desired_result = FilterError::InvalidArgumentCount("expected 2, 0 given".to_owned()); + let desired_result = FilterError::InvalidArgumentCount("expected at least 2, 0 given" + .to_owned()); assert_eq!(failed!(date_in_tz, input, args), desired_result); } @@ -1033,7 +1164,8 @@ mod tests { fn unit_date_in_tz_one_argument() { let input = &tos!("13 Jun 2016 12:00:00 +0000"); let args = &[tos!("%Y-%m-%d %H:%M:%S %z")]; - let desired_result = FilterError::InvalidArgumentCount("expected 2, 1 given".to_owned()); + let desired_result = FilterError::InvalidArgumentCount("expected at least 2, 1 given" + .to_owned()); assert_eq!(failed!(date_in_tz, input, args), desired_result); } @@ -1042,7 +1174,8 @@ mod tests { fn unit_date_in_tz_three_arguments() { let input = &tos!("13 Jun 2016 12:00:00 +0000"); let args = &[tos!("%Y-%m-%d %H:%M:%S %z"), Num(0f32), Num(1f32)]; - let desired_result = FilterError::InvalidArgumentCount("expected 2, 3 given".to_owned()); + let desired_result = FilterError::InvalidArgumentCount("expected at most 2, 3 given" + .to_owned()); assert_eq!(failed!(date_in_tz, input, args), desired_result); } @@ -1080,6 +1213,22 @@ mod tests { tos!("<>&'"&xyz;")); } + #[test] + fn unit_url_encode() { + assert_eq!(unit!(url_encode, tos!("foo bar")), tos!("foo%20bar")); + assert_eq!(unit!(url_encode, tos!("foo+1@example.com")), + tos!("foo%2B1%40example.com")); + } + + #[test] + fn unit_url_decode() { + // TODO Test case from shopify/liquid that we aren't handling: + // - assert_eq!(unit!(url_decode, tos!("foo+bar")), tos!("foo bar")); + assert_eq!(unit!(url_decode, tos!("foo%20bar")), tos!("foo bar")); + assert_eq!(unit!(url_decode, tos!("foo%2B1%40example.com")), + tos!("foo+1@example.com")); + } + #[test] fn unit_first() { assert_eq!(unit!(first, @@ -1167,7 +1316,8 @@ mod tests { fn unit_lstrip_one_argument() { let input = &tos!(" \n \r test"); let args = &[Num(0f32)]; - let desired_result = FilterError::InvalidArgumentCount("expected 0, 1 given".to_owned()); + let desired_result = FilterError::InvalidArgumentCount("expected at most 0, 1 given" + .to_owned()); assert_eq!(failed!(lstrip, input, args), desired_result); } @@ -1231,7 +1381,8 @@ mod tests { fn unit_newline_to_br_one_argument() { let input = &tos!("a\nb"); let args = &[Num(0f32)]; - let desired_result = FilterError::InvalidArgumentCount("expected 0, 1 given".to_owned()); + let desired_result = FilterError::InvalidArgumentCount("expected at most 0, 1 given" + .to_owned()); assert_eq!(failed!(newline_to_br, input, args), desired_result); } @@ -1320,7 +1471,8 @@ mod tests { fn unit_reverse_array_extra_args() { let input = &Array(vec![Num(3f32), Num(1f32), Num(2f32)]); let args = &[Num(0f32)]; - let desired_result = FilterError::InvalidArgumentCount("expected 0, 1 given".to_owned()); + let desired_result = FilterError::InvalidArgumentCount("expected at most 0, 1 given" + .to_owned()); assert_eq!(failed!(reverse, input, args), desired_result); } @@ -1386,7 +1538,8 @@ mod tests { fn unit_rstrip_one_argument() { let input = &tos!(" \n \r test"); let args = &[Num(0f32)]; - let desired_result = FilterError::InvalidArgumentCount("expected 0, 1 given".to_owned()); + let desired_result = FilterError::InvalidArgumentCount("expected at most 0, 1 given" + .to_owned()); assert_eq!(failed!(rstrip, input, args), desired_result); } @@ -1475,7 +1628,8 @@ mod tests { fn unit_strip_one_argument() { let input = &tos!(" \n \r test \n \r "); let args = &[Num(0f32)]; - let desired_result = FilterError::InvalidArgumentCount("expected 0, 1 given".to_owned()); + let desired_result = FilterError::InvalidArgumentCount("expected at most 0, 1 given" + .to_owned()); assert_eq!(failed!(strip, input, args), desired_result); } @@ -1556,7 +1710,8 @@ mod tests { fn unit_strip_newlines_one_argument() { let input = &tos!("ab\n"); let args = &[Num(0f32)]; - let desired_result = FilterError::InvalidArgumentCount("expected 0, 1 given".to_owned()); + let desired_result = FilterError::InvalidArgumentCount("expected at most 0, 1 given" + .to_owned()); assert_eq!(failed!(strip_newlines, input, args), desired_result); } @@ -1631,8 +1786,8 @@ mod tests { fn unit_truncate_three_arguments() { let input = &tos!("I often quote myself. It adds spice to my conversation."); let args = &[Num(17f32), tos!("..."), Num(0f32)]; - let desired_result = - FilterError::InvalidArgumentCount("expected one or two arguments, 3 given".to_string()); + let desired_result = FilterError::InvalidArgumentCount("expected at most 2, 3 given" + .to_string()); assert_eq!(failed!(truncate, input, args), desired_result); } @@ -1718,7 +1873,8 @@ mod tests { fn unit_uniq_one_argument() { let input = &Array(vec![tos!("a"), tos!("b"), tos!("a")]); let args = &[Num(0f32)]; - let desired_result = FilterError::InvalidArgumentCount("expected 0, 1 given".to_string()); + let desired_result = FilterError::InvalidArgumentCount("expected at most 0, 1 given" + .to_string()); assert_eq!(failed!(uniq, input, args), desired_result); } diff --git a/src/lib.rs b/src/lib.rs index 86fe6ba16..319a47a0f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,6 +35,7 @@ extern crate regex; extern crate chrono; extern crate unicode_segmentation; extern crate itertools; +extern crate url; #[macro_use] extern crate lazy_static; diff --git a/src/template.rs b/src/template.rs index a0a8e94ad..e5597619c 100644 --- a/src/template.rs +++ b/src/template.rs @@ -1,17 +1,8 @@ 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, 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 filters; use error::Result; -#[cfg(feature = "extra-filters")] -use filters::pluralize; -#[cfg(feature = "extra-filters")] -use filters::date_in_tz; - pub struct Template { pub elements: Vec>, } @@ -19,50 +10,55 @@ pub struct Template { impl Renderable for Template { fn render(&self, context: &mut Context) -> Result> { - context.maybe_add_filter("abs", Box::new(abs)); - context.maybe_add_filter("append", Box::new(append)); - context.maybe_add_filter("capitalize", Box::new(capitalize)); - context.maybe_add_filter("ceil", Box::new(ceil)); - context.maybe_add_filter("date", Box::new(date)); - context.maybe_add_filter("default", Box::new(default)); - context.maybe_add_filter("divided_by", Box::new(divided_by)); - context.maybe_add_filter("downcase", Box::new(downcase)); - context.maybe_add_filter("escape", Box::new(escape)); - context.maybe_add_filter("escape_once", Box::new(escape_once)); - context.maybe_add_filter("first", Box::new(first)); - context.maybe_add_filter("floor", Box::new(floor)); - context.maybe_add_filter("join", Box::new(join)); - context.maybe_add_filter("last", Box::new(last)); - context.maybe_add_filter("lstrip", Box::new(lstrip)); - 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("plus", Box::new(plus)); - context.maybe_add_filter("prepend", Box::new(prepend)); - context.maybe_add_filter("remove", Box::new(remove)); - context.maybe_add_filter("remove_first", Box::new(remove_first)); - context.maybe_add_filter("replace", Box::new(replace)); - context.maybe_add_filter("replace_first", Box::new(replace_first)); - context.maybe_add_filter("reverse", Box::new(reverse)); - context.maybe_add_filter("round", Box::new(round)); - context.maybe_add_filter("rstrip", Box::new(rstrip)); - context.maybe_add_filter("size", Box::new(size)); - context.maybe_add_filter("slice", Box::new(slice)); - context.maybe_add_filter("sort", Box::new(sort)); - context.maybe_add_filter("split", Box::new(split)); - context.maybe_add_filter("strip", Box::new(strip)); - context.maybe_add_filter("strip_html", Box::new(strip_html)); - context.maybe_add_filter("strip_newlines", Box::new(strip_newlines)); - context.maybe_add_filter("times", Box::new(times)); - context.maybe_add_filter("truncate", Box::new(truncate)); - context.maybe_add_filter("truncatewords", Box::new(truncatewords)); - context.maybe_add_filter("uniq", Box::new(uniq)); - context.maybe_add_filter("upcase", Box::new(upcase)); + context.maybe_add_filter("abs", Box::new(filters::abs)); + context.maybe_add_filter("append", Box::new(filters::append)); + context.maybe_add_filter("capitalize", Box::new(filters::capitalize)); + context.maybe_add_filter("ceil", Box::new(filters::ceil)); + context.maybe_add_filter("compact", Box::new(filters::compact)); + context.maybe_add_filter("concat", Box::new(filters::concat)); + context.maybe_add_filter("date", Box::new(filters::date)); + context.maybe_add_filter("default", Box::new(filters::default)); + context.maybe_add_filter("divided_by", Box::new(filters::divided_by)); + context.maybe_add_filter("downcase", Box::new(filters::downcase)); + context.maybe_add_filter("escape", Box::new(filters::escape)); + context.maybe_add_filter("escape_once", Box::new(filters::escape_once)); + context.maybe_add_filter("first", Box::new(filters::first)); + context.maybe_add_filter("floor", Box::new(filters::floor)); + context.maybe_add_filter("join", Box::new(filters::join)); + context.maybe_add_filter("last", Box::new(filters::last)); + context.maybe_add_filter("lstrip", Box::new(filters::lstrip)); + context.maybe_add_filter("map", Box::new(filters::map)); + context.maybe_add_filter("minus", Box::new(filters::minus)); + context.maybe_add_filter("modulo", Box::new(filters::modulo)); + context.maybe_add_filter("newline_to_br", Box::new(filters::newline_to_br)); + context.maybe_add_filter("plus", Box::new(filters::plus)); + context.maybe_add_filter("prepend", Box::new(filters::prepend)); + context.maybe_add_filter("remove", Box::new(filters::remove)); + context.maybe_add_filter("remove_first", Box::new(filters::remove_first)); + context.maybe_add_filter("replace", Box::new(filters::replace)); + context.maybe_add_filter("replace_first", Box::new(filters::replace_first)); + context.maybe_add_filter("reverse", Box::new(filters::reverse)); + context.maybe_add_filter("round", Box::new(filters::round)); + context.maybe_add_filter("rstrip", Box::new(filters::rstrip)); + context.maybe_add_filter("size", Box::new(filters::size)); + context.maybe_add_filter("slice", Box::new(filters::slice)); + context.maybe_add_filter("sort", Box::new(filters::sort)); + context.maybe_add_filter("split", Box::new(filters::split)); + context.maybe_add_filter("strip", Box::new(filters::strip)); + context.maybe_add_filter("strip_html", Box::new(filters::strip_html)); + context.maybe_add_filter("strip_newlines", Box::new(filters::strip_newlines)); + context.maybe_add_filter("times", Box::new(filters::times)); + context.maybe_add_filter("truncate", Box::new(filters::truncate)); + context.maybe_add_filter("truncatewords", Box::new(filters::truncatewords)); + context.maybe_add_filter("uniq", Box::new(filters::uniq)); + context.maybe_add_filter("upcase", Box::new(filters::upcase)); + context.maybe_add_filter("url_decode", Box::new(filters::url_decode)); + context.maybe_add_filter("url_encode", Box::new(filters::url_encode)); #[cfg(feature = "extra-filters")] - context.maybe_add_filter("pluralize", Box::new(pluralize)); + context.maybe_add_filter("pluralize", Box::new(filters::pluralize)); #[cfg(feature = "extra-filters")] - context.maybe_add_filter("date_in_tz", Box::new(date_in_tz)); + context.maybe_add_filter("date_in_tz", Box::new(filters::date_in_tz)); let mut buf = String::new(); for el in &self.elements {