pub fn lower(s: &str) -> String {
s.to_lowercase()
}
pub fn upper(s: &str) -> String {
s.to_uppercase()
}
pub fn capitalize(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(first) => {
let upper_first: String = first.to_uppercase().collect();
let lower_rest: String = chars.as_str().to_lowercase();
format!("{upper_first}{lower_rest}")
}
}
}
pub fn title(s: &str) -> String {
s.split_whitespace()
.map(capitalize)
.collect::<Vec<_>>()
.join(" ")
}
pub fn slug(s: &str) -> String {
slug_with_separator(s, "-")
}
pub fn slug_with_separator(s: &str, separator: &str) -> String {
let lowered = s.to_lowercase();
let mut result = String::with_capacity(lowered.len());
let mut prev_was_sep = true;
for ch in lowered.chars() {
if ch.is_ascii_alphanumeric() {
result.push(ch);
prev_was_sep = false;
} else if !prev_was_sep {
result.push_str(separator);
prev_was_sep = true;
}
}
if result.ends_with(separator) {
result.truncate(result.len() - separator.len());
}
result
}
pub fn deslug(s: &str) -> String {
s.chars()
.map(|c| if c == '-' || c == '_' { ' ' } else { c })
.collect()
}
pub fn deslug_title(s: &str) -> String {
title(&deslug(s))
}
pub fn strip_non_ascii(s: &str, separator: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut prev_was_sep = true;
for ch in s.chars() {
if ch.is_ascii_alphanumeric() {
result.push(ch);
prev_was_sep = false;
} else if ch.is_ascii() && !prev_was_sep {
result.push_str(separator);
prev_was_sep = true;
}
}
if result.ends_with(separator) {
result.truncate(result.len() - separator.len());
}
result
}
pub fn normalize_unicode(s: &str) -> String {
unicode_normalization::UnicodeNormalization::nfc(s).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lower_basic() {
assert_eq!(lower("Hello World"), "hello world");
}
#[test]
fn lower_unicode() {
assert_eq!(lower("CAFÉ"), "café");
}
#[test]
fn lower_empty() {
assert_eq!(lower(""), "");
}
#[test]
fn upper_basic() {
assert_eq!(upper("hello world"), "HELLO WORLD");
}
#[test]
fn upper_unicode() {
assert_eq!(upper("café"), "CAFÉ");
}
#[test]
fn upper_empty() {
assert_eq!(upper(""), "");
}
#[test]
fn capitalize_basic() {
assert_eq!(capitalize("hello"), "Hello");
}
#[test]
fn capitalize_all_caps() {
assert_eq!(capitalize("HELLO"), "Hello");
}
#[test]
fn capitalize_empty() {
assert_eq!(capitalize(""), "");
}
#[test]
fn capitalize_unicode_first_char() {
assert_eq!(capitalize("über"), "Über");
}
#[test]
fn capitalize_single_char() {
assert_eq!(capitalize("a"), "A");
}
#[test]
fn title_multi_word() {
assert_eq!(title("hello world"), "Hello World");
}
#[test]
fn title_mixed_case() {
assert_eq!(title("hELLO wORLD"), "Hello World");
}
#[test]
fn title_extra_spaces() {
assert_eq!(title("hello world"), "Hello World");
}
#[test]
fn title_single_word() {
assert_eq!(title("hello"), "Hello");
}
#[test]
fn title_empty() {
assert_eq!(title(""), "");
}
#[test]
fn slug_spaces() {
assert_eq!(slug("Hello World"), "hello-world");
}
#[test]
fn slug_special_chars() {
assert_eq!(slug("file (copy).bak"), "file-copy-bak");
}
#[test]
fn slug_consecutive_separators() {
assert_eq!(slug("file---name...here"), "file-name-here");
}
#[test]
fn slug_leading_trailing() {
assert_eq!(slug(" Hello World! "), "hello-world");
}
#[test]
fn slug_unicode() {
assert_eq!(slug("Café Résumé"), "caf-r-sum");
}
#[test]
fn slug_already_clean() {
assert_eq!(slug("already-clean"), "already-clean");
}
#[test]
fn slug_with_custom_separator() {
assert_eq!(slug_with_separator("Hello World", "_"), "hello_world");
}
#[test]
fn deslug_hyphens() {
assert_eq!(deslug("vehicles-and-monsters"), "vehicles and monsters");
}
#[test]
fn deslug_underscores() {
assert_eq!(deslug("my_project_name"), "my project name");
}
#[test]
fn deslug_mixed() {
assert_eq!(deslug("my-project_name"), "my project name");
}
#[test]
fn deslug_no_separators() {
assert_eq!(deslug("hello"), "hello");
}
#[test]
fn deslug_empty() {
assert_eq!(deslug(""), "");
}
#[test]
fn deslug_title_hyphens() {
assert_eq!(
deslug_title("vehicles-and-monsters"),
"Vehicles And Monsters"
);
}
#[test]
fn deslug_title_underscores() {
assert_eq!(deslug_title("my_project_name"), "My Project Name");
}
#[test]
fn deslug_title_single_word() {
assert_eq!(deslug_title("hello"), "Hello");
}
#[test]
fn deslug_title_empty() {
assert_eq!(deslug_title(""), "");
}
#[test]
fn strip_non_ascii_accented() {
assert_eq!(strip_non_ascii("café résumé", "-"), "caf-rsum");
}
#[test]
fn strip_non_ascii_emoji() {
assert_eq!(strip_non_ascii("hello 🎉 world", "-"), "hello-world");
}
#[test]
fn strip_non_ascii_clean_input() {
assert_eq!(strip_non_ascii("hello", "-"), "hello");
}
#[test]
fn strip_non_ascii_preserves_case() {
assert_eq!(strip_non_ascii("Café", "-"), "Caf");
}
#[test]
fn normalize_composed_vs_decomposed() {
let nfd = "re\u{0301}sume\u{0301}";
let nfc = normalize_unicode(nfd);
assert_eq!(nfc, "r\u{00e9}sum\u{00e9}");
}
#[test]
fn normalize_already_nfc() {
let nfc = "résumé";
assert_eq!(normalize_unicode(nfc), nfc);
}
#[test]
fn normalize_empty() {
assert_eq!(normalize_unicode(""), "");
}
}