liquid_lib/jekyll/
slugify.rs1use 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}