1use std::io::{Result, Write};
2
3#[derive(Clone, Copy)]
4pub struct IndentInfo {
6 pub first_line: usize,
8 pub last_line: usize,
10 pub leading_whitespace: usize,
12}
13
14impl IndentInfo {
15 pub fn new(input: &str) -> Option<IndentInfo> {
17 let mut first_char_line = None;
18 let mut last_char_line = 0;
19 let mut common: Option<usize> = None;
20
21 for (index, line) in input.lines().enumerate() {
22 let ws = line
23 .bytes()
24 .take_while(|&c| c.is_ascii_whitespace())
25 .count();
26
27 if ws == line.len() {
28 continue;
29 }
30
31 first_char_line.get_or_insert(index);
32 last_char_line = index;
33
34 common = match common {
35 None => Some(ws),
36 Some(x) => Some(x.min(ws)),
37 };
38 }
39
40 first_char_line.map(|x| IndentInfo {
41 first_line: x,
42 last_line: last_char_line,
43 leading_whitespace: common.unwrap_or(0),
44 })
45 }
46}
47
48pub struct Deindenter<'a> {
51 indent_info: IndentInfo,
52 input: &'a str,
53}
54
55impl Deindenter<'_> {
56 pub fn new(input: &str) -> Option<Deindenter> {
58 let indent_info = IndentInfo::new(input)?;
59
60 Some(Deindenter { indent_info, input })
61 }
62
63 pub fn to_writer<T: Write>(&self, mut out: T) -> Result<()> {
66 let IndentInfo {
67 first_line,
68 last_line,
69 leading_whitespace,
70 } = self.indent_info;
71
72 for line in self
73 .input
74 .split_inclusive('\n')
75 .skip(first_line)
76 .take(1 + last_line - first_line)
77 {
78 let trim = line.bytes().take(leading_whitespace).len();
79 let skip = if trim < leading_whitespace {
80 0 } else {
82 trim
83 };
84
85 out.write_all(&line.as_bytes()[skip..])?;
86 }
87
88 Ok(())
89 }
90
91 pub fn to_string(&self) -> Result<String> {
99 let mut buf = Vec::new();
100 self.to_writer(&mut buf)?;
101
102 let s = String::from_utf8(buf).expect("deindented string contains non-utf8 text");
103 Ok(s)
104 }
105
106 pub fn indent_info(&self) -> IndentInfo {
108 self.indent_info
109 }
110}
111
112impl From<Deindenter<'_>> for IndentInfo {
113 fn from(value: Deindenter) -> Self {
114 value.indent_info
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use crate::Deindenter;
121
122 fn test(input: &str, expected: &str) {
123 let d = Deindenter::new(input).unwrap();
124 assert_eq!(d.to_string().unwrap(), expected);
125 }
126
127 const EXPECTED: &str = r#"impl From<Deindenter<'_>> for IndentInfo {
128 fn from(value: Deindenter) -> Self {
129 value.indent_info
130 }
131}
132"#;
133
134 #[test]
135 fn extra_whitespace_lines() {
136 let input = r#"
137
138impl From<Deindenter<'_>> for IndentInfo {
139 fn from(value: Deindenter) -> Self {
140 value.indent_info
141 }
142}
143
144"#;
145
146 test(input, EXPECTED);
147 }
148
149 #[test]
150 fn noop() {
151 let input = r#"impl From<Deindenter<'_>> for IndentInfo {
152 fn from(value: Deindenter) -> Self {
153 value.indent_info
154 }
155}
156"#;
157
158 test(input, input);
159 }
160
161 #[test]
162 fn indented() {
163 let input = r#" impl From<Deindenter<'_>> for IndentInfo {
164 fn from(value: Deindenter) -> Self {
165 value.indent_info
166 }
167 }
168"#;
169
170 test(input, EXPECTED);
171 }
172
173 #[test]
174 fn almost_indented() {
175 let input = r#"impl From<Deindenter<'_>> for IndentInfo {
177 fn from(value: Deindenter) -> Self {
178 value.indent_info
179 }
180 }
181"#;
182
183 test(input, input);
184 }
185
186 #[test]
187 fn no_trailing_newline() {
188 let input = r#" impl From<Deindenter<'_>> for IndentInfo {
189 fn from(value: Deindenter) -> Self {
190 value.indent_info
191 }
192 }"#;
193
194 let expected = r#"impl From<Deindenter<'_>> for IndentInfo {
195 fn from(value: Deindenter) -> Self {
196 value.indent_info
197 }
198}"#;
199
200 test(input, expected);
201 }
202
203 #[test]
204 fn multiple_paragraphs() {
205 let mut input = r#"
206 this is p1
207
208 this is p2"#
209 .to_owned();
210
211 let mut expected = r#"this is p1
212
213this is p2"#
214 .to_owned();
215
216 test(&input, &expected);
217
218 input.push('\n');
220 expected.push('\n');
221
222 test(&input, &expected);
223 }
224}