Skip to main content

deindent/
lib.rs

1use std::io::{Result, Write};
2
3#[derive(Clone, Copy)]
4/// Indent information from a scanned string.
5pub struct IndentInfo {
6    /// Index of first non-empty line.
7    pub first_line: usize,
8    /// Index of last non-empty line.
9    pub last_line: usize,
10    /// Least number of whitespace character that can be stripped from each line to deindent.
11    pub leading_whitespace: usize,
12}
13
14impl IndentInfo {
15    /// Returns `None` on empty or whitespace-only input.
16    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
48/// An [`IndentInfo`] with a reference to the scanned string, making it possible to write a
49/// deindented copy.
50pub struct Deindenter<'a> {
51    indent_info: IndentInfo,
52    input: &'a str,
53}
54
55impl Deindenter<'_> {
56    /// Returns `None` on empty or whitespace-only input.
57    pub fn new(input: &str) -> Option<Deindenter> {
58        let indent_info = IndentInfo::new(input)?;
59
60        Some(Deindenter { indent_info, input })
61    }
62
63    /// Writes the scanned input with leading whitespace removed.
64    /// Will return an error if `T::write_all` fails.
65    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 // Whitespace-only line.
81            } else {
82                trim
83            };
84
85            out.write_all(&line.as_bytes()[skip..])?;
86        }
87
88        Ok(())
89    }
90
91    /// Writes the scanned input with leading whitespace removed. Allocates a buffer and uses
92    /// `to_writer`. Will error on the same conditions.
93    ///
94    /// # Panics
95    ///
96    /// Will panic if the buffer cannot be converted to a UTF-8 string. Since the input must be a
97    /// valid `&str`, this should never happen.
98    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    /// Copy of the underlying [`IndentInfo`].
107    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        // First row is not indented at all == noop.
176        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        // With trailing newline.
219        input.push('\n');
220        expected.push('\n');
221
222        test(&input, &expected);
223    }
224}