el_slugify/
lib.rs

1//! URL slug generator utility.
2//! Fast and efficient.
3//! Your string will be transliterated and sanitized for use in URLs.
4//!
5//! Created by Ana Bujan <ana@eisberg-labs.com>, MIT license
6//!
7//! # Examples
8//!
9//! ```rust
10//! use el_slugify::{slugify, slugify_with_replacement};
11//!
12//! assert_eq!(slugify("di su ćevapi?"), "di-su-cevapi");
13//! assert_eq!(slugify_with_replacement("di su ćevapi?", '_'), "di_su_cevapi");
14//! ```
15#![deny(missing_docs, rust_2018_idioms, elided_lifetimes_in_paths)]
16#![crate_name = "el_slugify"]
17
18use deunicode::deunicode_char;
19
20/// Converts value to URL friendly slug. Default replacement is with "-"
21///
22/// # Examples
23///
24/// ```rust
25/// use el_slugify::slugify;
26///
27/// assert_eq!(slugify("di su ćevapi?"), "di-su-cevapi");
28/// ```
29pub fn slugify(value: &str) -> String {
30    slugify_with_replacement(value, '-')
31}
32
33/// Converts value to URL friendly slug
34pub fn slugify_with_replacement(value: &str, replacement: char) -> String {
35    trim_trailing_space(sanitize(value, replacement).to_lowercase().as_str(), replacement)
36}
37
38/// Removes all non alphanumeric, substitutes to replacement character, without trailing replacement
39fn sanitize(value: &str, replacement: char) -> String {
40    let mut out = String::new();
41    for elem in value.chars() {
42        if is_contained_in_limited_set(elem) {
43            out.push(elem)
44        } else if elem.is_alphabetic() {
45            // characters that need to be decoded should already be in the alphabetic range, everything else is for replacement
46            let decoded_elem = deunicode_char(elem).map(|d| sanitize(d, replacement));
47            if let Some(decoded) = decoded_elem {
48                out.push_str(&decoded);
49            }
50        } else if !out.ends_with(replacement) {
51            out.push(replacement)
52        }
53    }
54
55    out.to_string()
56}
57
58fn is_contained_in_limited_set(value: char) -> bool {
59    matches!(value, '0'..='9' | 'a'..='z' | 'A'..='Z')
60}
61
62fn trim_trailing_space(value: &str, replacement: char) -> String {
63    let mut check_value = value.to_string();
64    if check_value.starts_with(replacement) {
65        check_value.remove(0);
66    }
67    if check_value.ends_with(replacement) {
68        check_value.pop();
69    }
70    check_value.to_string()
71}
72
73#[cfg(test)]
74mod tests {
75    use std::time::Instant;
76    use slugify::slugify;
77    use crate::{slugify as el_slugify};
78
79    #[test]
80    fn slugify_long_special_chars_wor2() {
81        let binding = std::iter::repeat("#% MaČKA mački grize rep! (RIB-a) ~*")
82            .take(10000).collect::<String>();
83
84        let start = Instant::now();
85        let _ = el_slugify(binding.as_str());
86        let elapsed = start.elapsed();
87        println!(">> El slugify took: {:?}", elapsed);
88
89        let start = Instant::now();
90        let _ = slugify!(binding.as_str());
91        let elapsed = start.elapsed();
92        println!(">> Slugify took: {:?}", elapsed);
93    }
94
95    #[test]
96    fn test_slugify() {
97        assert_eq!(el_slugify("Wait! Listen    \n\t!!!Runagalðr$ ~"), "wait-listen-runagaldr");
98        assert_eq!(el_slugify("影 _ 師 嗎"), "ying-shi-ma");
99        assert_eq!(el_slugify("影師嗎"), "ying-shi-ma");
100        assert_eq!(el_slugify("di su ćevapi?"), "di-su-cevapi");
101        assert_eq!(el_slugify("!-.#"), "");
102        assert_eq!(el_slugify("!  kako  a je"), "kako-a-je");
103        assert_eq!(el_slugify("iako **  .+a  na"), "iako-a-na");
104        assert_eq!(el_slugify("   -!abc"), "abc");
105        assert_eq!(el_slugify("iako *."), "iako");
106        assert_eq!(el_slugify("dakako"), "dakako"); // unchanged
107        assert_eq!(el_slugify("   -!kako tako _:+"), "kako-tako");
108        assert_eq!(el_slugify(""), "");
109        assert_eq!(el_slugify("!-.#"), "");
110    }
111}