string_auto_indent/
lib.rs1#[cfg(doctest)]
2doc_comment::doctest!("../README.md");
3
4pub use line_ending::LineEnding;
5
6struct AutoIndent {
8 line_ending: LineEnding,
9}
10
11impl AutoIndent {
12 fn new(input: &str) -> Self {
14 Self {
15 line_ending: LineEnding::from(input),
16 }
17 }
18
19 fn apply(&self, input: &str) -> String {
21 if input.trim().is_empty() {
22 return String::new();
23 }
24
25 let mut lines: Vec<String> = LineEnding::split(input);
26
27 let first_line = Some(lines.remove(0));
29
30 let min_indent = lines
32 .iter()
33 .filter(|line| !line.trim().is_empty()) .map(|line| line.chars().take_while(|c| c.is_whitespace()).count())
35 .min()
36 .unwrap_or(0);
37
38 let mut result: Vec<String> = Vec::new();
40
41 if let Some(first) = first_line {
42 result.push(first.to_string()); }
44
45 result.extend(lines.iter().map(|line| {
46 if line.trim().is_empty() {
47 String::new() } else {
49 line.chars().skip(min_indent).collect() }
51 }));
52
53 if result.last().map(|s| s.trim()).unwrap_or("").is_empty() {
55 *result.last_mut().unwrap() = String::new();
56 }
57
58 self.line_ending.join(result)
60 }
61}
62
63pub fn auto_indent(input: &str) -> String {
65 AutoIndent::new(input).apply(input)
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71 use line_ending::LineEnding;
72
73 fn get_readme_contents() -> String {
74 use std::fs::File;
75 use std::io::Read;
76
77 let readme_file = "README.md";
78
79 let mut read_content = String::new();
81 File::open(readme_file)
82 .unwrap_or_else(|_| panic!("Failed to open {}", readme_file))
83 .read_to_string(&mut read_content)
84 .unwrap_or_else(|_| panic!("Failed to read {}", readme_file));
85
86 read_content
87 }
88
89 #[test]
90 fn test_preserves_formatting() {
91 let readme_contents = get_readme_contents();
92
93 assert_eq!(auto_indent(&readme_contents), readme_contents);
94
95 let lines = LineEnding::split(&readme_contents);
97 assert_eq!(lines.first().unwrap(), "# Multi-line String Auto-Indent");
98
99 assert!(
101 lines.len() > 5,
102 "Expected README to have more than 5 lines, but got {}",
103 lines.len()
104 );
105 }
106
107 #[test]
108 fn test_basic_implementation() {
109 let input = r#"Basic Test
110 1
111 2
112 3
113 "#;
114
115 let line_ending = LineEnding::from(input);
116
117 assert_eq!(
119 auto_indent(input),
120 line_ending.denormalize("Basic Test\n1\n 2\n 3\n")
122 );
123
124 assert_eq!(
126 input,
127 line_ending
128 .denormalize("Basic Test\n 1\n 2\n 3\n ")
129 );
130 }
131
132 #[test]
133 fn test_empty_first_line() {
134 let input = r#"
135 1
136 2
137 3
138 "#;
139
140 let line_ending = LineEnding::from(input);
141
142 assert_eq!(
144 auto_indent(input),
145 line_ending.denormalize("\n1\n 2\n 3\n")
146 );
147
148 assert_eq!(
150 input,
151 line_ending.denormalize("\n 1\n 2\n 3\n "),
152 );
153 }
154
155 #[test]
156 fn test_indented_first_line() {
157 let input = r#" <- First Line
158 Second Line
159 "#;
160
161 let line_ending = LineEnding::from(input);
162
163 assert_eq!(
165 auto_indent(input),
166 line_ending.denormalize(" <- First Line\nSecond Line\n")
167 );
168
169 assert_eq!(
171 input,
172 line_ending.denormalize(" <- First Line\n Second Line\n "),
173 );
174 }
175
176 #[test]
177 fn test_mixed_indentation() {
178 let input = r#"First Line
179 Second Line
180Third Line
181 "#;
182
183 let line_ending = LineEnding::from(input);
184
185 assert_eq!(
187 auto_indent(input),
188 line_ending.denormalize("First Line\n Second Line\nThird Line\n",)
189 );
190
191 assert_eq!(
193 input,
194 line_ending.denormalize("First Line\n Second Line\nThird Line\n "),
195 );
196 }
197
198 #[test]
199 fn test_single_line_no_change() {
200 let input = "Single line no change";
201
202 let line_ending = LineEnding::from(input);
203
204 assert_eq!(
206 auto_indent(input),
207 line_ending.denormalize("Single line no change")
208 );
209
210 assert_eq!(input, line_ending.denormalize("Single line no change"));
212 }
213
214 #[test]
215 fn test_multiple_blank_lines() {
216 let input = r#"First Line
217
218 A
219
220 B
221
222 C
223
224 D
225
226 E
227 "#;
228
229 let line_ending = LineEnding::from(input);
230
231 assert_eq!(
233 auto_indent(input),
234 line_ending.denormalize("First Line\n\n A\n\n B\n\n C\n\n D\n\nE\n")
235 );
236
237 assert_eq!(
239 input,
240 line_ending.denormalize(
241 "First Line\n \n A\n\n B\n\n C\n\n D\n\n E\n "
242 ),
243 );
244 }
245}