formatparse_core/
indent_block.rs1pub fn strip_common_indent(s: &str) -> String {
9 let lines: Vec<String> = s
10 .split('\n')
11 .map(|p| p.strip_suffix('\r').unwrap_or(p).to_string())
12 .collect();
13
14 let mut margin: Option<usize> = None;
15 for line in &lines {
16 if line.is_empty() {
17 continue;
18 }
19 if line.chars().all(|c| c == ' ' || c == '\t') {
20 continue;
21 }
22 let indent = line.chars().take_while(|c| *c == ' ' || *c == '\t').count();
23 margin = Some(match margin {
24 None => indent,
25 Some(m) => m.min(indent),
26 });
27 }
28 let m = margin.unwrap_or(0);
29 if m == 0 {
30 return s.to_string();
31 }
32
33 let mut out = String::with_capacity(s.len());
34 for (i, line) in lines.iter().enumerate() {
35 if i > 0 {
36 out.push('\n');
37 }
38 let lead = line.chars().take_while(|c| *c == ' ' || *c == '\t').count();
39 let rest: String = if lead >= m {
40 line.chars().skip(m).collect()
41 } else {
42 line.trim_start_matches([' ', '\t']).to_string()
43 };
44 out.push_str(&rest);
45 }
46 out.trim_start_matches('\n').to_string()
47}
48
49#[cfg(test)]
50mod tests {
51 use super::*;
52
53 #[test]
54 fn empty_string() {
55 assert_eq!(strip_common_indent(""), "");
56 }
57
58 #[test]
59 fn no_leading_indent_unchanged() {
60 assert_eq!(strip_common_indent("a\nb"), "a\nb");
61 }
62
63 #[test]
64 fn strips_common_spaces() {
65 assert_eq!(strip_common_indent(" a\n b"), "a\nb");
66 }
67
68 #[test]
69 fn blank_lines_do_not_set_margin() {
70 assert_eq!(strip_common_indent(" a\n\n b"), "a\n\nb");
71 }
72
73 #[test]
74 fn crlf_segments() {
75 assert_eq!(strip_common_indent(" a\r\n b"), "a\nb");
76 }
77
78 #[test]
79 fn leading_newline_after_dedent_trimmed() {
80 assert_eq!(strip_common_indent("\n a\n b"), "a\nb");
81 }
82
83 #[test]
84 fn tabs_count_as_single_indent_chars() {
85 assert_eq!(strip_common_indent("\tx\n\ty"), "x\ny");
86 }
87}