liquid_lib/jekyll/
array.rs

1use std::cmp;
2use std::fmt::Write;
3
4use liquid_core::model::try_find;
5use liquid_core::model::KStringCow;
6use liquid_core::model::ValueViewCmp;
7use liquid_core::parser::parse_variable;
8use liquid_core::Expression;
9use liquid_core::Result;
10use liquid_core::Runtime;
11use liquid_core::ValueCow;
12use liquid_core::{
13    Display_filter, Filter, FilterParameters, FilterReflection, FromFilterParameters, ParseFilter,
14};
15use liquid_core::{Value, ValueView};
16
17use crate::invalid_input;
18
19#[derive(Debug, Default, FilterParameters)]
20struct SortArgs {
21    #[parameter(description = "The property accessed by the filter.", arg_type = "str")]
22    property: Option<Expression>,
23    #[parameter(
24        description = "nils appear before or after non-nil values, either ('first' | 'last')",
25        arg_type = "str"
26    )]
27    nils: Option<Expression>,
28}
29
30#[derive(Clone, ParseFilter, FilterReflection)]
31#[filter(
32    name = "sort",
33    description = "Sorts items in an array. The order of the sorted array is case-sensitive.",
34    parameters(SortArgs),
35    parsed(SortFilter)
36)]
37pub struct Sort;
38
39#[derive(Debug, Default, FromFilterParameters, Display_filter)]
40#[name = "sort"]
41struct SortFilter {
42    #[parameters]
43    args: SortArgs,
44}
45
46#[derive(Copy, Clone)]
47enum NilsOrder {
48    First,
49    Last,
50}
51
52fn safe_property_getter<'v>(
53    value: &'v Value,
54    property: &KStringCow<'_>,
55    runtime: &dyn Runtime,
56) -> ValueCow<'v> {
57    let variable = parse_variable(property).expect("Failed to parse variable");
58    if let Some(path) = variable.try_evaluate(runtime) {
59        try_find(value, path.as_slice()).unwrap_or(ValueCow::Borrowed(&Value::Nil))
60    } else {
61        ValueCow::Borrowed(&Value::Nil)
62    }
63}
64
65fn nil_safe_compare(
66    a: &dyn ValueView,
67    b: &dyn ValueView,
68    nils: NilsOrder,
69) -> Option<cmp::Ordering> {
70    if a.is_nil() && b.is_nil() {
71        Some(cmp::Ordering::Equal)
72    } else if a.is_nil() {
73        match nils {
74            NilsOrder::First => Some(cmp::Ordering::Less),
75            NilsOrder::Last => Some(cmp::Ordering::Greater),
76        }
77    } else if b.is_nil() {
78        match nils {
79            NilsOrder::First => Some(cmp::Ordering::Greater),
80            NilsOrder::Last => Some(cmp::Ordering::Less),
81        }
82    } else {
83        ValueViewCmp::new(a).partial_cmp(&ValueViewCmp::new(b))
84    }
85}
86
87fn as_sequence<'k>(input: &'k dyn ValueView) -> Box<dyn Iterator<Item = &'k dyn ValueView> + 'k> {
88    if let Some(array) = input.as_array() {
89        array.values()
90    } else if input.is_nil() {
91        Box::new(vec![].into_iter())
92    } else {
93        Box::new(std::iter::once(input))
94    }
95}
96
97impl Filter for SortFilter {
98    fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
99        let args = self.args.evaluate(runtime)?;
100
101        let input: Vec<_> = as_sequence(input).collect();
102        if input.is_empty() {
103            return Err(invalid_input("Non-empty array expected"));
104        }
105        if args.property.is_some() && !input.iter().all(|v| v.is_object()) {
106            return Err(invalid_input("Array of objects expected"));
107        }
108        let nils = if let Some(nils) = &args.nils {
109            match nils.to_kstr().as_str() {
110                "first" => NilsOrder::First,
111                "last" => NilsOrder::Last,
112                _ => {
113                    return Err(invalid_input(
114                        "Invalid nils order. Must be \"first\" or \"last\".",
115                    ))
116                }
117            }
118        } else {
119            NilsOrder::First
120        };
121
122        let mut sorted: Vec<Value> = input.iter().map(|v| v.to_value()).collect();
123        if let Some(property) = &args.property {
124            // Using unwrap is ok since all of the elements are objects
125            sorted.sort_by(|a, b| {
126                nil_safe_compare(
127                    safe_property_getter(a, property, runtime).as_view(),
128                    safe_property_getter(b, property, runtime).as_view(),
129                    nils,
130                )
131                .unwrap_or(cmp::Ordering::Equal)
132            });
133        } else {
134            sorted.sort_by(|a, b| nil_safe_compare(a, b, nils).unwrap_or(cmp::Ordering::Equal));
135        }
136        Ok(Value::array(sorted))
137    }
138}
139
140#[derive(Debug, FilterParameters)]
141struct PushArgs {
142    #[parameter(description = "The element to append to the array.")]
143    element: Expression,
144}
145
146#[derive(Clone, ParseFilter, FilterReflection)]
147#[filter(
148    name = "push",
149    description = "Appends the given element to the end of an array.",
150    parameters(PushArgs),
151    parsed(PushFilter)
152)]
153pub struct Push;
154
155#[derive(Debug, FromFilterParameters, Display_filter)]
156#[name = "push"]
157struct PushFilter {
158    #[parameters]
159    args: PushArgs,
160}
161
162impl Filter for PushFilter {
163    fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
164        let args = self.args.evaluate(runtime)?;
165
166        let element = args.element.to_value();
167        let mut array = input
168            .to_value()
169            .into_array()
170            .ok_or_else(|| invalid_input("Array expected"))?;
171        array.push(element);
172
173        Ok(Value::Array(array))
174    }
175}
176
177#[derive(Clone, ParseFilter, FilterReflection)]
178#[filter(
179    name = "pop",
180    description = "Removes the last element of an array.",
181    parsed(PopFilter)
182)]
183pub struct Pop;
184
185#[derive(Debug, Default, Display_filter)]
186#[name = "pop"]
187struct PopFilter;
188
189impl Filter for PopFilter {
190    fn evaluate(&self, input: &dyn ValueView, _runtime: &dyn Runtime) -> Result<Value> {
191        let mut array = input
192            .to_value()
193            .into_array()
194            .ok_or_else(|| invalid_input("Array expected"))?;
195        array.pop();
196
197        Ok(Value::Array(array))
198    }
199}
200
201#[derive(Debug, FilterParameters)]
202struct UnshiftArgs {
203    #[parameter(description = "The element to append to the array.")]
204    element: Expression,
205}
206
207#[derive(Clone, ParseFilter, FilterReflection)]
208#[filter(
209    name = "unshift",
210    description = "Appends the given element to the start of an array.",
211    parameters(UnshiftArgs),
212    parsed(UnshiftFilter)
213)]
214pub struct Unshift;
215
216#[derive(Debug, FromFilterParameters, Display_filter)]
217#[name = "unshift"]
218struct UnshiftFilter {
219    #[parameters]
220    args: UnshiftArgs,
221}
222
223impl Filter for UnshiftFilter {
224    fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
225        let args = self.args.evaluate(runtime)?;
226
227        let element = args.element.to_value();
228        let mut array = input
229            .to_value()
230            .into_array()
231            .ok_or_else(|| invalid_input("Array expected"))?;
232        array.insert(0, element);
233
234        Ok(Value::Array(array))
235    }
236}
237
238#[derive(Clone, ParseFilter, FilterReflection)]
239#[filter(
240    name = "shift",
241    description = "Removes the first element of an array.",
242    parsed(ShiftFilter)
243)]
244pub struct Shift;
245
246#[derive(Debug, Default, Display_filter)]
247#[name = "shift"]
248struct ShiftFilter;
249
250impl Filter for ShiftFilter {
251    fn evaluate(&self, input: &dyn ValueView, _runtime: &dyn Runtime) -> Result<Value> {
252        let mut array = input
253            .to_value()
254            .into_array()
255            .ok_or_else(|| invalid_input("Array expected"))?;
256
257        if !array.is_empty() {
258            array.remove(0);
259        }
260
261        Ok(Value::Array(array))
262    }
263}
264
265#[derive(Debug, FilterParameters)]
266struct ArrayToSentenceStringArgs {
267    #[parameter(
268        description = "The connector between the last two elements. Defaults to \"and\".",
269        arg_type = "str"
270    )]
271    connector: Option<Expression>,
272}
273
274#[derive(Clone, ParseFilter, FilterReflection)]
275#[filter(
276    name = "array_to_sentence_string",
277    description = "Converts an array into a sentence. This sentence will be a list of the elements of the array separated by comma, with a connector between the last two elements.",
278    parameters(ArrayToSentenceStringArgs),
279    parsed(ArrayToSentenceStringFilter)
280)]
281pub struct ArrayToSentenceString;
282
283#[derive(Debug, FromFilterParameters, Display_filter)]
284#[name = "array_to_sentence_string"]
285struct ArrayToSentenceStringFilter {
286    #[parameters]
287    args: ArrayToSentenceStringArgs,
288}
289
290impl Filter for ArrayToSentenceStringFilter {
291    fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
292        let args = self.args.evaluate(runtime)?;
293
294        let connector = args.connector.unwrap_or_else(|| "and".into());
295
296        let mut array = input
297            .as_array()
298            .ok_or_else(|| invalid_input("Array expected"))?
299            .values();
300
301        let mut sentence = array
302            .next()
303            .map(|v| v.to_kstr().into_string())
304            .unwrap_or_else(|| "".to_owned());
305
306        let mut iter = array.peekable();
307        while let Some(value) = iter.next() {
308            if iter.peek().is_some() {
309                write!(sentence, ", {}", value.render())
310                    .expect("It should be safe to write to a string.");
311            } else {
312                write!(sentence, ", {} {}", connector, value.render())
313                    .expect("It should be safe to write to a string.");
314            }
315        }
316
317        Ok(Value::scalar(sentence))
318    }
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324
325    #[test]
326    fn unit_sort() {
327        let input = &liquid_core::value!(["Z", "b", "c", "a"]);
328        let desired_result = liquid_core::value!(["Z", "a", "b", "c"]);
329        assert_eq!(
330            liquid_core::call_filter!(Sort, input).unwrap(),
331            desired_result
332        );
333    }
334
335    #[test]
336    fn unit_push() {
337        let input = liquid_core::value!(["Seattle", "Tacoma"]);
338        let unit_result = liquid_core::call_filter!(Push, input, "Spokane").unwrap();
339        let desired_result = liquid_core::value!(["Seattle", "Tacoma", "Spokane",]);
340        assert_eq!(unit_result, desired_result);
341    }
342
343    #[test]
344    fn unit_pop() {
345        let input = liquid_core::value!(["Seattle", "Tacoma"]);
346        let unit_result = liquid_core::call_filter!(Pop, input).unwrap();
347        let desired_result = liquid_core::value!(["Seattle"]);
348        assert_eq!(unit_result, desired_result);
349    }
350
351    #[test]
352    fn unit_pop_empty() {
353        let input = liquid_core::value!([]);
354        let unit_result = liquid_core::call_filter!(Pop, input).unwrap();
355        let desired_result = liquid_core::value!([]);
356        assert_eq!(unit_result, desired_result);
357    }
358
359    #[test]
360    fn unit_unshift() {
361        let input = liquid_core::value!(["Seattle", "Tacoma"]);
362        let unit_result = liquid_core::call_filter!(Unshift, input, "Olympia").unwrap();
363        let desired_result = liquid_core::value!(["Olympia", "Seattle", "Tacoma"]);
364        assert_eq!(unit_result, desired_result);
365    }
366
367    #[test]
368    fn unit_shift() {
369        let input = liquid_core::value!(["Seattle", "Tacoma"]);
370        let unit_result = liquid_core::call_filter!(Shift, input).unwrap();
371        let desired_result = liquid_core::value!(["Tacoma"]);
372        assert_eq!(unit_result, desired_result);
373    }
374
375    #[test]
376    fn unit_shift_empty() {
377        let input = liquid_core::value!([]);
378        let unit_result = liquid_core::call_filter!(Shift, input).unwrap();
379        let desired_result = liquid_core::value!([]);
380        assert_eq!(unit_result, desired_result);
381    }
382
383    #[test]
384    fn unit_array_to_sentence_string() {
385        let input = liquid_core::value!(["foo", "bar", "baz"]);
386        let unit_result = liquid_core::call_filter!(ArrayToSentenceString, input).unwrap();
387        let desired_result = "foo, bar, and baz";
388        assert_eq!(unit_result, desired_result);
389    }
390
391    #[test]
392    fn unit_array_to_sentence_string_two_elements() {
393        let input = liquid_core::value!(["foo", "bar"]);
394        let unit_result = liquid_core::call_filter!(ArrayToSentenceString, input).unwrap();
395        let desired_result = "foo, and bar";
396        assert_eq!(unit_result, desired_result);
397    }
398
399    #[test]
400    fn unit_array_to_sentence_string_one_element() {
401        let input = liquid_core::value!(["foo"]);
402        let unit_result = liquid_core::call_filter!(ArrayToSentenceString, input).unwrap();
403        let desired_result = "foo";
404        assert_eq!(unit_result, desired_result);
405    }
406
407    #[test]
408    fn unit_array_to_sentence_string_no_elements() {
409        let input = liquid_core::value!([]);
410        let unit_result = liquid_core::call_filter!(ArrayToSentenceString, input).unwrap();
411        let desired_result = "";
412        assert_eq!(unit_result, desired_result);
413    }
414
415    #[test]
416    fn unit_array_to_sentence_string_custom_connector() {
417        let input = liquid_core::value!(["foo", "bar", "baz"]);
418        let unit_result = liquid_core::call_filter!(ArrayToSentenceString, input, "or").unwrap();
419        let desired_result = "foo, bar, or baz";
420        assert_eq!(unit_result, desired_result);
421    }
422}