string_auto_indent/
lib.rs

1#[cfg(doctest)]
2doc_comment::doctest!("../README.md");
3
4pub use line_ending::LineEnding;
5
6/// Struct that encapsulates auto-indentation logic.
7struct AutoIndent {
8    line_ending: LineEnding,
9}
10
11impl AutoIndent {
12    /// Creates a new instance by detecting the line ending from the input.
13    fn new(input: &str) -> Self {
14        Self {
15            line_ending: LineEnding::from(input),
16        }
17    }
18
19    /// Applies auto-indentation rules.
20    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        // Take first line exactly as is
28        let first_line = Some(lines.remove(0));
29
30        // Find the minimum indentation for all remaining lines
31        let min_indent = lines
32            .iter()
33            .filter(|line| !line.trim().is_empty()) // Ignore empty lines
34            .map(|line| line.chars().take_while(|c| c.is_whitespace()).count())
35            .min()
36            .unwrap_or(0);
37
38        // Adjust indentation for all lines except the first
39        let mut result: Vec<String> = Vec::new();
40
41        if let Some(first) = first_line {
42            result.push(first.to_string()); // Preserve the first line exactly
43        }
44
45        result.extend(lines.iter().map(|line| {
46            if line.trim().is_empty() {
47                String::new() // Convert empty lines to actual empty lines
48            } else {
49                line.chars().skip(min_indent).collect() // Trim only relative indentation
50            }
51        }));
52
53        // Ensure the final line is empty if it originally contained only whitespace
54        if result.last().map(|s| s.trim()).unwrap_or("").is_empty() {
55            *result.last_mut().unwrap() = String::new();
56        }
57
58        // Preserve the original trailing newline behavior
59        self.line_ending.join(result)
60    }
61}
62
63/// Auto-indents a string while preserving original line endings.
64pub 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        // Read file contents
80        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        // Validate the content was actually read
96        let lines = LineEnding::split(&readme_contents);
97        assert_eq!(lines.first().unwrap(), "# Multi-line String Auto-Indent");
98
99        // Ensure the README has more than 5 lines
100        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        // With auto-indent
118        assert_eq!(
119            auto_indent(input),
120            // string_replace_all("Basic Test\n1\n    2\n        3\n", "\n", e.as_str())
121            line_ending.denormalize("Basic Test\n1\n    2\n        3\n")
122        );
123
124        // Without auto-indent
125        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        // With auto-indent
143        assert_eq!(
144            auto_indent(input),
145            line_ending.denormalize("\n1\n    2\n        3\n")
146        );
147
148        // Without auto-indent
149        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        // With auto-indent
164        assert_eq!(
165            auto_indent(input),
166            line_ending.denormalize("     <- First Line\nSecond Line\n")
167        );
168
169        // Without auto-indent
170        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        // With auto-indent
186        assert_eq!(
187            auto_indent(input),
188            line_ending.denormalize("First Line\n        Second Line\nThird Line\n",)
189        );
190
191        // Without auto-indent
192        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        // With auto-indent
205        assert_eq!(
206            auto_indent(input),
207            line_ending.denormalize("Single line no change")
208        );
209
210        // Without auto-indent
211        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        // With auto-indent
232        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        // Without auto-indent
238        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}