string_auto_indent/
lib.rs1#[cfg(doctest)]
2doc_comment::doctest!("../README.md");
3
4pub mod line_ending;
5pub use line_ending::LineEnding;
6
7struct AutoIndent {
9 line_ending: LineEnding,
10}
11
12impl AutoIndent {
13 fn new(input: &str) -> Self {
15 Self {
16 line_ending: LineEnding::detect(input),
17 }
18 }
19
20 fn apply(&self, input: &str) -> String {
22 if input.trim().is_empty() {
23 return String::new();
24 }
25
26 let input = LineEnding::normalize(input);
28 let mut lines: Vec<&str> = input.lines().collect();
29
30 let ends_with_newline = input.ends_with('\n');
32
33 let first_line = if lines.first().map(|s| s.trim()).unwrap_or("").is_empty() {
35 lines.remove(0);
36 None
37 } else {
38 Some(lines.remove(0)) };
40
41 let min_indent = lines
43 .iter()
44 .filter(|line| !line.trim().is_empty()) .map(|line| line.chars().take_while(|c| c.is_whitespace()).count())
46 .min()
47 .unwrap_or(0);
48
49 let mut result: Vec<String> = Vec::new();
51
52 if let Some(first) = first_line {
53 result.push(first.to_string()); }
55
56 result.extend(lines.iter().map(|line| {
57 if line.trim().is_empty() {
58 String::new() } else {
60 line.chars().skip(min_indent).collect() }
62 }));
63
64 if result.last().map(|s| s.trim()).unwrap_or("").is_empty() {
66 *result.last_mut().unwrap() = String::new();
67 }
68
69 let mut output = self.line_ending.restore_from_lines(result);
71 if ends_with_newline {
72 output.push_str(self.line_ending.as_str());
73 }
74
75 output
76 }
77}
78
79pub fn auto_indent(input: &str) -> String {
81 AutoIndent::new(input).apply(input)
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use line_ending::LineEnding;
88
89 #[test]
90 fn test_basic_implementation() {
91 let input = r#"Basic Test
92 1
93 2
94 3
95 "#;
96
97 let line_ending = LineEnding::detect(input);
98
99 assert_eq!(
101 auto_indent(input),
102 line_ending.restore("Basic Test\n1\n 2\n 3\n")
104 );
105
106 assert_eq!(
108 input,
109 line_ending
110 .restore("Basic Test\n 1\n 2\n 3\n ")
111 );
112 }
113
114 #[test]
115 fn test_empty_first_line() {
116 let input = r#"
117 1
118 2
119 3
120 "#;
121
122 let line_ending = LineEnding::detect(input);
123
124 assert_eq!(
126 auto_indent(input),
127 line_ending.restore("1\n 2\n 3\n")
128 );
129
130 assert_eq!(
132 input,
133 line_ending.restore("\n 1\n 2\n 3\n "),
134 );
135 }
136
137 #[test]
138 fn test_indented_first_line() {
139 let input = r#" <- First Line
140 Second Line
141 "#;
142
143 let line_ending = LineEnding::detect(input);
144
145 assert_eq!(
147 auto_indent(input),
148 line_ending.restore(" <- First Line\nSecond Line\n")
149 );
150
151 assert_eq!(
153 input,
154 line_ending.restore(" <- First Line\n Second Line\n "),
155 );
156 }
157
158 #[test]
159 fn test_mixed_indentation() {
160 let input = r#"First Line
161 Second Line
162Third Line
163 "#;
164
165 let line_ending = LineEnding::detect(input);
166
167 assert_eq!(
169 auto_indent(input),
170 line_ending.restore("First Line\n Second Line\nThird Line\n",)
171 );
172
173 assert_eq!(
175 input,
176 line_ending.restore("First Line\n Second Line\nThird Line\n "),
177 );
178 }
179
180 #[test]
181 fn test_single_line_no_change() {
182 let input = "Single line no change";
183
184 let line_ending = LineEnding::detect(input);
185
186 assert_eq!(
188 auto_indent(input),
189 line_ending.restore("Single line no change")
190 );
191
192 assert_eq!(input, line_ending.restore("Single line no change"));
194 }
195
196 #[test]
197 fn test_multiple_blank_lines() {
198 let input = r#"First Line
199
200 A
201
202 B
203
204 C
205
206 D
207
208 E
209 "#;
210
211 let line_ending = LineEnding::detect(input);
212
213 assert_eq!(
215 auto_indent(input),
216 line_ending.restore("First Line\n\n A\n\n B\n\n C\n\n D\n\nE\n")
217 );
218
219 assert_eq!(
221 input,
222 line_ending.restore(
223 "First Line\n \n A\n\n B\n\n C\n\n D\n\n E\n "
224 ),
225 );
226 }
227}