Skip to main content

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