39 lines
1.2 KiB
Rust
39 lines
1.2 KiB
Rust
//! URL slug helpers shared by the catalog admin (products and categories).
|
|
|
|
use loco_rs::prelude::*;
|
|
|
|
/// Lowercase a string and collapse every run of non-alphanumeric characters
|
|
/// into a single dash, trimming dashes from the ends.
|
|
pub fn slugify(value: &str) -> String {
|
|
let mut slug = String::new();
|
|
let mut last_was_dash = false;
|
|
for ch in value.chars().flat_map(char::to_lowercase) {
|
|
if ch.is_ascii_alphanumeric() {
|
|
slug.push(ch);
|
|
last_was_dash = false;
|
|
} else if !last_was_dash && !slug.is_empty() {
|
|
slug.push('-');
|
|
last_was_dash = true;
|
|
}
|
|
}
|
|
slug.trim_matches('-').to_string()
|
|
}
|
|
|
|
/// Find the first slug that does not already exist, appending `-2`, `-3`, … to
|
|
/// `base` until `exists` reports the candidate as free. An empty `base` falls
|
|
/// back to `"item"`.
|
|
pub async fn unique_slug<F, Fut>(base: &str, mut exists: F) -> Result<String>
|
|
where
|
|
F: FnMut(String) -> Fut,
|
|
Fut: std::future::Future<Output = Result<bool>>,
|
|
{
|
|
let base = if base.is_empty() { "item" } else { base };
|
|
let mut slug = base.to_string();
|
|
let mut suffix = 2;
|
|
while exists(slug.clone()).await? {
|
|
slug = format!("{base}-{suffix}");
|
|
suffix += 1;
|
|
}
|
|
Ok(slug)
|
|
}
|