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