liquid_lib/stdlib/filters/
slice.rs

1use 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};
10
11use crate::invalid_argument;
12
13fn canonicalize_slice(
14    slice_offset: isize,
15    slice_length: isize,
16    vec_length: usize,
17) -> (usize, usize) {
18    let vec_length = vec_length as isize;
19
20    // Cap slice_offset
21    let slice_offset = cmp::min(slice_offset, vec_length);
22    // Reverse indexing
23    let slice_offset = if slice_offset < 0 {
24        slice_offset + vec_length
25    } else {
26        slice_offset
27    };
28
29    // Cap slice_length
30    let slice_length = if slice_offset + slice_length > vec_length {
31        vec_length - slice_offset
32    } else {
33        slice_length
34    };
35
36    (slice_offset as usize, slice_length as usize)
37}
38
39#[derive(Debug, FilterParameters)]
40struct SliceArgs {
41    #[parameter(description = "The offset of the slice.", arg_type = "integer")]
42    offset: Expression,
43
44    #[parameter(description = "The length of the slice.", arg_type = "integer")]
45    length: Option<Expression>,
46}
47
48#[derive(Clone, ParseFilter, FilterReflection)]
49#[filter(
50    name = "slice",
51    description = "Takes a slice of a given string or array.",
52    parameters(SliceArgs),
53    parsed(SliceFilter)
54)]
55pub struct Slice;
56
57#[derive(Debug, FromFilterParameters, Display_filter)]
58#[name = "slice"]
59struct SliceFilter {
60    #[parameters]
61    args: SliceArgs,
62}
63
64impl Filter for SliceFilter {
65    fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
66        let args = self.args.evaluate(runtime)?;
67
68        let offset = args.offset as isize;
69        let length = args.length.unwrap_or(1) as isize;
70
71        if length < 1 {
72            return invalid_argument("length", "Positive number expected").into_err();
73        }
74
75        if let Some(input) = input.as_array() {
76            let (offset, length) = canonicalize_slice(offset, length, input.size() as usize);
77            Ok(Value::array(
78                input
79                    .values()
80                    .skip(offset)
81                    .take(length)
82                    .map(|s| s.to_value()),
83            ))
84        } else {
85            let input = input.to_kstr();
86            let (offset, length) = canonicalize_slice(offset, length, input.len());
87            Ok(Value::scalar(
88                input.chars().skip(offset).take(length).collect::<String>(),
89            ))
90        }
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn unit_slice() {
100        assert_eq!(
101            liquid_core::call_filter!(
102                Slice,
103                "I often quote myself.  It adds spice to my conversation.",
104                2,
105                3
106            )
107            .unwrap(),
108            liquid_core::value!("oft")
109        );
110    }
111
112    #[test]
113    fn unit_slice_no_length_specified() {
114        assert_eq!(
115            liquid_core::call_filter!(
116                Slice,
117                "I often quote myself.  It adds spice to my conversation.",
118                4
119            )
120            .unwrap(),
121            liquid_core::value!("t")
122        );
123    }
124
125    #[test]
126    fn unit_slice_negative_offset() {
127        assert_eq!(
128            liquid_core::call_filter!(
129                Slice,
130                "I often quote myself.  It adds spice to my conversation.",
131                -10,
132                3
133            )
134            .unwrap(),
135            liquid_core::value!("ver")
136        );
137    }
138
139    #[test]
140    fn unit_slice_non_positive_length() {
141        liquid_core::call_filter!(
142            Slice,
143            "I often quote myself.  It adds spice to my conversation.",
144            -10,
145            0
146        )
147        .unwrap_err();
148        liquid_core::call_filter!(
149            Slice,
150            "I often quote myself.  It adds spice to my conversation.",
151            -10,
152            -1
153        )
154        .unwrap_err();
155    }
156}