textwrap/
indentation.rs

1//! Functions related to adding and removing indentation from lines of
2//! text.
3//!
4//! The functions here can be used to uniformly indent or dedent
5//! (unindent) word wrapped lines of text.
6
7/// Add prefix to each non-empty line.
8///
9/// ```
10/// use textwrap::indent;
11///
12/// assert_eq!(indent("
13/// Foo
14/// Bar
15/// ", "  "), "
16///   Foo
17///   Bar
18/// ");
19/// ```
20///
21/// Empty lines (lines consisting only of whitespace) are not indented
22/// and the whitespace is replaced by a single newline (`\n`):
23///
24/// ```
25/// use textwrap::indent;
26///
27/// assert_eq!(indent("
28/// Foo
29///
30/// Bar
31///   \t
32/// Baz
33/// ", "->"), "
34/// ->Foo
35///
36/// ->Bar
37///
38/// ->Baz
39/// ");
40/// ```
41///
42/// Leading and trailing whitespace on non-empty lines is kept
43/// unchanged:
44///
45/// ```
46/// use textwrap::indent;
47///
48/// assert_eq!(indent(" \t  Foo   ", "->"), "-> \t  Foo   \n");
49/// ```
50pub fn indent(s: &str, prefix: &str) -> String {
51    let mut result = String::new();
52    for line in s.lines() {
53        if line.chars().any(|c| !c.is_whitespace()) {
54            result.push_str(prefix);
55            result.push_str(line);
56        }
57        result.push('\n');
58    }
59    result
60}
61
62/// Removes common leading whitespace from each line.
63///
64/// This function will look at each non-empty line and determine the
65/// maximum amount of whitespace that can be removed from all lines:
66///
67/// ```
68/// use textwrap::dedent;
69///
70/// assert_eq!(dedent("
71///     1st line
72///       2nd line
73///     3rd line
74/// "), "
75/// 1st line
76///   2nd line
77/// 3rd line
78/// ");
79/// ```
80pub fn dedent(s: &str) -> String {
81    let mut prefix = "";
82    let mut lines = s.lines();
83
84    // We first search for a non-empty line to find a prefix.
85    for line in &mut lines {
86        let mut whitespace_idx = line.len();
87        for (idx, ch) in line.char_indices() {
88            if !ch.is_whitespace() {
89                whitespace_idx = idx;
90                break;
91            }
92        }
93
94        // Check if the line had anything but whitespace
95        if whitespace_idx < line.len() {
96            prefix = &line[..whitespace_idx];
97            break;
98        }
99    }
100
101    // We then continue looking through the remaining lines to
102    // possibly shorten the prefix.
103    for line in &mut lines {
104        let mut whitespace_idx = line.len();
105        for ((idx, a), b) in line.char_indices().zip(prefix.chars()) {
106            if a != b {
107                whitespace_idx = idx;
108                break;
109            }
110        }
111
112        // Check if the line had anything but whitespace and if we
113        // have found a shorter prefix
114        if whitespace_idx < line.len() && whitespace_idx < prefix.len() {
115            prefix = &line[..whitespace_idx];
116        }
117    }
118
119    // We now go over the lines a second time to build the result.
120    let mut result = String::new();
121    for line in s.lines() {
122        if line.starts_with(&prefix) && line.chars().any(|c| !c.is_whitespace()) {
123            let (_, tail) = line.split_at(prefix.len());
124            result.push_str(tail);
125        }
126        result.push('\n');
127    }
128
129    if result.ends_with('\n') && !s.ends_with('\n') {
130        let new_len = result.len() - 1;
131        result.truncate(new_len);
132    }
133
134    result
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    /// Add newlines. Ensures that the final line in the vector also
142    /// has a newline.
143    fn add_nl(lines: &[&str]) -> String {
144        lines.join("\n") + "\n"
145    }
146
147    #[test]
148    fn indent_empty() {
149        assert_eq!(indent("\n", "  "), "\n");
150    }
151
152    #[test]
153    #[cfg_attr(rustfmt, rustfmt_skip)]
154    fn indent_nonempty() {
155        let x = vec!["  foo",
156                     "bar",
157                     "  baz"];
158        let y = vec!["//  foo",
159                     "//bar",
160                     "//  baz"];
161        assert_eq!(indent(&add_nl(&x), "//"), add_nl(&y));
162    }
163
164    #[test]
165    #[cfg_attr(rustfmt, rustfmt_skip)]
166    fn indent_empty_line() {
167        let x = vec!["  foo",
168                     "bar",
169                     "",
170                     "  baz"];
171        let y = vec!["//  foo",
172                     "//bar",
173                     "",
174                     "//  baz"];
175        assert_eq!(indent(&add_nl(&x), "//"), add_nl(&y));
176    }
177
178    #[test]
179    fn dedent_empty() {
180        assert_eq!(dedent(""), "");
181    }
182
183    #[test]
184    #[cfg_attr(rustfmt, rustfmt_skip)]
185    fn dedent_multi_line() {
186        let x = vec!["    foo",
187                     "  bar",
188                     "    baz"];
189        let y = vec!["  foo",
190                     "bar",
191                     "  baz"];
192        assert_eq!(dedent(&add_nl(&x)), add_nl(&y));
193    }
194
195    #[test]
196    #[cfg_attr(rustfmt, rustfmt_skip)]
197    fn dedent_empty_line() {
198        let x = vec!["    foo",
199                     "  bar",
200                     "   ",
201                     "    baz"];
202        let y = vec!["  foo",
203                     "bar",
204                     "",
205                     "  baz"];
206        assert_eq!(dedent(&add_nl(&x)), add_nl(&y));
207    }
208
209    #[test]
210    #[cfg_attr(rustfmt, rustfmt_skip)]
211    fn dedent_blank_line() {
212        let x = vec!["      foo",
213                     "",
214                     "        bar",
215                     "          foo",
216                     "          bar",
217                     "          baz"];
218        let y = vec!["foo",
219                     "",
220                     "  bar",
221                     "    foo",
222                     "    bar",
223                     "    baz"];
224        assert_eq!(dedent(&add_nl(&x)), add_nl(&y));
225    }
226
227    #[test]
228    #[cfg_attr(rustfmt, rustfmt_skip)]
229    fn dedent_whitespace_line() {
230        let x = vec!["      foo",
231                     " ",
232                     "        bar",
233                     "          foo",
234                     "          bar",
235                     "          baz"];
236        let y = vec!["foo",
237                     "",
238                     "  bar",
239                     "    foo",
240                     "    bar",
241                     "    baz"];
242        assert_eq!(dedent(&add_nl(&x)), add_nl(&y));
243    }
244
245    #[test]
246    #[cfg_attr(rustfmt, rustfmt_skip)]
247    fn dedent_mixed_whitespace() {
248        let x = vec!["\tfoo",
249                     "  bar"];
250        let y = vec!["\tfoo",
251                     "  bar"];
252        assert_eq!(dedent(&add_nl(&x)), add_nl(&y));
253    }
254
255    #[test]
256    #[cfg_attr(rustfmt, rustfmt_skip)]
257    fn dedent_tabbed_whitespace() {
258        let x = vec!["\t\tfoo",
259                     "\t\t\tbar"];
260        let y = vec!["foo",
261                     "\tbar"];
262        assert_eq!(dedent(&add_nl(&x)), add_nl(&y));
263    }
264
265    #[test]
266    #[cfg_attr(rustfmt, rustfmt_skip)]
267    fn dedent_mixed_tabbed_whitespace() {
268        let x = vec!["\t  \tfoo",
269                     "\t  \t\tbar"];
270        let y = vec!["foo",
271                     "\tbar"];
272        assert_eq!(dedent(&add_nl(&x)), add_nl(&y));
273    }
274
275    #[test]
276    #[cfg_attr(rustfmt, rustfmt_skip)]
277    fn dedent_mixed_tabbed_whitespace2() {
278        let x = vec!["\t  \tfoo",
279                     "\t    \tbar"];
280        let y = vec!["\tfoo",
281                     "  \tbar"];
282        assert_eq!(dedent(&add_nl(&x)), add_nl(&y));
283    }
284
285    #[test]
286    #[cfg_attr(rustfmt, rustfmt_skip)]
287    fn dedent_preserve_no_terminating_newline() {
288        let x = vec!["  foo",
289                     "    bar"].join("\n");
290        let y = vec!["foo",
291                     "  bar"].join("\n");
292        assert_eq!(dedent(&x), y);
293    }
294}