sal-text 0.1.0

SAL Text - Text processing and manipulation utilities with regex, templating, and normalization
Documentation
pub fn name_fix(text: &str) -> String {
    let mut result = String::with_capacity(text.len());

    let mut last_was_underscore = false;
    for c in text.chars() {
        // Keep only ASCII characters
        if c.is_ascii() {
            // Replace specific characters with underscore
            if c.is_whitespace()
                || c == ','
                || c == '-'
                || c == '"'
                || c == '\''
                || c == '#'
                || c == '!'
                || c == '('
                || c == ')'
                || c == '['
                || c == ']'
                || c == '='
                || c == '+'
                || c == '<'
                || c == '>'
                || c == '@'
                || c == '$'
                || c == '%'
                || c == '^'
                || c == '&'
                || c == '*'
            {
                // Only add underscore if the last character wasn't an underscore
                if !last_was_underscore {
                    result.push('_');
                    last_was_underscore = true;
                }
            } else {
                // Add the character as is (will be converted to lowercase later)
                result.push(c);
                last_was_underscore = false;
            }
        }
        // Non-ASCII characters are simply skipped
    }

    // Convert to lowercase
    return result.to_lowercase();
}

pub fn path_fix(text: &str) -> String {
    // If path ends with '/', return as is
    if text.ends_with('/') {
        return text.to_string();
    }

    // Find the last '/' to extract the filename part
    match text.rfind('/') {
        Some(pos) => {
            // Extract the path and filename parts
            let path = &text[..=pos];
            let filename = &text[pos + 1..];

            // Apply name_fix to the filename part only
            return format!("{}{}", path, name_fix(filename));
        }
        None => {
            // No '/' found, so the entire text is a filename
            return name_fix(text);
        }
    }
}

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

    #[test]
    fn test_name_fix() {
        // Test ASCII conversion and special character replacement
        assert_eq!(name_fix("Hello World"), "hello_world");
        assert_eq!(name_fix("File-Name.txt"), "file_name.txt");
        assert_eq!(name_fix("Test!@#$%^&*()"), "test_");
        assert_eq!(name_fix("Space, Tab\t, Comma,"), "space_tab_comma_");
        assert_eq!(name_fix("Quotes\"'"), "quotes_");
        assert_eq!(name_fix("Brackets[]<>"), "brackets_");
        assert_eq!(name_fix("Operators=+-"), "operators_");

        // Test non-ASCII characters removal
        assert_eq!(name_fix("Café"), "caf");
        assert_eq!(name_fix("Résumé"), "rsum");
        assert_eq!(name_fix("Über"), "ber");

        // Test lowercase conversion
        assert_eq!(name_fix("UPPERCASE"), "uppercase");
        assert_eq!(name_fix("MixedCase"), "mixedcase");
    }

    #[test]
    fn test_path_fix() {
        // Test path ending with /
        assert_eq!(path_fix("/path/to/directory/"), "/path/to/directory/");

        // Test single filename
        assert_eq!(path_fix("filename.txt"), "filename.txt");
        assert_eq!(path_fix("UPPER-file.md"), "upper_file.md");

        // Test path with filename
        assert_eq!(path_fix("/path/to/File Name.txt"), "/path/to/file_name.txt");
        assert_eq!(
            path_fix("./relative/path/to/DOCUMENT-123.pdf"),
            "./relative/path/to/document_123.pdf"
        );
        assert_eq!(
            path_fix("/absolute/path/to/Résumé.doc"),
            "/absolute/path/to/rsum.doc"
        );

        // Test path with special characters in filename
        assert_eq!(
            path_fix("/path/with/[special]<chars>.txt"),
            "/path/with/_special_chars_.txt"
        );
    }
}