pub_just/
unindent.rs

1#[must_use]
2pub fn unindent(text: &str) -> String {
3  // find line start and end indices
4  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}