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/// Indent each line by the given prefix.
8///
9/// # Examples
10///
11/// ```
12/// use textwrap::indent;
13///
14/// assert_eq!(indent("First line.\nSecond line.\n", "  "),
15///            "  First line.\n  Second line.\n");
16/// ```
17///
18/// When indenting, trailing whitespace is stripped from the prefix.
19/// This means that empty lines remain empty afterwards:
20///
21/// ```
22/// use textwrap::indent;
23///
24/// assert_eq!(indent("First line.\n\n\nSecond line.\n", "  "),
25///            "  First line.\n\n\n  Second line.\n");
26/// ```
27///
28/// Notice how `"\n\n\n"` remained as `"\n\n\n"`.
29///
30/// This feature is useful when you want to indent text and have a
31/// space between your prefix and the text. In this case, you _don't_
32/// want a trailing space on empty lines:
33///
34/// ```
35/// use textwrap::indent;
36///
37/// assert_eq!(indent("foo = 123\n\nprint(foo)\n", "# "),
38///            "# foo = 123\n#\n# print(foo)\n");
39/// ```
40///
41/// Notice how `"\n\n"` became `"\n#\n"` instead of `"\n# \n"` which
42/// would have trailing whitespace.
43///
44/// Leading and trailing whitespace coming from the text itself is
45/// kept unchanged:
46///
47/// ```
48/// use textwrap::indent;
49///
50/// assert_eq!(indent(" \t  Foo   ", "->"), "-> \t  Foo   ");
51/// ```
52pub fn indent(s: &str, prefix: &str) -> String {
53    // We know we'll need more than s.len() bytes for the output, but
54    // without counting '\n' characters (which is somewhat slow), we
55    // don't know exactly how much. However, we can preemptively do
56    // the first doubling of the output size.
57    let mut result = String::with_capacity(2 * s.len());
58    let trimmed_prefix = prefix.trim_end();
59    for (idx, line) in s.split_terminator('\n').enumerate() {
60        if idx > 0 {
61            result.push('\n');
62        }
63        if line.trim().is_empty() {
64            result.push_str(trimmed_prefix);
65        } else {
66            result.push_str(prefix);
67        }
68        result.push_str(line);
69    }
70    if s.ends_with('\n') {
71        // split_terminator will have eaten the final '\n'.
72        result.push('\n');
73    }
74    result
75}
76
77/// Removes common leading whitespace from each line.
78///
79/// This function will look at each non-empty line and determine the
80/// maximum amount of whitespace that can be removed from all lines:
81///
82/// ```
83/// use textwrap::dedent;
84///
85/// assert_eq!(dedent("
86///     1st line
87///       2nd line
88///     3rd line
89/// "), "
90/// 1st line
91///   2nd line
92/// 3rd line
93/// ");
94/// ```
95pub fn dedent(s: &str) -> String {
96    let mut prefix = "";
97    let mut lines = s.lines();
98
99    // We first search for a non-empty line to find a prefix.
100    for line in &mut lines {
101        let mut whitespace_idx = line.len();
102        for (idx, ch) in line.char_indices() {
103            if !ch.is_whitespace() {
104                whitespace_idx = idx;
105                break;
106            }
107        }
108
109        // Check if the line had anything but whitespace
110        if whitespace_idx < line.len() {
111            prefix = &line[..whitespace_idx];
112            break;
113        }
114    }
115
116    // We then continue looking through the remaining lines to
117    // possibly shorten the prefix.
118    for line in &mut lines {
119        let mut whitespace_idx = line.len();
120        for ((idx, a), b) in line.char_indices().zip(prefix.chars()) {
121            if a != b {
122                whitespace_idx = idx;
123                break;
124            }
125        }
126
127        // Check if the line had anything but whitespace and if we
128        // have found a shorter prefix
129        if whitespace_idx < line.len() && whitespace_idx < prefix.len() {
130            prefix = &line[..whitespace_idx];
131        }
132    }
133
134    // We now go over the lines a second time to build the result.
135    let mut result = String::new();
136    for line in s.lines() {
137        if line.starts_with(prefix) && line.chars().any(|c| !c.is_whitespace()) {
138            let (_, tail) = line.split_at(prefix.len());
139            result.push_str(tail);
140        }
141        result.push('\n');
142    }
143
144    if result.ends_with('\n') && !s.ends_with('\n') {
145        let new_len = result.len() - 1;
146        result.truncate(new_len);
147    }
148
149    result
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn indent_empty() {
158        assert_eq!(indent("\n", "  "), "\n");
159    }
160
161    #[test]
162    #[rustfmt::skip]
163    fn indent_nonempty() {
164        let text = [
165            "  foo\n",
166            "bar\n",
167            "  baz\n",
168        ].join("");
169        let expected = [
170            "//   foo\n",
171            "// bar\n",
172            "//   baz\n",
173        ].join("");
174        assert_eq!(indent(&text, "// "), expected);
175    }
176
177    #[test]
178    #[rustfmt::skip]
179    fn indent_empty_line() {
180        let text = [
181            "  foo",
182            "bar",
183            "",
184            "  baz",
185        ].join("\n");
186        let expected = [
187            "//   foo",
188            "// bar",
189            "//",
190            "//   baz",
191        ].join("\n");
192        assert_eq!(indent(&text, "// "), expected);
193    }
194
195    #[test]
196    fn dedent_empty() {
197        assert_eq!(dedent(""), "");
198    }
199
200    #[test]
201    #[rustfmt::skip]
202    fn dedent_multi_line() {
203        let x = [
204            "    foo",
205            "  bar",
206            "    baz",
207        ].join("\n");
208        let y = [
209            "  foo",
210            "bar",
211            "  baz"
212        ].join("\n");
213        assert_eq!(dedent(&x), y);
214    }
215
216    #[test]
217    #[rustfmt::skip]
218    fn dedent_empty_line() {
219        let x = [
220            "    foo",
221            "  bar",
222            "   ",
223            "    baz"
224        ].join("\n");
225        let y = [
226            "  foo",
227            "bar",
228            "",
229            "  baz"
230        ].join("\n");
231        assert_eq!(dedent(&x), y);
232    }
233
234    #[test]
235    #[rustfmt::skip]
236    fn dedent_blank_line() {
237        let x = [
238            "      foo",
239            "",
240            "        bar",
241            "          foo",
242            "          bar",
243            "          baz",
244        ].join("\n");
245        let y = [
246            "foo",
247            "",
248            "  bar",
249            "    foo",
250            "    bar",
251            "    baz",
252        ].join("\n");
253        assert_eq!(dedent(&x), y);
254    }
255
256    #[test]
257    #[rustfmt::skip]
258    fn dedent_whitespace_line() {
259        let x = [
260            "      foo",
261            " ",
262            "        bar",
263            "          foo",
264            "          bar",
265            "          baz",
266        ].join("\n");
267        let y = [
268            "foo",
269            "",
270            "  bar",
271            "    foo",
272            "    bar",
273            "    baz",
274        ].join("\n");
275        assert_eq!(dedent(&x), y);
276    }
277
278    #[test]
279    #[rustfmt::skip]
280    fn dedent_mixed_whitespace() {
281        let x = [
282            "\tfoo",
283            "  bar",
284        ].join("\n");
285        let y = [
286            "\tfoo",
287            "  bar",
288        ].join("\n");
289        assert_eq!(dedent(&x), y);
290    }
291
292    #[test]
293    #[rustfmt::skip]
294    fn dedent_tabbed_whitespace() {
295        let x = [
296            "\t\tfoo",
297            "\t\t\tbar",
298        ].join("\n");
299        let y = [
300            "foo",
301            "\tbar",
302        ].join("\n");
303        assert_eq!(dedent(&x), y);
304    }
305
306    #[test]
307    #[rustfmt::skip]
308    fn dedent_mixed_tabbed_whitespace() {
309        let x = [
310            "\t  \tfoo",
311            "\t  \t\tbar",
312        ].join("\n");
313        let y = [
314            "foo",
315            "\tbar",
316        ].join("\n");
317        assert_eq!(dedent(&x), y);
318    }
319
320    #[test]
321    #[rustfmt::skip]
322    fn dedent_mixed_tabbed_whitespace2() {
323        let x = [
324            "\t  \tfoo",
325            "\t    \tbar",
326        ].join("\n");
327        let y = [
328            "\tfoo",
329            "  \tbar",
330        ].join("\n");
331        assert_eq!(dedent(&x), y);
332    }
333
334    #[test]
335    #[rustfmt::skip]
336    fn dedent_preserve_no_terminating_newline() {
337        let x = [
338            "  foo",
339            "    bar",
340        ].join("\n");
341        let y = [
342            "foo",
343            "  bar",
344        ].join("\n");
345        assert_eq!(dedent(&x), y);
346    }
347}