1pub fn slugify_filename(filename: &str) -> Option<String> {
7 let stem = match filename.rfind('.') {
10 Some(0) => return None,
11 Some(pos) => &filename[..pos],
12 None => filename,
13 };
14
15 let slug: String = stem
16 .chars()
17 .map(|c| {
18 if c.is_ascii_alphanumeric() {
19 c.to_ascii_lowercase()
20 } else {
21 '-'
22 }
23 })
24 .collect();
25
26 let mut collapsed = String::with_capacity(slug.len());
28 let mut prev_hyphen = true; for ch in slug.chars() {
30 if ch == '-' {
31 if !prev_hyphen {
32 collapsed.push('-');
33 }
34 prev_hyphen = true;
35 } else {
36 collapsed.push(ch);
37 prev_hyphen = false;
38 }
39 }
40 if collapsed.ends_with('-') {
42 collapsed.pop();
43 }
44
45 if collapsed.len() > 48 {
47 collapsed.truncate(48);
48 if let Some(last_hyphen) = collapsed.rfind('-') {
49 collapsed.truncate(last_hyphen);
50 }
51 }
52
53 if collapsed.len() <= 1 || collapsed.chars().all(|c| c.is_ascii_digit()) {
55 return None;
56 }
57
58 Some(collapsed)
59}
60
61pub fn extract_uid(path_segment: &str) -> &str {
66 if path_segment.len() >= 10 {
67 &path_segment[..10]
68 } else {
69 path_segment
70 }
71}
72
73pub fn uid_with_slug(uid: &str, slug: Option<&str>) -> String {
77 match slug {
78 Some(s) if !s.is_empty() => format!("{uid}-{s}"),
79 _ => uid.to_string(),
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86
87 #[test]
88 fn slugify_basic_filename() {
89 assert_eq!(
90 slugify_filename("stdlib-fix-plan.md"),
91 Some("stdlib-fix-plan".to_string())
92 );
93 }
94
95 #[test]
96 fn slugify_strips_extension() {
97 assert_eq!(
98 slugify_filename("My Report.html"),
99 Some("my-report".to_string())
100 );
101 }
102
103 #[test]
104 fn slugify_collapses_special_chars() {
105 assert_eq!(
106 slugify_filename("foo___bar--baz.txt"),
107 Some("foo-bar-baz".to_string())
108 );
109 }
110
111 #[test]
112 fn slugify_trims_leading_trailing_hyphens() {
113 assert_eq!(slugify_filename("--hello--.md"), Some("hello".to_string()));
114 }
115
116 #[test]
117 fn slugify_truncates_long_names() {
118 let long_name = "a".repeat(60) + ".md";
119 let result = slugify_filename(&long_name).unwrap();
120 assert!(result.len() <= 48);
121 }
122
123 #[test]
124 fn slugify_truncates_on_hyphen_boundary() {
125 let name = format!("{}-bbb.md", "a".repeat(45));
127 let result = slugify_filename(&name).unwrap();
128 assert!(result.len() <= 48);
129 assert!(!result.ends_with('-'));
130 }
131
132 #[test]
133 fn slugify_returns_none_for_empty() {
134 assert_eq!(slugify_filename(".md"), None);
135 }
136
137 #[test]
138 fn slugify_returns_none_for_single_char() {
139 assert_eq!(slugify_filename("a.md"), None);
140 }
141
142 #[test]
143 fn slugify_returns_none_for_numeric_only() {
144 assert_eq!(slugify_filename("12345.txt"), None);
145 }
146
147 #[test]
148 fn slugify_returns_none_for_no_extension_single_char() {
149 assert_eq!(slugify_filename("x"), None);
150 }
151
152 #[test]
153 fn slugify_no_extension() {
154 assert_eq!(slugify_filename("readme"), Some("readme".to_string()));
155 }
156
157 #[test]
158 fn extract_uid_with_slug() {
159 assert_eq!(extract_uid("1vjmeRjNdi-stdlib-fix-plan"), "1vjmeRjNdi");
160 }
161
162 #[test]
163 fn extract_uid_plain() {
164 assert_eq!(extract_uid("1vjmeRjNdi"), "1vjmeRjNdi");
165 }
166
167 #[test]
168 fn extract_uid_short_input() {
169 assert_eq!(extract_uid("abc"), "abc");
170 }
171
172 #[test]
173 fn uid_with_slug_some() {
174 assert_eq!(
175 uid_with_slug("1vjmeRjNdi", Some("stdlib-fix-plan")),
176 "1vjmeRjNdi-stdlib-fix-plan"
177 );
178 }
179
180 #[test]
181 fn uid_with_slug_none() {
182 assert_eq!(uid_with_slug("1vjmeRjNdi", None), "1vjmeRjNdi");
183 }
184
185 #[test]
186 fn uid_with_slug_empty() {
187 assert_eq!(uid_with_slug("1vjmeRjNdi", Some("")), "1vjmeRjNdi");
188 }
189}