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 /// ```
indent(s: &str, prefix: &str) -> String52 pub 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 /// ```
dedent(s: &str) -> String95 pub 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)]
153 mod tests {
154     use super::*;
155 
156     #[test]
indent_empty()157     fn indent_empty() {
158         assert_eq!(indent("\n", "  "), "\n");
159     }
160 
161     #[test]
162     #[rustfmt::skip]
indent_nonempty()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]
indent_empty_line()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]
dedent_empty()196     fn dedent_empty() {
197         assert_eq!(dedent(""), "");
198     }
199 
200     #[test]
201     #[rustfmt::skip]
dedent_multi_line()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]
dedent_empty_line()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]
dedent_blank_line()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]
dedent_whitespace_line()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]
dedent_mixed_whitespace()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]
dedent_tabbed_whitespace()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]
dedent_mixed_tabbed_whitespace()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]
dedent_mixed_tabbed_whitespace2()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]
dedent_preserve_no_terminating_newline()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 }
348