string_auto_indent/
lib.rs

1#[cfg(doctest)]
2doc_comment::doctest!("../README.md");
3
4pub mod line_ending;
5pub use line_ending::LineEnding;
6
7/// Struct that encapsulates auto-indentation logic.
8struct AutoIndent {
9    line_ending: LineEnding,
10}
11
12impl AutoIndent {
13    /// Creates a new instance by detecting the line ending from the input.
14    fn new(input: &str) -> Self {
15        Self {
16            line_ending: LineEnding::detect(input),
17        }
18    }
19
20    /// Applies auto-indentation rules.
21    fn apply(&self, input: &str) -> String {
22        if input.trim().is_empty() {
23            return String::new();
24        }
25
26        // Normalize to `\n` for consistent processing
27        let input = LineEnding::normalize(input);
28        let mut lines: Vec<&str> = input.lines().collect();
29
30        // Track whether the original input ended with a newline
31        let ends_with_newline = input.ends_with('\n');
32
33        // Remove the first line if it's empty
34        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)) // Take first line exactly as is
39        };
40
41        // Find the minimum indentation for all remaining lines
42        let min_indent = lines
43            .iter()
44            .filter(|line| !line.trim().is_empty()) // Ignore empty lines
45            .map(|line| line.chars().take_while(|c| c.is_whitespace()).count())
46            .min()
47            .unwrap_or(0);
48
49        // Adjust indentation for all lines except the first
50        let mut result: Vec<String> = Vec::new();
51
52        if let Some(first) = first_line {
53            result.push(first.to_string()); // Preserve the first line exactly
54        }
55
56        result.extend(lines.iter().map(|line| {
57            if line.trim().is_empty() {
58                String::new() // Convert empty lines to actual empty lines
59            } else {
60                line.chars().skip(min_indent).collect() // Trim only relative indentation
61            }
62        }));
63
64        // Ensure the final line is empty if it originally contained only whitespace
65        if result.last().map(|s| s.trim()).unwrap_or("").is_empty() {
66            *result.last_mut().unwrap() = String::new();
67        }
68
69        // Preserve the original trailing newline behavior
70        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
79/// Auto-indents a string while preserving original line endings.
80pub 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        // With auto-indent
100        assert_eq!(
101            auto_indent(input),
102            // string_replace_all("Basic Test\n1\n    2\n        3\n", "\n", e.as_str())
103            line_ending.restore("Basic Test\n1\n    2\n        3\n")
104        );
105
106        // Without auto-indent
107        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        // With auto-indent
125        assert_eq!(
126            auto_indent(input),
127            line_ending.restore("1\n    2\n        3\n")
128        );
129
130        // Without auto-indent
131        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        // With auto-indent
146        assert_eq!(
147            auto_indent(input),
148            line_ending.restore("     <- First Line\nSecond Line\n")
149        );
150
151        // Without auto-indent
152        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        // With auto-indent
168        assert_eq!(
169            auto_indent(input),
170            line_ending.restore("First Line\n        Second Line\nThird Line\n",)
171        );
172
173        // Without auto-indent
174        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        // With auto-indent
187        assert_eq!(
188            auto_indent(input),
189            line_ending.restore("Single line no change")
190        );
191
192        // Without auto-indent
193        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        // With auto-indent
214        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        // Without auto-indent
220        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}