Skip to main content

sal_text/
dedent.rs

1/**
2 * Dedent a multiline string by removing common leading whitespace.
3 *
4 * This function analyzes all non-empty lines in the input text to determine
5 * the minimum indentation level, then removes that amount of whitespace
6 * from the beginning of each line. This is useful for working with
7 * multi-line strings in code that have been indented to match the
8 * surrounding code structure.
9 *
10 * # Arguments
11 *
12 * * `text` - The multiline string to dedent
13 *
14 * # Returns
15 *
16 * * `String` - The dedented string
17 *
18 * # Examples
19 *
20 * ```
21 * use sal_text::dedent;
22 *
23 * let indented = "    line 1\n    line 2\n        line 3";
24 * let dedented = dedent(indented);
25 * assert_eq!(dedented, "line 1\nline 2\n    line 3");
26 * ```
27 *
28 * # Notes
29 *
30 * - Empty lines are preserved but have all leading whitespace removed
31 * - Tabs are counted as 4 spaces for indentation purposes
32 */
33pub fn dedent(text: &str) -> String {
34    let lines: Vec<&str> = text.lines().collect();
35
36    // Find the minimum indentation level (ignore empty lines)
37    let min_indent = lines
38        .iter()
39        .filter(|line| !line.trim().is_empty())
40        .map(|line| {
41            let mut spaces = 0;
42            for c in line.chars() {
43                if c == ' ' {
44                    spaces += 1;
45                } else if c == '\t' {
46                    spaces += 4; // Count tabs as 4 spaces
47                } else {
48                    break;
49                }
50            }
51            spaces
52        })
53        .min()
54        .unwrap_or(0);
55
56    // Remove that many spaces from the beginning of each line
57    lines
58        .iter()
59        .map(|line| {
60            if line.trim().is_empty() {
61                return String::new();
62            }
63
64            let mut count = 0;
65            let mut chars = line.chars().peekable();
66
67            // Skip initial spaces up to min_indent
68            while count < min_indent && chars.peek().is_some() {
69                match chars.peek() {
70                    Some(' ') => {
71                        chars.next();
72                        count += 1;
73                    }
74                    Some('\t') => {
75                        chars.next();
76                        count += 4;
77                    }
78                    _ => break,
79                }
80            }
81
82            // Return the remaining characters
83            chars.collect::<String>()
84        })
85        .collect::<Vec<String>>()
86        .join("\n")
87}
88
89/**
90 * Prefix a multiline string with a specified prefix.
91 *
92 * This function adds the specified prefix to the beginning of each line in the input text.
93 *
94 * # Arguments
95 *
96 * * `text` - The multiline string to prefix
97 * * `prefix` - The prefix to add to each line
98 *
99 * # Returns
100 *
101 * * `String` - The prefixed string
102 *
103 * # Examples
104 *
105 * ```
106 * use sal_text::prefix;
107 *
108 * let text = "line 1\nline 2\nline 3";
109 * let prefixed = prefix(text, "    ");
110 * assert_eq!(prefixed, "    line 1\n    line 2\n    line 3");
111 * ```
112 */
113pub fn prefix(text: &str, prefix: &str) -> String {
114    text.lines()
115        .map(|line| format!("{}{}", prefix, line))
116        .collect::<Vec<String>>()
117        .join("\n")
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn test_dedent() {
126        let indented = "    line 1\n    line 2\n        line 3";
127        let dedented = dedent(indented);
128        assert_eq!(dedented, "line 1\nline 2\n    line 3");
129    }
130
131    #[test]
132    fn test_prefix() {
133        let text = "line 1\nline 2\nline 3";
134        let prefixed = prefix(text, "    ");
135        assert_eq!(prefixed, "    line 1\n    line 2\n    line 3");
136    }
137}