loose_liquid_lib/stdlib/filters/string/
truncate.rs1use std::cmp;
2
3use liquid_core::Expression;
4use liquid_core::Result;
5use liquid_core::Runtime;
6use liquid_core::{
7 Display_filter, Filter, FilterParameters, FilterReflection, FromFilterParameters, ParseFilter,
8};
9use liquid_core::{Value, ValueView};
10use unicode_segmentation::UnicodeSegmentation;
11
12#[derive(Debug, FilterParameters)]
13struct TruncateArgs {
14 #[parameter(
15 description = "The maximum length of the string, after which it will be truncated.",
16 arg_type = "integer"
17 )]
18 length: Option<Expression>,
19
20 #[parameter(
21 description = "The text appended to the end of the string if it is truncated. This text counts to the maximum length of the string. Defaults to \"...\".",
22 arg_type = "str"
23 )]
24 ellipsis: Option<Expression>,
25}
26
27#[derive(Clone, ParseFilter, FilterReflection)]
54#[filter(
55 name = "truncate",
56 description = "Shortens a string down to the number of characters passed as a parameter.",
57 parameters(TruncateArgs),
58 parsed(TruncateFilter)
59)]
60pub struct Truncate;
61
62#[derive(Debug, FromFilterParameters, Display_filter)]
63#[name = "truncate"]
64struct TruncateFilter {
65 #[parameters]
66 args: TruncateArgs,
67}
68
69impl Filter for TruncateFilter {
70 fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
71 let args = self.args.evaluate(runtime)?;
72
73 let length = args.length.unwrap_or(50) as usize;
74 let truncate_string = args.ellipsis.unwrap_or_else(|| "...".into());
75 let diff = if length >= truncate_string.len() {
76 length - truncate_string.len()
77 } else {
78 0
79 };
80 let l = cmp::max(diff, 0);
81
82 let input_string = input.to_kstr();
83 let result = if length < input_string.len() {
84 let result = UnicodeSegmentation::graphemes(input_string.as_str(), true)
85 .take(l)
86 .collect::<Vec<&str>>()
87 .join("")
88 + truncate_string.as_str();
89 Value::scalar(result)
90 } else {
91 input.to_value()
92 };
93 Ok(result)
94 }
95}
96
97#[derive(Debug, FilterParameters)]
98struct TruncateWordsArgs {
99 #[parameter(
100 description = "The maximum number of words, after which the string will be truncated.",
101 arg_type = "integer"
102 )]
103 length: Option<Expression>,
104
105 #[parameter(
106 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 \"...\".",
107 arg_type = "str"
108 )]
109 ellipsis: Option<Expression>,
110}
111
112#[derive(Clone, ParseFilter, FilterReflection)]
113#[filter(
114 name = "truncatewords",
115 description = "Shortens a string down to the number of characters passed as a parameter.",
116 parameters(TruncateWordsArgs),
117 parsed(TruncateWordsFilter)
118)]
119pub struct TruncateWords;
120
121#[derive(Debug, FromFilterParameters, Display_filter)]
122#[name = "truncate"]
123struct TruncateWordsFilter {
124 #[parameters]
125 args: TruncateWordsArgs,
126}
127
128impl Filter for TruncateWordsFilter {
129 fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
130 let args = self.args.evaluate(runtime)?;
131
132 let words = args.length.unwrap_or(50) as usize;
133
134 let truncate_string = args.ellipsis.unwrap_or_else(|| "...".into());
135
136 let l = cmp::max(words, 0);
137
138 let input_string = input.to_kstr();
139
140 let word_list: Vec<&str> = input_string.split(' ').collect();
141 let result = if words < word_list.len() {
142 let result = itertools::join(word_list.iter().take(l), " ") + truncate_string.as_str();
143 Value::scalar(result)
144 } else {
145 input.to_value()
146 };
147 Ok(result)
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn unit_truncate() {
157 assert_eq!(
158 liquid_core::call_filter!(
159 Truncate,
160 "I often quote myself. It adds spice to my conversation.",
161 17i64
162 )
163 .unwrap(),
164 liquid_core::value!("I often quote ...")
165 );
166 }
167
168 #[test]
169 fn unit_truncate_negative_length() {
170 assert_eq!(
171 liquid_core::call_filter!(
172 Truncate,
173 "I often quote myself. It adds spice to my conversation.",
174 -17i64
175 )
176 .unwrap(),
177 liquid_core::value!("I often quote myself. It adds spice to my conversation.")
178 );
179 }
180
181 #[test]
182 fn unit_truncate_non_string() {
183 assert_eq!(
184 liquid_core::call_filter!(Truncate, 10000000f64, 5i64).unwrap(),
185 liquid_core::value!("10...")
186 );
187 }
188
189 #[test]
190 fn unit_truncate_shopify_liquid() {
191 let input = "Ground control to Major Tom.";
193
194 assert_eq!(
195 liquid_core::call_filter!(Truncate, input, 20i64).unwrap(),
196 liquid_core::value!("Ground control to...")
197 );
198
199 assert_eq!(
200 liquid_core::call_filter!(Truncate, input, 25i64, ", and so on").unwrap(),
201 liquid_core::value!("Ground control, and so on")
202 );
203
204 assert_eq!(
205 liquid_core::call_filter!(Truncate, input, 20i64, "").unwrap(),
206 liquid_core::value!("Ground control to Ma")
207 );
208 }
209
210 #[test]
211 fn unit_truncate_three_arguments() {
212 liquid_core::call_filter!(
213 Truncate,
214 "I often quote myself. It adds spice to my conversation.",
215 17i64,
216 "...",
217 0i64
218 )
219 .unwrap_err();
220 }
221
222 #[test]
223 fn unit_truncate_unicode_codepoints_examples() {
224 assert_eq!(
232 liquid_core::call_filter!(
233 Truncate,
234 "Here is an a\u{310}, e\u{301}, and o\u{308}\u{332}.",
235 20i64
236 )
237 .unwrap(),
238 liquid_core::value!("Here is an a\u{310}, e\u{301}, ...")
239 );
240
241 assert_eq!(
243 liquid_core::call_filter!(Truncate, "Here is a RUST: π·πΊπΈπΉ.", 20i64).unwrap(),
244 liquid_core::value!("Here is a RUST: π·πΊ...")
245 );
246 }
247
248 #[test]
249 fn unit_truncate_zero_arguments() {
250 assert_eq!(
251 liquid_core::call_filter!(
252 Truncate,
253 "I often quote myself. It adds spice to my conversation."
254 )
255 .unwrap(),
256 liquid_core::value!("I often quote myself. It adds spice to my conv...")
257 );
258 }
259
260 #[test]
261 fn unit_truncatewords_negative_length() {
262 assert_eq!(
263 liquid_core::call_filter!(TruncateWords, "one two three", -1_i64).unwrap(),
264 liquid_core::value!("one two three")
265 );
266 }
267
268 #[test]
269 fn unit_truncatewords_zero_length() {
270 assert_eq!(
271 liquid_core::call_filter!(TruncateWords, "one two three", 0_i64).unwrap(),
272 liquid_core::value!("...")
273 );
274 }
275
276 #[test]
277 fn unit_truncatewords_no_truncation() {
278 assert_eq!(
279 liquid_core::call_filter!(TruncateWords, "one two three", 4_i64).unwrap(),
280 liquid_core::value!("one two three")
281 );
282 }
283
284 #[test]
285 fn unit_truncatewords_truncate() {
286 assert_eq!(
287 liquid_core::call_filter!(TruncateWords, "one two three", 2_i64).unwrap(),
288 liquid_core::value!("one two...")
289 );
290 assert_eq!(
291 liquid_core::call_filter!(TruncateWords, "one two three", 2_i64, 1_i64).unwrap(),
292 liquid_core::value!("one two1")
293 );
294 }
295
296 #[test]
297 fn unit_truncatewords_empty_string() {
298 assert_eq!(
299 liquid_core::call_filter!(TruncateWords, "", 1_i64).unwrap(),
300 liquid_core::value!("")
301 );
302 }
303}