liquid_lib/stdlib/filters/
url.rs

1use liquid_core::Result;
2use liquid_core::Runtime;
3use liquid_core::{Display_filter, Filter, FilterReflection, ParseFilter};
4use liquid_core::{Value, ValueView};
5
6use crate::invalid_input;
7
8const FRAGMENT: &percent_encoding::AsciiSet = &percent_encoding::NON_ALPHANUMERIC
9    .remove(b'-')
10    .remove(b'.')
11    .remove(b'_');
12
13#[derive(Clone, ParseFilter, FilterReflection)]
14#[filter(
15    name = "url_encode",
16    description = "Converts any URL-unsafe characters in a string into percent-encoded characters.",
17    parsed(UrlEncodeFilter)
18)]
19pub struct UrlEncode;
20
21#[derive(Debug, Default, Display_filter)]
22#[name = "url_encode"]
23struct UrlEncodeFilter;
24
25impl Filter for UrlEncodeFilter {
26    fn evaluate(&self, input: &dyn ValueView, _runtime: &dyn Runtime) -> Result<Value> {
27        if input.is_nil() {
28            return Ok(Value::Nil);
29        }
30
31        let s = input.to_kstr();
32
33        let result: String = percent_encoding::utf8_percent_encode(s.as_str(), FRAGMENT).collect();
34        Ok(Value::scalar(result))
35    }
36}
37
38#[derive(Clone, ParseFilter, FilterReflection)]
39#[filter(
40    name = "url_decode",
41    description = "Decodes a string that has been encoded as a URL or by url_encode.",
42    parsed(UrlDecodeFilter)
43)]
44pub struct UrlDecode;
45
46#[derive(Debug, Default, Display_filter)]
47#[name = "url_decode"]
48struct UrlDecodeFilter;
49
50impl Filter for UrlDecodeFilter {
51    fn evaluate(&self, input: &dyn ValueView, _runtime: &dyn Runtime) -> Result<Value> {
52        if input.is_nil() {
53            return Ok(Value::Nil);
54        }
55
56        let s = input.to_kstr().replace('+', " ");
57
58        let result = percent_encoding::percent_decode(s.as_bytes())
59            .decode_utf8()
60            .map_err(|_| invalid_input("Malformed UTF-8"))?
61            .into_owned();
62        Ok(Value::scalar(result))
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn unit_url_encode() {
72        assert_eq!(
73            liquid_core::call_filter!(UrlEncode, "foo bar").unwrap(),
74            liquid_core::value!("foo%20bar")
75        );
76        assert_eq!(
77            liquid_core::call_filter!(UrlEncode, "foo+1@example.com").unwrap(),
78            liquid_core::value!("foo%2B1%40example.com")
79        );
80    }
81
82    #[test]
83    fn unit_url_decode() {
84        // TODO Test case from shopify/liquid that we aren't handling:
85        // - assert_eq!(
86        //      liquid_core::call_filter!(url_decode, "foo+bar").unwrap(),
87        //      liquid_core::value!("foo bar")
88        //  );
89        assert_eq!(
90            liquid_core::call_filter!(UrlDecode, "foo%20bar").unwrap(),
91            liquid_core::value!("foo bar")
92        );
93        assert_eq!(
94            liquid_core::call_filter!(UrlDecode, "foo%2B1%40example.com").unwrap(),
95            liquid_core::value!("foo+1@example.com")
96        );
97    }
98}