liquid_lib/jekyll/
slugify.rs

1use liquid_core::Expression;
2use liquid_core::Result;
3use liquid_core::Runtime;
4use liquid_core::{
5    Display_filter, Filter, FilterParameters, FilterReflection, FromFilterParameters, ParseFilter,
6};
7use liquid_core::{Value, ValueView};
8use regex::Regex;
9
10#[derive(PartialEq)]
11enum SlugifyMode {
12    No,
13    Def,
14    Raw,
15    Pretty,
16    Ascii,
17    Latin,
18}
19
20impl SlugifyMode {
21    fn new(mode_str: &str) -> SlugifyMode {
22        match mode_str {
23            "none" => SlugifyMode::No,
24            "raw" => SlugifyMode::Raw,
25            "pretty" => SlugifyMode::Pretty,
26            "ascii" => SlugifyMode::Ascii,
27            "latin" => SlugifyMode::Latin,
28            _ => SlugifyMode::Def,
29        }
30    }
31}
32
33static SLUG_INVALID_CHARS_DEFAULT: std::sync::LazyLock<Regex> =
34    std::sync::LazyLock::new(|| Regex::new(r"([^0-9\p{Alphabetic}]+)").unwrap());
35static SLUG_INVALID_CHARS_RAW: std::sync::LazyLock<Regex> =
36    std::sync::LazyLock::new(|| Regex::new(r"([\s]+)").unwrap());
37static SLUG_INVALID_CHARS_PRETTY: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
38    Regex::new(r"([^\p{Alphabetic}0-9\._\~!\$&'\(\)\+,;=@]+)").unwrap()
39});
40static SLUG_INVALID_CHARS_ASCII: std::sync::LazyLock<Regex> =
41    std::sync::LazyLock::new(|| Regex::new(r"([^a-zA-Z0-9]+)").unwrap());
42
43#[derive(Debug, FilterParameters)]
44struct SlugifyArgs {
45    #[parameter(
46        description = "The slugify mode. May be \"none\", \"raw\", \"pretty\", \"ascii\", \"latin\" or \"default\".",
47        arg_type = "str"
48    )]
49    mode: Option<Expression>,
50}
51
52#[derive(Clone, ParseFilter, FilterReflection)]
53#[filter(
54    name = "slugify",
55    description = "Convert a string into a lowercase URL \"slug\".",
56    parameters(SlugifyArgs),
57    parsed(SlugifyFilter)
58)]
59pub struct Slugify;
60
61#[derive(Debug, FromFilterParameters, Display_filter)]
62#[name = "slugify"]
63struct SlugifyFilter {
64    #[parameters]
65    args: SlugifyArgs,
66}
67
68impl Filter for SlugifyFilter {
69    fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
70        let args = self.args.evaluate(runtime)?;
71
72        let s = input.to_kstr();
73        let mode = args
74            .mode
75            .map(|mode| SlugifyMode::new(mode.as_str()))
76            .unwrap_or(SlugifyMode::Def);
77
78        let s = if mode == SlugifyMode::Latin {
79            deunicode::deunicode_with_tofu(s.trim(), "-")
80        } else {
81            s.trim().to_owned()
82        };
83
84        let result = match mode {
85            SlugifyMode::No => s,
86            SlugifyMode::Def => SLUG_INVALID_CHARS_DEFAULT.replace_all(&s, "-").to_string(),
87            SlugifyMode::Raw => SLUG_INVALID_CHARS_RAW.replace_all(&s, "-").to_string(),
88            SlugifyMode::Pretty => SLUG_INVALID_CHARS_PRETTY.replace_all(&s, "-").to_string(),
89            SlugifyMode::Ascii | SlugifyMode::Latin => {
90                SLUG_INVALID_CHARS_ASCII.replace_all(&s, "-").to_string()
91            }
92        };
93
94        Ok(Value::scalar(result.trim_matches('-').to_lowercase()))
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn test_slugify_default() {
104        assert_eq!(
105            liquid_core::call_filter!(Slugify, "The _cönfig.yml file").unwrap(),
106            liquid_core::value!("the-cönfig-yml-file")
107        );
108    }
109
110    #[test]
111    fn test_slugify_ascii() {
112        assert_eq!(
113            liquid_core::call_filter!(Slugify, "The _cönfig.yml file", "ascii").unwrap(),
114            liquid_core::value!("the-c-nfig-yml-file")
115        );
116    }
117
118    #[test]
119    fn test_slugify_latin() {
120        assert_eq!(
121            liquid_core::call_filter!(Slugify, "The _cönfig.yml file", "latin").unwrap(),
122            liquid_core::value!("the-config-yml-file")
123        );
124    }
125
126    #[test]
127    fn test_slugify_raw() {
128        assert_eq!(
129            liquid_core::call_filter!(Slugify, "The _config.yml file", "raw").unwrap(),
130            liquid_core::value!("the-_config.yml-file")
131        );
132    }
133
134    #[test]
135    fn test_slugify_none() {
136        assert_eq!(
137            liquid_core::call_filter!(Slugify, "The _config.yml file", "none").unwrap(),
138            liquid_core::value!("the _config.yml file")
139        );
140    }
141}