pub fn generate_slug(text: &str) -> String {
let result = text
.to_lowercase()
.replace([' ', '_'], "-")
.replace('&', "and")
.replace('@', "at")
.replace('+', "plus")
.replace('#', "hash")
.replace('%', "percent")
.chars()
.map(|c| if c.is_alphanumeric() || c == '-' || c == '.' { c } else { '-' })
.collect::<String>()
.split('-')
.filter(|s| !s.is_empty())
.collect::<Vec<&str>>()
.join("-")
.trim_matches('-')
.chars()
.take(100)
.collect::<String>()
.trim_end_matches('-')
.to_string();
if result.is_empty() { "untitled".to_string() } else { result }
}
pub fn normalize_separators(s: &str) -> String {
s.replace('\\', "/")
}
pub fn slugify_path_segments(path: &str) -> String {
if path.is_empty() {
return String::new();
}
normalize_separators(path)
.split('/')
.filter(|s| !s.is_empty())
.map(generate_slug)
.collect::<Vec<_>>()
.join("/")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ascii_lowercased_and_hyphenated() {
assert_eq!(generate_slug("Hello World"), "hello-world");
}
#[test]
fn special_chars_replaced() {
assert_eq!(generate_slug("A & B"), "a-and-b");
assert_eq!(generate_slug("price@50%"), "priceat50percent");
}
#[test]
fn cjk_preserved() {
assert_eq!(generate_slug("你好世界"), "你好世界");
}
#[test]
fn empty_falls_back_to_untitled() {
assert_eq!(generate_slug("---"), "untitled");
}
#[test]
fn path_segments_slugified() {
assert_eq!(slugify_path_segments("News/Sub Section"), "news/sub-section");
assert_eq!(slugify_path_segments(""), "");
}
#[test]
fn normalize_separators_converts_backslashes() {
assert_eq!(normalize_separators("News\\2025"), "News/2025");
assert_eq!(normalize_separators("a/b/c"), "a/b/c");
assert_eq!(normalize_separators("Sub Dir\\Winter-Song.mov"), "Sub Dir/Winter-Song.mov");
assert_eq!(normalize_separators(""), "");
}
#[test]
fn path_segments_handle_backslash_separators() {
assert_eq!(slugify_path_segments("News\\Sub Section"), "news/sub-section");
assert_eq!(
slugify_path_segments("News\\Sub Section"),
slugify_path_segments("News/Sub Section"),
);
assert!(!slugify_path_segments("A\\B\\C").contains('\\'));
}
}