sal-text 0.1.0

SAL Text - Text processing and manipulation utilities with regex, templating, and normalization
Documentation
/**
 * Dedent a multiline string by removing common leading whitespace.
 *
 * This function analyzes all non-empty lines in the input text to determine
 * the minimum indentation level, then removes that amount of whitespace
 * from the beginning of each line. This is useful for working with
 * multi-line strings in code that have been indented to match the
 * surrounding code structure.
 *
 * # Arguments
 *
 * * `text` - The multiline string to dedent
 *
 * # Returns
 *
 * * `String` - The dedented string
 *
 * # Examples
 *
 * ```
 * use sal_text::dedent;
 *
 * let indented = "    line 1\n    line 2\n        line 3";
 * let dedented = dedent(indented);
 * assert_eq!(dedented, "line 1\nline 2\n    line 3");
 * ```
 *
 * # Notes
 *
 * - Empty lines are preserved but have all leading whitespace removed
 * - Tabs are counted as 4 spaces for indentation purposes
 */
pub fn dedent(text: &str) -> String {
    let lines: Vec<&str> = text.lines().collect();

    // Find the minimum indentation level (ignore empty lines)
    let min_indent = lines
        .iter()
        .filter(|line| !line.trim().is_empty())
        .map(|line| {
            let mut spaces = 0;
            for c in line.chars() {
                if c == ' ' {
                    spaces += 1;
                } else if c == '\t' {
                    spaces += 4; // Count tabs as 4 spaces
                } else {
                    break;
                }
            }
            spaces
        })
        .min()
        .unwrap_or(0);

    // Remove that many spaces from the beginning of each line
    lines
        .iter()
        .map(|line| {
            if line.trim().is_empty() {
                return String::new();
            }

            let mut count = 0;
            let mut chars = line.chars().peekable();

            // Skip initial spaces up to min_indent
            while count < min_indent && chars.peek().is_some() {
                match chars.peek() {
                    Some(' ') => {
                        chars.next();
                        count += 1;
                    }
                    Some('\t') => {
                        chars.next();
                        count += 4;
                    }
                    _ => break,
                }
            }

            // Return the remaining characters
            chars.collect::<String>()
        })
        .collect::<Vec<String>>()
        .join("\n")
}

/**
 * Prefix a multiline string with a specified prefix.
 *
 * This function adds the specified prefix to the beginning of each line in the input text.
 *
 * # Arguments
 *
 * * `text` - The multiline string to prefix
 * * `prefix` - The prefix to add to each line
 *
 * # Returns
 *
 * * `String` - The prefixed string
 *
 * # Examples
 *
 * ```
 * use sal_text::prefix;
 *
 * let text = "line 1\nline 2\nline 3";
 * let prefixed = prefix(text, "    ");
 * assert_eq!(prefixed, "    line 1\n    line 2\n    line 3");
 * ```
 */
pub fn prefix(text: &str, prefix: &str) -> String {
    text.lines()
        .map(|line| format!("{}{}", prefix, line))
        .collect::<Vec<String>>()
        .join("\n")
}

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

    #[test]
    fn test_dedent() {
        let indented = "    line 1\n    line 2\n        line 3";
        let dedented = dedent(indented);
        assert_eq!(dedented, "line 1\nline 2\n    line 3");
    }

    #[test]
    fn test_prefix() {
        let text = "line 1\nline 2\nline 3";
        let prefixed = prefix(text, "    ");
        assert_eq!(prefixed, "    line 1\n    line 2\n    line 3");
    }
}