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}