1use std::iter::Peekable;
2use std::slice::Split;
3
4pub fn unindent(s: &str) -> String {
5 let bytes: &[u8] = s.as_bytes();
6 let unindented: Vec = unindent_bytes(bytes);
7 String::from_utf8(vec:unindented).unwrap()
8}
9
10// Compute the maximal number of spaces that can be removed from every line, and
11// remove them.
12pub fn unindent_bytes(s: &[u8]) -> Vec<u8> {
13 // Document may start either on the same line as opening quote or
14 // on the next line
15 let ignore_first_line = s.starts_with(b"\n") || s.starts_with(b"\r\n");
16
17 // Largest number of spaces that can be removed from every
18 // non-whitespace-only line after the first
19 let spaces = s
20 .lines()
21 .skip(1)
22 .filter_map(count_spaces)
23 .min()
24 .unwrap_or(0);
25
26 let mut result = Vec::with_capacity(s.len());
27 for (i, line) in s.lines().enumerate() {
28 if i > 1 || (i == 1 && !ignore_first_line) {
29 result.push(b'\n');
30 }
31 if i == 0 {
32 // Do not un-indent anything on same line as opening quote
33 result.extend_from_slice(line);
34 } else if line.len() > spaces {
35 // Whitespace-only lines may have fewer than the number of spaces
36 // being removed
37 result.extend_from_slice(&line[spaces..]);
38 }
39 }
40 result
41}
42
43pub trait Unindent {
44 type Output;
45
46 fn unindent(&self) -> Self::Output;
47}
48
49impl Unindent for str {
50 type Output = String;
51
52 fn unindent(&self) -> Self::Output {
53 unindent(self)
54 }
55}
56
57impl Unindent for String {
58 type Output = String;
59
60 fn unindent(&self) -> Self::Output {
61 unindent(self)
62 }
63}
64
65impl Unindent for [u8] {
66 type Output = Vec<u8>;
67
68 fn unindent(&self) -> Self::Output {
69 unindent_bytes(self)
70 }
71}
72
73impl<'a, T: ?Sized + Unindent> Unindent for &'a T {
74 type Output = T::Output;
75
76 fn unindent(&self) -> Self::Output {
77 (**self).unindent()
78 }
79}
80
81// Number of leading spaces in the line, or None if the line is entirely spaces.
82fn count_spaces(line: &[u8]) -> Option<usize> {
83 for (i: usize, ch: &u8) in line.iter().enumerate() {
84 if *ch != b' ' && *ch != b'\t' {
85 return Some(i);
86 }
87 }
88 None
89}
90
91// Based on core::str::StrExt.
92trait BytesExt {
93 fn lines(&self) -> Lines;
94}
95
96impl BytesExt for [u8] {
97 fn lines(&self) -> Lines {
98 fn is_newline(b: &u8) -> bool {
99 *b == b'\n'
100 }
101 let bytestring: &[u8] = if self.starts_with(needle:b"\r\n") {
102 &self[1..]
103 } else {
104 self
105 };
106 Lines {
107 split: bytestring.split(pred:is_newline as fn(&u8) -> bool).peekable(),
108 }
109 }
110}
111
112struct Lines<'a> {
113 split: Peekable<Split<'a, u8, fn(&u8) -> bool>>,
114}
115
116impl<'a> Iterator for Lines<'a> {
117 type Item = &'a [u8];
118
119 fn next(&mut self) -> Option<Self::Item> {
120 match self.split.next() {
121 None => None,
122 Some(fragment: &[u8]) => {
123 if fragment.is_empty() && self.split.peek().is_none() {
124 None
125 } else {
126 Some(fragment)
127 }
128 }
129 }
130 }
131}
132