cobalt/cobalt_model/slug.rs
1use std::sync::LazyLock;
2
3use deunicode;
4use itertools::Itertools;
5use regex::Regex;
6
7static SLUG_INVALID_CHARS: LazyLock<Regex> =
8 LazyLock::new(|| Regex::new(r"([^a-zA-Z0-9]+)").unwrap());
9
10/// Create a slug for a given file. Correlates to Jekyll's :slug path tag
11pub fn slugify<S: AsRef<str>>(name: S) -> String {
12 slugify_str(name.as_ref())
13}
14
15fn slugify_str(name: &str) -> String {
16 let name = deunicode::deunicode_with_tofu(name, "-");
17 let slug = SLUG_INVALID_CHARS.replace_all(&name, "-");
18 slug.trim_matches('-').to_lowercase()
19}
20
21/// Title-case a single word
22fn title_case(s: &str) -> String {
23 let mut c = s.chars();
24 match c.next() {
25 None => String::new(),
26 Some(f) => f
27 .to_uppercase()
28 .chain(c.flat_map(|t| t.to_lowercase()))
29 .collect(),
30 }
31}
32
33/// Format a user-visible title out of a slug. Correlates to Jekyll's "title" attribute
34pub fn titleize_slug<S: AsRef<str>>(slug: S) -> String {
35 titleize_slug_str(slug.as_ref())
36}
37
38fn titleize_slug_str(slug: &str) -> String {
39 slug.split('-').map(title_case).join(" ")
40}
41
42#[test]
43fn test_slugify() {
44 let actual = slugify("___filE-worldD-__09___");
45 assert_eq!(actual, "file-worldd-09");
46}
47
48#[test]
49fn test_slugify_unicode() {
50 let actual = slugify("__Æneid__北亰-worldD-__09___");
51 assert_eq!(actual, "aeneid-bei-jing-worldd-09");
52}
53
54#[test]
55fn test_titleize_slug() {
56 let actual = titleize_slug("tItLeIzE-sLuG");
57 assert_eq!(actual, "Titleize Slug");
58}