1#[must_use]
2pub fn unindent(text: &str) -> String {
3 let mut lines = Vec::new();
5 let mut start = 0;
6 for (i, c) in text.char_indices() {
7 if c == '\n' || i == text.len() - c.len_utf8() {
8 let end = i + c.len_utf8();
9 lines.push(&text[start..end]);
10 start = end;
11 }
12 }
13
14 let common_indentation = lines
15 .iter()
16 .filter(|line| !blank(line))
17 .copied()
18 .map(indentation)
19 .fold(
20 None,
21 |common_indentation, line_indentation| match common_indentation {
22 Some(common_indentation) => Some(common(common_indentation, line_indentation)),
23 None => Some(line_indentation),
24 },
25 )
26 .unwrap_or("");
27
28 let mut replacements = Vec::with_capacity(lines.len());
29
30 for (i, line) in lines.iter().enumerate() {
31 let blank = blank(line);
32 let first = i == 0;
33 let last = i == lines.len() - 1;
34
35 let replacement = match (blank, first, last) {
36 (true, false, false) => "\n",
37 (true, _, _) => "",
38 (false, _, _) => &line[common_indentation.len()..],
39 };
40
41 replacements.push(replacement);
42 }
43
44 replacements.into_iter().collect()
45}
46
47fn indentation(line: &str) -> &str {
48 let i = line
49 .char_indices()
50 .take_while(|(_, c)| matches!(c, ' ' | '\t'))
51 .map(|(i, _)| i + 1)
52 .last()
53 .unwrap_or(0);
54
55 &line[..i]
56}
57
58fn blank(line: &str) -> bool {
59 line.chars().all(|c| matches!(c, ' ' | '\t' | '\r' | '\n'))
60}
61
62fn common<'s>(a: &'s str, b: &'s str) -> &'s str {
63 let i = a
64 .char_indices()
65 .zip(b.chars())
66 .take_while(|((_, ac), bc)| ac == bc)
67 .map(|((i, c), _)| i + c.len_utf8())
68 .last()
69 .unwrap_or(0);
70
71 &a[0..i]
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 #[test]
79 fn unindents() {
80 assert_eq!(unindent("foo"), "foo");
81 assert_eq!(unindent("foo\nbar\nbaz\n"), "foo\nbar\nbaz\n");
82 assert_eq!(unindent(""), "");
83 assert_eq!(unindent(" foo\n bar"), "foo\nbar");
84 assert_eq!(unindent(" foo\n bar\n\n"), "foo\nbar\n");
85
86 assert_eq!(
87 unindent(
88 "
89 hello
90 bar
91 "
92 ),
93 "hello\nbar\n"
94 );
95
96 assert_eq!(unindent("hello\n bar\n foo"), "hello\n bar\n foo");
97
98 assert_eq!(
99 unindent(
100 "
101
102 hello
103 bar
104
105 "
106 ),
107 "\nhello\nbar\n\n"
108 );
109 }
110
111 #[test]
112 fn indentations() {
113 assert_eq!(indentation(""), "");
114 assert_eq!(indentation("foo"), "");
115 assert_eq!(indentation(" foo"), " ");
116 assert_eq!(indentation("\t\tfoo"), "\t\t");
117 assert_eq!(indentation("\t \t foo"), "\t \t ");
118 }
119
120 #[test]
121 fn blanks() {
122 assert!(blank(" \n"));
123 assert!(!blank(" foo\n"));
124 assert!(blank("\t\t\n"));
125 }
126
127 #[test]
128 fn commons() {
129 assert_eq!(common("foo", "foobar"), "foo");
130 assert_eq!(common("foo", "bar"), "");
131 assert_eq!(common("", ""), "");
132 assert_eq!(common("", "bar"), "");
133 }
134}