pub fn slugify(text: &str) -> String {
text.to_lowercase()
.chars()
.map(|c| {
if c.is_alphanumeric() || c == '-' || c == '_' {
c
} else if c == ' ' {
'-'
} else {
' '
}
})
.collect::<String>()
.split_whitespace()
.collect::<Vec<_>>()
.join("")
}
#[derive(Default)]
pub struct SlugGenerator {
seen: std::collections::HashMap<String, usize>,
}
impl SlugGenerator {
pub fn new() -> Self {
Self::default()
}
pub fn generate(&mut self, text: &str) -> String {
let base = slugify(text);
let base = if base.is_empty() {
"section".to_string()
} else {
base
};
let Some(&last) = self.seen.get(&base) else {
self.seen.insert(base.clone(), 0);
return base;
};
let mut count = last;
loop {
count += 1;
let candidate = format!("{base}-{count}");
if !self.seen.contains_key(&candidate) {
self.seen.insert(base, count);
self.seen.insert(candidate.clone(), 0);
return candidate;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn slugify_simple_text() {
assert_eq!(slugify("Hello World"), "hello-world");
}
#[test]
fn slugify_strips_special_characters() {
assert_eq!(slugify("Hello, World! (2024)"), "hello-world-2024");
}
#[test]
fn first_occurrence_keeps_the_plain_slug() {
let mut slugs = SlugGenerator::new();
assert_eq!(slugs.generate("Setup"), "setup");
}
#[test]
fn duplicate_headings_get_distinct_anchors() {
let mut slugs = SlugGenerator::new();
assert_eq!(slugs.generate("Setup"), "setup");
assert_eq!(slugs.generate("Setup"), "setup-1");
assert_eq!(slugs.generate("Setup"), "setup-2");
}
#[test]
fn suffix_collision_with_an_explicit_heading_is_avoided() {
let mut slugs = SlugGenerator::new();
assert_eq!(slugs.generate("Setup"), "setup");
assert_eq!(slugs.generate("Setup 1"), "setup-1");
assert_eq!(slugs.generate("Setup"), "setup-2");
}
#[test]
fn headings_with_no_slugifiable_text_still_get_anchors() {
let mut slugs = SlugGenerator::new();
assert_eq!(slugs.generate("!!!"), "section");
assert_eq!(slugs.generate("???"), "section-1");
}
#[test]
fn different_headings_do_not_interfere() {
let mut slugs = SlugGenerator::new();
assert_eq!(slugs.generate("Install"), "install");
assert_eq!(slugs.generate("Usage"), "usage");
assert_eq!(slugs.generate("Install"), "install-1");
}
}