diff --git a/src/filters.rs b/src/filters.rs index 48de4866f..b40c78af1 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -117,35 +117,29 @@ 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::() { - 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)), + _ => Err(InvalidType("String, Array or Object expected".to_owned())), } } -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.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?; + 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.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?; + Ok(Str(s.to_uppercase())) } pub fn capitalize(input: &Value, args: &[Value]) -> FilterResult { @@ -161,106 +155,154 @@ 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)); - - let n = input.as_float().ok_or_else(|| InvalidType("Number expected".to_owned()))?; - Ok(Num(n.ceil())) +pub fn escape(input: &Value, args: &[Value]) -> FilterResult { + _escape(input, args, false) } -pub fn date(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 1)); - - 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()))?; - - Ok(Value::Str(date.format(format).to_string())) +pub fn escape_once(input: &Value, args: &[Value]) -> FilterResult { + _escape(input, args, true) } -#[cfg(feature = "extra-filters")] -pub fn date_in_tz(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 2)); +// Missing: 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)))?; +// Missing: url_decode - 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); +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, + }; - Ok(Value::Str(date.with_timezone(&timezone).format(format).to_string())) + 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())) } -pub fn divided_by(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 x = args[0].as_float().ok_or_else(|| InvalidArgument(0, "Number expected".to_owned()))?; +/// `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()))); + } - Ok(Num((n / x).floor())) -} + let num_chars = match args.first() { + Some(&Num(x)) if x > 0f32 => x as usize, + _ => return Err(InvalidArgument(0, "Positive number expected".to_string())), + }; -pub fn downcase(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 0)); + let ellipsis = Value::str("..."); + let append = args.get(1) + .unwrap_or(&ellipsis) + .as_str() + .ok_or_else(|| InvalidArgument(1, "String expected".to_owned()))?; let s = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?; - Ok(Str(s.to_lowercase())) -} -pub fn escape(input: &Value, args: &[Value]) -> FilterResult { - _escape(input, args, false) -} - -pub fn escape_once(input: &Value, args: &[Value]) -> FilterResult { - _escape(input, args, true) + Ok(Str(UnicodeSegmentation::graphemes(s, true) + .take(num_chars - append.len()) + .collect::>() + .join("") + .to_string() + append)) } -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())), +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 floor(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 0)); + 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 n = input.as_float().ok_or_else(|| InvalidType("Number expected".to_owned()))?; - Ok(Num(n.floor())) + let empty = Value::str(""); + let append = args.get(1) + .unwrap_or(&empty) + .as_str() + .ok_or_else(|| InvalidArgument(1, "String expected".to_owned()))?; + + let x = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?; + + let words: Vec<&str> = x.split(' ').take(num_words).collect(); + let mut result = words.join(" "); + if *x != result { + result = result + append; + } + Ok(Str(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 string_to_split = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?; - let join_string = args[0].as_str() + let split_string = args[0].as_str() .ok_or_else(|| InvalidArgument(0, "String expected".to_owned()))?; - Ok(Str(itertools::join(strings_to_join, join_string))) + + // Split and construct resulting Array + Ok(Array(string_to_split.split(split_string) + .map(|x| Str(String::from(x))) + .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 s = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?; + Ok(Str(s.trim().to_string())) } /// Removes all whitespaces (tabs, spaces, and newlines) from the beginning of a string. @@ -276,87 +318,99 @@ pub fn lstrip(input: &Value, args: &[Value]) -> FilterResult { Ok(Str(s.trim_left().to_string())) } -pub fn minus(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 x = args[0].as_float().ok_or_else(|| InvalidArgument(0, "Number expected".to_owned()))?; +/// 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)); - Ok(Num(n - x)) + let s = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?; + Ok(Str(s.trim_right().to_string())) } -pub fn modulo(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 1)); - - let n = input.as_float().ok_or_else(|| InvalidType("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)").unwrap(), + Regex::new(r"(?is)").unwrap(), + Regex::new(r"(?is)").unwrap(), + Regex::new(r"(?is)<.*?>").unwrap()]; + } + try!(check_args_len(args, 0)); - let x = args[0].as_float().ok_or_else(|| InvalidArgument(0, "Number expected".to_owned()))?; + let x = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?; - Ok(Num(n % x)) + let result = MATCHERS.iter() + .fold(x.to_string(), + |acc, &ref matcher| matcher.replace_all(&acc, "").into_owned()); + Ok(Str(result)) } -/// Replaces every newline (`\n`) with an HTML line break (`
`). -pub fn newline_to_br(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)); let s = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?; - Ok(Str(s.replace("\n", "
"))) -} - -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()) - } + Ok(Str(s.replace("\n", ""))) } -pub fn plus(input: &Value, args: &[Value]) -> FilterResult { +pub fn join(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 x = args[0].as_float().ok_or_else(|| InvalidArgument(0, "Number expected".to_owned()))?; + 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()); - Ok(Num(n + x)) + 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))) } -pub fn prepend(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 a = args[0].as_str() - .ok_or_else(|| InvalidArgument(0, "String expected".to_owned()))?; +pub fn sort(input: &Value, args: &[Value]) -> FilterResult { + try!(check_args_len(args, 0)); - Ok(Str(format!("{}{}", a, x))) + 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)) } -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()))?; +// Missing: sort_natural - let a = args[0].as_str() - .ok_or_else(|| InvalidArgument(0, "String expected".to_owned()))?; +/// 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)); - Ok(Str(x.replace(a, ""))) + let array = input.as_array() + .ok_or_else(|| InvalidType("Array expected".to_owned()))?; + let mut deduped: Vec = Vec::new(); + for x in array.iter() { + if !deduped.contains(x) { + deduped.push(x.clone()) + } + } + Ok(Value::Array(deduped)) } -pub fn remove_first(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 x = input.as_str().ok_or_else(|| InvalidType("String 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 a = args[0].as_str() - .ok_or_else(|| InvalidArgument(0, "String expected".to_owned()))?; +// Missing: map - Ok(Str(x.splitn(2, a).collect())) -} +// Missing: compact pub fn replace(input: &Value, args: &[Value]) -> FilterResult { try!(check_args_len(args, 2)); @@ -390,137 +444,130 @@ pub fn replace_first(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)); +pub fn remove(input: &Value, args: &[Value]) -> FilterResult { + try!(check_args_len(args, 1)); - 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 = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?; -pub fn round(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 0)); + let a = args[0].as_str() + .ok_or_else(|| InvalidArgument(0, "String expected".to_owned()))?; - let n = input.as_float().ok_or_else(|| InvalidType("Number expected".to_owned()))?; - Ok(Num(n.round())) + Ok(Str(x.replace(a, ""))) } -/// 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 s = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?; - Ok(Str(s.trim_right().to_string())) -} +pub fn remove_first(input: &Value, args: &[Value]) -> FilterResult { + try!(check_args_len(args, 1)); -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 x = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?; -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, - }; + let a = args[0].as_str() + .ok_or_else(|| InvalidArgument(0, "String expected".to_owned()))?; - 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(x.splitn(2, a).collect())) } -pub fn sort(input: &Value, args: &[Value]) -> FilterResult { - try!(check_args_len(args, 0)); +pub fn append(input: &Value, args: &[Value]) -> FilterResult { + try!(check_args_len(args, 1)); - 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)) + let x = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?; + + let a = args[0].as_str() + .ok_or_else(|| InvalidArgument(0, "String expected".to_owned()))?; + + Ok(Str(format!("{}{}", x, a))) } -pub fn split(input: &Value, args: &[Value]) -> FilterResult { +// Missing: concat + +pub fn prepend(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 x = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?; - let split_string = args[0].as_str() + let a = args[0].as_str() .ok_or_else(|| InvalidArgument(0, "String expected".to_owned()))?; - // Split and construct resulting Array - Ok(Array(string_to_split.split(split_string) - .map(|x| Str(String::from(x))) - .collect())) + Ok(Str(format!("{}{}", a, x))) } -/// 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 { +/// 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)); let s = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?; - Ok(Str(s.trim().to_string())) + Ok(Str(s.replace("\n", "
"))) } -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)").unwrap(), - Regex::new(r"(?is)").unwrap(), - Regex::new(r"(?is)").unwrap(), - Regex::new(r"(?is)<.*?>").unwrap()]; +pub fn date(input: &Value, args: &[Value]) -> FilterResult { + try!(check_args_len(args, 1)); + + 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()))?; + + Ok(Value::Str(date.format(format).to_string())) +} + +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())), + } +} + +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())), } +} + +/// 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::() { + 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())), + } +} - let x = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?; +pub fn plus(input: &Value, args: &[Value]) -> FilterResult { + try!(check_args_len(args, 1)); - let result = MATCHERS.iter() - .fold(x.to_string(), - |acc, &ref matcher| matcher.replace_all(&acc, "").into_owned()); - Ok(Str(result)) + let n = 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()))?; + + Ok(Num(n + x)) } -/// 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 s = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?; - Ok(Str(s.replace("\n", ""))) + let n = 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()))?; + + Ok(Num(n - x)) } pub fn times(input: &Value, args: &[Value]) -> FilterResult { @@ -533,107 +580,45 @@ pub fn times(input: &Value, args: &[Value]) -> FilterResult { Ok(Num(n * x)) } -/// `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()))); - } - - let num_chars = match args.first() { - Some(&Num(x)) if x > 0f32 => x as usize, - _ => return Err(InvalidArgument(0, "Positive number expected".to_string())), - }; +pub fn divided_by(input: &Value, args: &[Value]) -> FilterResult { + try!(check_args_len(args, 1)); - let ellipsis = Value::str("..."); - let append = args.get(1) - .unwrap_or(&ellipsis) - .as_str() - .ok_or_else(|| InvalidArgument(1, "String expected".to_owned()))?; + let n = input.as_float().ok_or_else(|| InvalidType("Number expected".to_owned()))?; - let s = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?; + let x = 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::>() - .join("") - .to_string() + append)) + Ok(Num((n / x).floor())) } -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 modulo(input: &Value, args: &[Value]) -> FilterResult { + try!(check_args_len(args, 1)); - 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 n = input.as_float().ok_or_else(|| InvalidType("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 x = args[0].as_float().ok_or_else(|| InvalidArgument(0, "Number expected".to_owned()))?; - let x = input.as_str().ok_or_else(|| InvalidType("String expected".to_owned()))?; + Ok(Num(n % x)) +} - let words: Vec<&str> = x.split(' ').take(num_words).collect(); - let mut result = words.join(" "); - if *x != result { - result = result + append; - } - Ok(Str(result)) +pub fn round(input: &Value, args: &[Value]) -> FilterResult { + try!(check_args_len(args, 0)); + + let n = input.as_float().ok_or_else(|| InvalidType("Number expected".to_owned()))?; + Ok(Num(n.round())) } -/// 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 = 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 +639,38 @@ 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 { @@ -1063,6 +1080,7 @@ mod tests { } #[test] + #[cfg(feature = "extra-filters")] fn unit_pluralize() { assert_eq!(unit!(pluralize, Num(1f32), &[tos!("one"), tos!("many")]), tos!("one")); diff --git a/src/template.rs b/src/template.rs index 3df45501a..1e32dd9e3 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/tests/filters.rs b/tests/filters.rs index 27b6994ea..0bddbd3ca 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();