buup/transformers/
slugify.rs

1use crate::{Transform, TransformError, TransformerCategory};
2
3/// Slugify transformer
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub struct Slugify;
6
7impl Transform for Slugify {
8    fn name(&self) -> &'static str {
9        "Slugify"
10    }
11
12    fn id(&self) -> &'static str {
13        "slugify"
14    }
15
16    fn description(&self) -> &'static str {
17        "Converts text into a URL-friendly slug (lowercase, dashes, removes special chars)"
18    }
19
20    fn category(&self) -> TransformerCategory {
21        TransformerCategory::Other
22    }
23
24    fn transform(&self, input: &str) -> Result<String, TransformError> {
25        if input.is_empty() {
26            return Ok(String::new());
27        }
28
29        let mut slug = String::with_capacity(input.len());
30        let mut last_char_was_dash = true; // Treat beginning as if preceded by a dash
31
32        for c in input.chars() {
33            if c.is_ascii_alphanumeric() {
34                slug.push(c.to_ascii_lowercase());
35                last_char_was_dash = false;
36            } else if c.is_whitespace() || c == '-' || c == '_' {
37                if !last_char_was_dash {
38                    slug.push('-');
39                    last_char_was_dash = true;
40                }
41            } else {
42                // Ignore other characters
43                // We could attempt transliteration here (e.g., 'é' to 'e')
44                // but keeping it simple and dependency-free for now.
45            }
46        }
47
48        // Remove trailing dash if exists and the slug is not just a dash
49        if slug.ends_with('-') && slug.len() > 1 {
50            slug.pop();
51        }
52
53        // Handle cases where the input consisted *only* of characters that were removed or replaced by dashes
54        // which might result in an empty slug or just a dash after trimming.
55        if slug.is_empty() || slug == "-" {
56            return Ok(String::new()); // Or decide on alternative output like "n-a"
57        }
58
59        Ok(slug)
60    }
61
62    fn default_test_input(&self) -> &'static str {
63        "This is a Test String! 123?"
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn test_slugify_basic() {
73        let transformer = Slugify;
74        assert_eq!(
75            transformer.transform(transformer.default_test_input()),
76            Ok("this-is-a-test-string-123".to_string())
77        );
78        assert_eq!(
79            transformer.transform("Hello World"),
80            Ok("hello-world".to_string())
81        );
82    }
83
84    #[test]
85    fn test_slugify_punctuation() {
86        let transformer = Slugify;
87        assert_eq!(
88            transformer.transform("Test! String?"),
89            Ok("test-string".to_string())
90        );
91    }
92
93    #[test]
94    fn test_slugify_uppercase() {
95        let transformer = Slugify;
96        assert_eq!(
97            transformer.transform("UPPERCASE"),
98            Ok("uppercase".to_string())
99        );
100    }
101
102    #[test]
103    fn test_slugify_consecutive_spaces() {
104        let transformer = Slugify;
105        assert_eq!(
106            transformer.transform("Multiple  Spaces"),
107            Ok("multiple-spaces".to_string())
108        );
109    }
110
111    #[test]
112    fn test_slugify_leading_trailing_spaces() {
113        let transformer = Slugify;
114        assert_eq!(
115            transformer.transform("  Leading and Trailing  "),
116            Ok("leading-and-trailing".to_string())
117        );
118    }
119
120    #[test]
121    fn test_slugify_leading_trailing_hyphens() {
122        let transformer = Slugify;
123        assert_eq!(
124            transformer.transform("-Hyphens-"),
125            Ok("hyphens".to_string())
126        ); // Inner logic handles this
127        assert_eq!(
128            transformer.transform(" Leading-Hyphen"),
129            Ok("leading-hyphen".to_string())
130        );
131        assert_eq!(
132            transformer.transform("Trailing-Hyphen- "),
133            Ok("trailing-hyphen".to_string())
134        );
135    }
136
137    #[test]
138    fn test_slugify_underscores() {
139        let transformer = Slugify;
140        assert_eq!(
141            transformer.transform("snake_case_string"),
142            Ok("snake-case-string".to_string())
143        );
144    }
145
146    #[test]
147    fn test_slugify_mixed() {
148        let transformer = Slugify;
149        assert_eq!(
150            transformer.transform("  Mixed CASE with Punctuations! and _underscores- "),
151            Ok("mixed-case-with-punctuations-and-underscores".to_string())
152        );
153    }
154
155    #[test]
156    fn test_slugify_empty() {
157        let transformer = Slugify;
158        assert_eq!(transformer.transform(""), Ok("".to_string()));
159    }
160
161    #[test]
162    fn test_slugify_only_special_chars() {
163        let transformer = Slugify;
164        assert_eq!(transformer.transform("!@#$%^"), Ok("".to_string()));
165        assert_eq!(transformer.transform(" - _ - "), Ok("".to_string()));
166    }
167
168    #[test]
169    fn test_slugify_non_ascii() {
170        let transformer = Slugify;
171        // Basic implementation ignores non-ASCII
172        assert_eq!(
173            transformer.transform("Héllö Wörld"),
174            Ok("hll-wrld".to_string())
175        );
176    }
177}