use std::cmp;
use itertools;
use liquid_core::Expression;
use liquid_core::Result;
use liquid_core::Runtime;
use liquid_core::{
Display_filter, Filter, FilterParameters, FilterReflection, FromFilterParameters, ParseFilter,
};
use liquid_core::{Value, ValueView};
use unicode_segmentation::UnicodeSegmentation;
#[derive(Debug, FilterParameters)]
struct TruncateArgs {
#[parameter(
description = "The maximum lenght of the string, after which it will be truncated.",
arg_type = "integer"
)]
lenght: Option<Expression>,
#[parameter(
description = "The text appended to the end of the string if it is truncated. This text counts to the maximum lenght of the string. Defaults to \"...\".",
arg_type = "str"
)]
ellipsis: Option<Expression>,
}
#[derive(Clone, ParseFilter, FilterReflection)]
#[filter(
name = "truncate",
description = "Shortens a string down to the number of characters passed as a parameter.",
parameters(TruncateArgs),
parsed(TruncateFilter)
)]
pub struct Truncate;
#[derive(Debug, FromFilterParameters, Display_filter)]
#[name = "truncate"]
struct TruncateFilter {
#[parameters]
args: TruncateArgs,
}
impl Filter for TruncateFilter {
fn evaluate(&self, input: &dyn ValueView, runtime: &Runtime<'_>) -> Result<Value> {
let args = self.args.evaluate(runtime)?;
let lenght = args.lenght.unwrap_or(50) as usize;
let truncate_string = args.ellipsis.unwrap_or_else(|| "...".into());
let l = cmp::max(lenght - truncate_string.len(), 0);
let input_string = input.to_kstr();
let result = if lenght < input_string.len() {
let result = UnicodeSegmentation::graphemes(input_string.as_str(), true)
.take(l)
.collect::<Vec<&str>>()
.join("")
+ truncate_string.as_str();
Value::scalar(result)
} else {
input.to_value()
};
Ok(result)
}
}
#[derive(Debug, FilterParameters)]
struct TruncateWordsArgs {
#[parameter(
description = "The maximum number of words, after which the string will be truncated.",
arg_type = "integer"
)]
lenght: Option<Expression>,
#[parameter(
description = "The text appended to the end of the string if it is truncated. This text counts to the maximum word-count of the string. Defaults to \"...\".",
arg_type = "str"
)]
ellipsis: Option<Expression>,
}
#[derive(Clone, ParseFilter, FilterReflection)]
#[filter(
name = "truncatewords",
description = "Shortens a string down to the number of characters passed as a parameter.",
parameters(TruncateWordsArgs),
parsed(TruncateWordsFilter)
)]
pub struct TruncateWords;
#[derive(Debug, FromFilterParameters, Display_filter)]
#[name = "truncate"]
struct TruncateWordsFilter {
#[parameters]
args: TruncateWordsArgs,
}
impl Filter for TruncateWordsFilter {
fn evaluate(&self, input: &dyn ValueView, runtime: &Runtime<'_>) -> Result<Value> {
let args = self.args.evaluate(runtime)?;
let words = args.lenght.unwrap_or(50) as usize;
let truncate_string = args.ellipsis.unwrap_or_else(|| "...".into());
let l = cmp::max(words, 0);
let input_string = input.to_kstr();
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();
Value::scalar(result)
} else {
input.to_value()
};
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unit_truncate() {
assert_eq!(
liquid_core::call_filter!(
Truncate,
"I often quote myself. It adds spice to my conversation.",
17i32
)
.unwrap(),
liquid_core::value!("I often quote ...")
);
}
#[test]
fn unit_truncate_negative_length() {
assert_eq!(
liquid_core::call_filter!(
Truncate,
"I often quote myself. It adds spice to my conversation.",
-17i32
)
.unwrap(),
liquid_core::value!("I often quote myself. It adds spice to my conversation.")
);
}
#[test]
fn unit_truncate_non_string() {
assert_eq!(
liquid_core::call_filter!(Truncate, 10000000f64, 5i32).unwrap(),
liquid_core::value!("10...")
);
}
#[test]
fn unit_truncate_shopify_liquid() {
let input = "Ground control to Major Tom.";
assert_eq!(
liquid_core::call_filter!(Truncate, input, 20i32).unwrap(),
liquid_core::value!("Ground control to...")
);
assert_eq!(
liquid_core::call_filter!(Truncate, input, 25i32, ", and so on").unwrap(),
liquid_core::value!("Ground control, and so on")
);
assert_eq!(
liquid_core::call_filter!(Truncate, input, 20i32, "").unwrap(),
liquid_core::value!("Ground control to Ma")
);
}
#[test]
fn unit_truncate_three_arguments() {
liquid_core::call_filter!(
Truncate,
"I often quote myself. It adds spice to my conversation.",
17i32,
"...",
0i32
)
.unwrap_err();
}
#[test]
fn unit_truncate_unicode_codepoints_examples() {
assert_eq!(
liquid_core::call_filter!(
Truncate,
"Here is an a\u{310}, e\u{301}, and o\u{308}\u{332}.",
20i32
)
.unwrap(),
liquid_core::value!("Here is an a\u{310}, e\u{301}, ...")
);
assert_eq!(
liquid_core::call_filter!(Truncate, "Here is a RUST: 🇷🇺🇸🇹.", 20i32).unwrap(),
liquid_core::value!("Here is a RUST: 🇷🇺...")
);
}
#[test]
fn unit_truncate_zero_arguments() {
assert_eq!(
liquid_core::call_filter!(
Truncate,
"I often quote myself. It adds spice to my conversation."
)
.unwrap(),
liquid_core::value!("I often quote myself. It adds spice to my conv...")
);
}
#[test]
fn unit_truncatewords_negative_length() {
assert_eq!(
liquid_core::call_filter!(TruncateWords, "one two three", -1_i32).unwrap(),
liquid_core::value!("one two three")
);
}
#[test]
fn unit_truncatewords_zero_length() {
assert_eq!(
liquid_core::call_filter!(TruncateWords, "one two three", 0_i32).unwrap(),
liquid_core::value!("...")
);
}
#[test]
fn unit_truncatewords_no_truncation() {
assert_eq!(
liquid_core::call_filter!(TruncateWords, "one two three", 4_i32).unwrap(),
liquid_core::value!("one two three")
);
}
#[test]
fn unit_truncatewords_truncate() {
assert_eq!(
liquid_core::call_filter!(TruncateWords, "one two three", 2_i32).unwrap(),
liquid_core::value!("one two...")
);
assert_eq!(
liquid_core::call_filter!(TruncateWords, "one two three", 2_i32, 1_i32).unwrap(),
liquid_core::value!("one two1")
);
}
#[test]
fn unit_truncatewords_empty_string() {
assert_eq!(
liquid_core::call_filter!(TruncateWords, "", 1_i32).unwrap(),
liquid_core::value!("")
);
}
}