1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
//! Filename-safe slugification for cache entry queries.
//!
//! Used by the `distill` feature to convert arbitrary user query text into
//! safe filenames. Lowercases, strips non-alphanumeric characters, collapses
//! runs of separators, and trims edges.
/// Slugify a query string for use as a filename.
///
/// - Lowercases the input
/// - Replaces any non-alphanumeric ASCII character with `-`
/// - Collapses runs of `-` into a single `-`
/// - Trims leading/trailing `-`
/// - Returns empty string if no alphanumeric characters remain
///
/// # Examples
///
/// ```
/// use conproxy::proxy::slugify;
/// assert_eq!(slugify("Hello, World!"), "hello-world");
/// assert_eq!(slugify(" multi space "), "multi-space");
/// assert_eq!(slugify("foo/bar\\baz"), "foo-bar-baz");
/// assert_eq!(slugify(""), "");
/// assert_eq!(slugify("!@#$%"), "");
/// ```
pub fn slugify(input: &str) -> String {
let mut out = String::with_capacity(input.len());
let mut last_was_sep = true; // treat start as separator so we don't emit leading '-'
for ch in input.chars() {
if ch.is_ascii_alphanumeric() {
out.push(ch.to_ascii_lowercase());
last_was_sep = false;
} else if !last_was_sep {
out.push('-');
last_was_sep = true;
}
}
// Strip trailing '-' if present
if out.ends_with('-') {
out.pop();
}
out
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::arithmetic_side_effects,
clippy::panic
)]
mod tests {
use super::*;
#[test]
fn test_slugify_basic() {
assert_eq!(slugify("Hello, World!"), "hello-world");
}
#[test]
fn test_slugify_whitespace_runs() {
assert_eq!(slugify(" multi space "), "multi-space");
}
#[test]
fn test_slugify_slashes_and_punctuation() {
assert_eq!(slugify("foo/bar\\baz"), "foo-bar-baz");
}
#[test]
fn test_slugify_empty() {
assert_eq!(slugify(""), "");
}
#[test]
fn test_slugify_only_punctuation() {
assert_eq!(slugify("!@#$%"), "");
}
#[test]
fn test_slugify_unicode_stripped() {
// Non-ASCII alphanumerics like accented chars are not ASCII alphanumeric,
// so they become separators. Result is still safe.
assert_eq!(slugify("café au lait"), "caf-au-lait");
}
#[test]
fn test_slugify_preserves_numbers() {
assert_eq!(slugify("test 123 foo"), "test-123-foo");
}
#[test]
fn test_slugify_no_trailing_separator() {
assert_eq!(slugify("trailing/"), "trailing");
assert_eq!(slugify("trailing "), "trailing");
}
#[test]
fn test_slugify_no_leading_separator() {
assert_eq!(slugify("/leading"), "leading");
assert_eq!(slugify(" leading"), "leading");
}
#[test]
fn test_slugify_already_clean() {
assert_eq!(slugify("already-clean-slug"), "already-clean-slug");
}
}