slug_generator_plus 0.1.1

Enhanced URL slug generator with optimizations
Documentation
use anyhow::Result;

/// Generate a URL-friendly slug from a string
///
/// # Examples
///
/// ```
/// use slug_generator_plus::generate_slug;
/// assert_eq!(generate_slug("Hello World!", None, None, None), "hello-world");
/// ```
pub fn generate_slug(
    text: &str,
    separator: Option<&str>,
    max_length: Option<usize>,
    lower_case: Option<bool>,
) -> String {
    let separator = separator.unwrap_or("-");
    let max_length = max_length.unwrap_or(100);
    let lower_case = lower_case.unwrap_or(true);
    
    if text.is_empty() {
        return String::new();
    }
    
    // Convert to lowercase if requested
    let text = if lower_case {
        text.to_lowercase()
    } else {
        text.to_string()
    };
    
    // Replace special characters with separator
    let mut result = String::new();
    let mut last_was_separator = false;
    
    for c in text.chars() {
        // Check if character is alphanumeric (including Unicode)
        if c.is_alphanumeric() {
            result.push(c);
            last_was_separator = false;
        } else if c == ' ' || c == '-' || c == '_' || c == '.' {
            // These characters become separators
            if !last_was_separator && !result.is_empty() {
                result.push_str(separator);
                last_was_separator = true;
            }
        } else if c == '\'' {
            // Apostrophe - just skip it
            continue;
        } else {
            // Other special characters - replace with separator if not already
            if !last_was_separator && !result.is_empty() {
                result.push_str(separator);
                last_was_separator = true;
            }
        }
    }
    
    // Remove trailing separator
    if result.ends_with(separator) {
        result.truncate(result.len() - separator.len());
    }
    
    // Remove leading separator
    if result.starts_with(separator) {
        result = result[separator.len()..].to_string();
    }
    
    // Trim to max length
    if result.len() > max_length {
        // Try to cut at a separator boundary
        let mut cut_at = max_length;
        while cut_at > 0 && !result.chars().nth(cut_at).unwrap_or(' ').is_alphanumeric() {
            cut_at -= 1;
        }
        if cut_at == 0 {
            cut_at = max_length;
        }
        result = result[..cut_at].to_string();
        // Remove trailing separator
        if result.ends_with(separator) {
            result.truncate(result.len() - separator.len());
        }
    }
    
    result
}

/// Generate slug with default settings
pub fn slug(text: &str) -> String {
    generate_slug(text, None, None, None)
}

/// Generate slug with custom separator
pub fn slug_with_separator(text: &str, separator: &str) -> String {
    generate_slug(text, Some(separator), None, None)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_slug_basic() {
        assert_eq!(slug("Hello World"), "hello-world");
        assert_eq!(slug("Hello World!"), "hello-world");
        assert_eq!(slug("  Hello   World  "), "hello-world");
    }

    #[test]
    fn test_slug_special_chars() {
        assert_eq!(slug("Hello, World!"), "hello-world");
        assert_eq!(slug("Rust & C++"), "rust-c");
        assert_eq!(slug("Email@example.com"), "email-example-com");
        assert_eq!(slug("Price: $100"), "price-100");
    }

    #[test]
    fn test_slug_unicode() {
        assert_eq!(slug("Привет мир"), "привет-мир");
        assert_eq!(slug("Hello 世界"), "hello-世界");
    }

    #[test]
    fn test_slug_empty() {
        assert_eq!(slug(""), "");
        assert_eq!(slug("   "), "");
    }

    #[test]
    fn test_slug_separator() {
        assert_eq!(slug_with_separator("Hello World", "_"), "hello_world");
        assert_eq!(slug_with_separator("Hello World", "."), "hello.world");
        assert_eq!(slug_with_separator("Hello World", ""), "helloworld");
    }

    #[test]
    fn test_slug_max_length() {
        let long = "a".repeat(200);
        let result = generate_slug(&long, None, Some(50), None);
        assert_eq!(result.len(), 50);
        assert_eq!(result, "a".repeat(50));
    }

    #[test]
    fn test_slug_max_length_with_cut() {
        let text = "Hello World This Is A Test";
        let result = generate_slug(text, None, Some(15), None);
        assert!(result.len() <= 15);
        // Should cut at word boundary
        assert!(!result.ends_with('-'));
    }

    #[test]
    fn test_slug_preserve_case() {
        assert_eq!(generate_slug("Hello World", None, None, Some(false)), "Hello-World");
        assert_eq!(generate_slug("Hello World", None, None, Some(true)), "hello-world");
    }

    #[test]
    fn test_slug_only_special_chars() {
        assert_eq!(slug("!@#$%^&*()"), "");
        assert_eq!(slug("---___..."), "");
    }

    #[test]
    fn test_slug_numbers() {
        assert_eq!(slug("123 Main St"), "123-main-st");
        assert_eq!(slug("Version 2.0"), "version-2-0");
    }

    #[test]
    fn test_slug_apostrophe() {
        assert_eq!(slug("John's Blog"), "johns-blog");
        assert_eq!(slug("Don't Stop"), "dont-stop");
    }

    #[test]
    fn test_slug_ampersand() {
        assert_eq!(slug("Rock & Roll"), "rock-roll");
        assert_eq!(slug("Tom & Jerry"), "tom-jerry");
    }

    #[test]
    fn test_slug_multiple_separators() {
        assert_eq!(slug("Hello---World"), "hello-world");
        assert_eq!(slug("Hello...World"), "hello-world");
        assert_eq!(slug("Hello___World"), "hello-world");
    }
}