Skip to main content

fond_domain/
slug.rs

1/// Generate a URL-friendly slug from a recipe title.
2///
3/// Lowercases, replaces non-alphanumeric characters with hyphens,
4/// collapses consecutive hyphens, and trims leading/trailing hyphens.
5///
6/// # Examples
7///
8/// ```
9/// # use fond_domain::slugify;
10/// assert_eq!(slugify("Classic Chicken Adobo"), "classic-chicken-adobo");
11/// assert_eq!(slugify("Crème Brûlée"), "crme-brle");
12/// assert_eq!(slugify("Mapo Tofu (四川麻婆豆腐)"), "mapo-tofu");
13/// ```
14pub fn slugify(title: &str) -> String {
15    let mut slug = String::with_capacity(title.len());
16
17    for ch in title.chars() {
18        if ch.is_ascii_alphanumeric() {
19            slug.push(ch.to_ascii_lowercase());
20        } else if ch == '-' || ch == '_' || ch == ' ' {
21            slug.push('-');
22        }
23        // Non-ASCII and other punctuation are dropped
24    }
25
26    // Collapse consecutive hyphens
27    let mut result = String::with_capacity(slug.len());
28    let mut prev_hyphen = false;
29    for ch in slug.chars() {
30        if ch == '-' {
31            if !prev_hyphen {
32                result.push('-');
33            }
34            prev_hyphen = true;
35        } else {
36            result.push(ch);
37            prev_hyphen = false;
38        }
39    }
40
41    // Trim leading/trailing hyphens
42    result.trim_matches('-').to_string()
43}
44
45/// Derive a title from a filename stem (e.g., "chicken-adobo" → "Chicken Adobo").
46pub fn title_from_stem(stem: &str) -> String {
47    stem.split('-')
48        .map(|word| {
49            let mut chars = word.chars();
50            match chars.next() {
51                None => String::new(),
52                Some(first) => {
53                    let mut s = first.to_uppercase().to_string();
54                    s.extend(chars);
55                    s
56                }
57            }
58        })
59        .collect::<Vec<_>>()
60        .join(" ")
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn basic_slugify() {
69        assert_eq!(slugify("Classic Chicken Adobo"), "classic-chicken-adobo");
70    }
71
72    #[test]
73    fn slugify_with_special_chars() {
74        assert_eq!(slugify("Pasta alla Norma!"), "pasta-alla-norma");
75    }
76
77    #[test]
78    fn slugify_collapses_hyphens() {
79        assert_eq!(slugify("one---two"), "one-two");
80    }
81
82    #[test]
83    fn slugify_trims_edges() {
84        assert_eq!(slugify("  hello  "), "hello");
85    }
86
87    #[test]
88    fn slugify_drops_non_ascii() {
89        assert_eq!(slugify("Crème Brûlée"), "crme-brle");
90    }
91
92    #[test]
93    fn slugify_mixed_unicode() {
94        assert_eq!(slugify("Mapo Tofu (四川麻婆豆腐)"), "mapo-tofu");
95    }
96
97    #[test]
98    fn title_from_stem_basic() {
99        assert_eq!(title_from_stem("chicken-adobo"), "Chicken Adobo");
100    }
101
102    #[test]
103    fn title_from_stem_single_word() {
104        assert_eq!(title_from_stem("sourdough"), "Sourdough");
105    }
106}