1 //! Functions for wrapping text.
2
3 use std::borrow::Cow;
4
5 use crate::core::{break_words, display_width, Word};
6 use crate::word_splitters::split_words;
7 use crate::Options;
8
9 /// Wrap a line of text at a given width.
10 ///
11 /// The result is a vector of lines, each line is of type [`Cow<'_,
12 /// str>`](Cow), which means that the line will borrow from the input
13 /// `&str` if possible. The lines do not have trailing whitespace,
14 /// including a final `'\n'`. Please use [`fill()`](crate::fill()) if
15 /// you need a [`String`] instead.
16 ///
17 /// The easiest way to use this function is to pass an integer for
18 /// `width_or_options`:
19 ///
20 /// ```
21 /// use textwrap::wrap;
22 ///
23 /// let lines = wrap("Memory safety without garbage collection.", 15);
24 /// assert_eq!(lines, &[
25 /// "Memory safety",
26 /// "without garbage",
27 /// "collection.",
28 /// ]);
29 /// ```
30 ///
31 /// If you need to customize the wrapping, you can pass an [`Options`]
32 /// instead of an `usize`:
33 ///
34 /// ```
35 /// use textwrap::{wrap, Options};
36 ///
37 /// let options = Options::new(15)
38 /// .initial_indent("- ")
39 /// .subsequent_indent(" ");
40 /// let lines = wrap("Memory safety without garbage collection.", &options);
41 /// assert_eq!(lines, &[
42 /// "- Memory safety",
43 /// " without",
44 /// " garbage",
45 /// " collection.",
46 /// ]);
47 /// ```
48 ///
49 /// # Optimal-Fit Wrapping
50 ///
51 /// By default, `wrap` will try to ensure an even right margin by
52 /// finding breaks which avoid short lines. We call this an
53 /// “optimal-fit algorithm” since the line breaks are computed by
54 /// considering all possible line breaks. The alternative is a
55 /// “first-fit algorithm” which simply accumulates words until they no
56 /// longer fit on the line.
57 ///
58 /// As an example, using the first-fit algorithm to wrap the famous
59 /// Hamlet quote “To be, or not to be: that is the question” in a
60 /// narrow column with room for only 10 characters looks like this:
61 ///
62 /// ```
63 /// # use textwrap::{WrapAlgorithm::FirstFit, Options, wrap};
64 /// #
65 /// # let lines = wrap("To be, or not to be: that is the question",
66 /// # Options::new(10).wrap_algorithm(FirstFit));
67 /// # assert_eq!(lines.join("\n") + "\n", "\
68 /// To be, or
69 /// not to be:
70 /// that is
71 /// the
72 /// question
73 /// # ");
74 /// ```
75 ///
76 /// Notice how the second to last line is quite narrow because
77 /// “question” was too large to fit? The greedy first-fit algorithm
78 /// doesn’t look ahead, so it has no other option than to put
79 /// “question” onto its own line.
80 ///
81 /// With the optimal-fit wrapping algorithm, the previous lines are
82 /// shortened slightly in order to make the word “is” go into the
83 /// second last line:
84 ///
85 /// ```
86 /// # #[cfg(feature = "smawk")] {
87 /// # use textwrap::{Options, WrapAlgorithm, wrap};
88 /// #
89 /// # let lines = wrap(
90 /// # "To be, or not to be: that is the question",
91 /// # Options::new(10).wrap_algorithm(WrapAlgorithm::new_optimal_fit())
92 /// # );
93 /// # assert_eq!(lines.join("\n") + "\n", "\
94 /// To be,
95 /// or not to
96 /// be: that
97 /// is the
98 /// question
99 /// # "); }
100 /// ```
101 ///
102 /// Please see [`WrapAlgorithm`](crate::WrapAlgorithm) for details on
103 /// the choices.
104 ///
105 /// # Examples
106 ///
107 /// The returned iterator yields lines of type `Cow<'_, str>`. If
108 /// possible, the wrapped lines will borrow from the input string. As
109 /// an example, a hanging indentation, the first line can borrow from
110 /// the input, but the subsequent lines become owned strings:
111 ///
112 /// ```
113 /// use std::borrow::Cow::{Borrowed, Owned};
114 /// use textwrap::{wrap, Options};
115 ///
116 /// let options = Options::new(15).subsequent_indent("....");
117 /// let lines = wrap("Wrapping text all day long.", &options);
118 /// let annotated = lines
119 /// .iter()
120 /// .map(|line| match line {
121 /// Borrowed(text) => format!("[Borrowed] {}", text),
122 /// Owned(text) => format!("[Owned] {}", text),
123 /// })
124 /// .collect::<Vec<_>>();
125 /// assert_eq!(
126 /// annotated,
127 /// &[
128 /// "[Borrowed] Wrapping text",
129 /// "[Owned] ....all day",
130 /// "[Owned] ....long.",
131 /// ]
132 /// );
133 /// ```
134 ///
135 /// ## Leading and Trailing Whitespace
136 ///
137 /// As a rule, leading whitespace (indentation) is preserved and
138 /// trailing whitespace is discarded.
139 ///
140 /// In more details, when wrapping words into lines, words are found
141 /// by splitting the input text on space characters. One or more
142 /// spaces (shown here as “␣”) are attached to the end of each word:
143 ///
144 /// ```text
145 /// "Foo␣␣␣bar␣baz" -> ["Foo␣␣␣", "bar␣", "baz"]
146 /// ```
147 ///
148 /// These words are then put into lines. The interword whitespace is
149 /// preserved, unless the lines are wrapped so that the `"Foo␣␣␣"`
150 /// word falls at the end of a line:
151 ///
152 /// ```
153 /// use textwrap::wrap;
154 ///
155 /// assert_eq!(wrap("Foo bar baz", 10), vec!["Foo bar", "baz"]);
156 /// assert_eq!(wrap("Foo bar baz", 8), vec!["Foo", "bar baz"]);
157 /// ```
158 ///
159 /// Notice how the trailing whitespace is removed in both case: in the
160 /// first example, `"bar␣"` becomes `"bar"` and in the second case
161 /// `"Foo␣␣␣"` becomes `"Foo"`.
162 ///
163 /// Leading whitespace is preserved when the following word fits on
164 /// the first line. To understand this, consider how words are found
165 /// in a text with leading spaces:
166 ///
167 /// ```text
168 /// "␣␣foo␣bar" -> ["␣␣", "foo␣", "bar"]
169 /// ```
170 ///
171 /// When put into lines, the indentation is preserved if `"foo"` fits
172 /// on the first line, otherwise you end up with an empty line:
173 ///
174 /// ```
175 /// use textwrap::wrap;
176 ///
177 /// assert_eq!(wrap(" foo bar", 8), vec![" foo", "bar"]);
178 /// assert_eq!(wrap(" foo bar", 4), vec!["", "foo", "bar"]);
179 /// ```
wrap<'a, Opt>(text: &str, width_or_options: Opt) -> Vec<Cow<'_, str>> where Opt: Into<Options<'a>>,180 pub fn wrap<'a, Opt>(text: &str, width_or_options: Opt) -> Vec<Cow<'_, str>>
181 where
182 Opt: Into<Options<'a>>,
183 {
184 let options: Options = width_or_options.into();
185 let line_ending_str = options.line_ending.as_str();
186
187 let mut lines = Vec::new();
188 for line in text.split(line_ending_str) {
189 wrap_single_line(line, &options, &mut lines);
190 }
191
192 lines
193 }
194
wrap_single_line<'a>( line: &'a str, options: &Options<'_>, lines: &mut Vec<Cow<'a, str>>, )195 pub(crate) fn wrap_single_line<'a>(
196 line: &'a str,
197 options: &Options<'_>,
198 lines: &mut Vec<Cow<'a, str>>,
199 ) {
200 let indent = if lines.is_empty() {
201 options.initial_indent
202 } else {
203 options.subsequent_indent
204 };
205 if line.len() < options.width && indent.is_empty() {
206 lines.push(Cow::from(line.trim_end_matches(' ')));
207 } else {
208 wrap_single_line_slow_path(line, options, lines)
209 }
210 }
211
212 /// Wrap a single line of text.
213 ///
214 /// This is taken when `line` is longer than `options.width`.
wrap_single_line_slow_path<'a>( line: &'a str, options: &Options<'_>, lines: &mut Vec<Cow<'a, str>>, )215 pub(crate) fn wrap_single_line_slow_path<'a>(
216 line: &'a str,
217 options: &Options<'_>,
218 lines: &mut Vec<Cow<'a, str>>,
219 ) {
220 let initial_width = options
221 .width
222 .saturating_sub(display_width(options.initial_indent));
223 let subsequent_width = options
224 .width
225 .saturating_sub(display_width(options.subsequent_indent));
226 let line_widths = [initial_width, subsequent_width];
227
228 let words = options.word_separator.find_words(line);
229 let split_words = split_words(words, &options.word_splitter);
230 let broken_words = if options.break_words {
231 let mut broken_words = break_words(split_words, line_widths[1]);
232 if !options.initial_indent.is_empty() {
233 // Without this, the first word will always go into the
234 // first line. However, since we break words based on the
235 // _second_ line width, it can be wrong to unconditionally
236 // put the first word onto the first line. An empty
237 // zero-width word fixed this.
238 broken_words.insert(0, Word::from(""));
239 }
240 broken_words
241 } else {
242 split_words.collect::<Vec<_>>()
243 };
244
245 let wrapped_words = options.wrap_algorithm.wrap(&broken_words, &line_widths);
246
247 let mut idx = 0;
248 for words in wrapped_words {
249 let last_word = match words.last() {
250 None => {
251 lines.push(Cow::from(""));
252 continue;
253 }
254 Some(word) => word,
255 };
256
257 // We assume here that all words are contiguous in `line`.
258 // That is, the sum of their lengths should add up to the
259 // length of `line`.
260 let len = words
261 .iter()
262 .map(|word| word.len() + word.whitespace.len())
263 .sum::<usize>()
264 - last_word.whitespace.len();
265
266 // The result is owned if we have indentation, otherwise we
267 // can simply borrow an empty string.
268 let mut result = if lines.is_empty() && !options.initial_indent.is_empty() {
269 Cow::Owned(options.initial_indent.to_owned())
270 } else if !lines.is_empty() && !options.subsequent_indent.is_empty() {
271 Cow::Owned(options.subsequent_indent.to_owned())
272 } else {
273 // We can use an empty string here since string
274 // concatenation for `Cow` preserves a borrowed value when
275 // either side is empty.
276 Cow::from("")
277 };
278
279 result += &line[idx..idx + len];
280
281 if !last_word.penalty.is_empty() {
282 result.to_mut().push_str(last_word.penalty);
283 }
284
285 lines.push(result);
286
287 // Advance by the length of `result`, plus the length of
288 // `last_word.whitespace` -- even if we had a penalty, we need
289 // to skip over the whitespace.
290 idx += len + last_word.whitespace.len();
291 }
292 }
293
294 #[cfg(test)]
295 mod tests {
296 use super::*;
297 use crate::{WordSeparator, WordSplitter, WrapAlgorithm};
298
299 #[cfg(feature = "hyphenation")]
300 use hyphenation::{Language, Load, Standard};
301
302 #[test]
no_wrap()303 fn no_wrap() {
304 assert_eq!(wrap("foo", 10), vec!["foo"]);
305 }
306
307 #[test]
wrap_simple()308 fn wrap_simple() {
309 assert_eq!(wrap("foo bar baz", 5), vec!["foo", "bar", "baz"]);
310 }
311
312 #[test]
to_be_or_not()313 fn to_be_or_not() {
314 assert_eq!(
315 wrap(
316 "To be, or not to be, that is the question.",
317 Options::new(10).wrap_algorithm(WrapAlgorithm::FirstFit)
318 ),
319 vec!["To be, or", "not to be,", "that is", "the", "question."]
320 );
321 }
322
323 #[test]
multiple_words_on_first_line()324 fn multiple_words_on_first_line() {
325 assert_eq!(wrap("foo bar baz", 10), vec!["foo bar", "baz"]);
326 }
327
328 #[test]
long_word()329 fn long_word() {
330 assert_eq!(wrap("foo", 0), vec!["f", "o", "o"]);
331 }
332
333 #[test]
long_words()334 fn long_words() {
335 assert_eq!(wrap("foo bar", 0), vec!["f", "o", "o", "b", "a", "r"]);
336 }
337
338 #[test]
max_width()339 fn max_width() {
340 assert_eq!(wrap("foo bar", usize::MAX), vec!["foo bar"]);
341
342 let text = "Hello there! This is some English text. \
343 It should not be wrapped given the extents below.";
344 assert_eq!(wrap(text, usize::MAX), vec![text]);
345 }
346
347 #[test]
leading_whitespace()348 fn leading_whitespace() {
349 assert_eq!(wrap(" foo bar", 6), vec![" foo", "bar"]);
350 }
351
352 #[test]
leading_whitespace_empty_first_line()353 fn leading_whitespace_empty_first_line() {
354 // If there is no space for the first word, the first line
355 // will be empty. This is because the string is split into
356 // words like [" ", "foobar ", "baz"], which puts "foobar " on
357 // the second line. We never output trailing whitespace
358 assert_eq!(wrap(" foobar baz", 6), vec!["", "foobar", "baz"]);
359 }
360
361 #[test]
trailing_whitespace()362 fn trailing_whitespace() {
363 // Whitespace is only significant inside a line. After a line
364 // gets too long and is broken, the first word starts in
365 // column zero and is not indented.
366 assert_eq!(wrap("foo bar baz ", 5), vec!["foo", "bar", "baz"]);
367 }
368
369 #[test]
issue_99()370 fn issue_99() {
371 // We did not reset the in_whitespace flag correctly and did
372 // not handle single-character words after a line break.
373 assert_eq!(
374 wrap("aaabbbccc x yyyzzzwww", 9),
375 vec!["aaabbbccc", "x", "yyyzzzwww"]
376 );
377 }
378
379 #[test]
issue_129()380 fn issue_129() {
381 // The dash is an em-dash which takes up four bytes. We used
382 // to panic since we tried to index into the character.
383 let options = Options::new(1).word_separator(WordSeparator::AsciiSpace);
384 assert_eq!(wrap("x – x", options), vec!["x", "–", "x"]);
385 }
386
387 #[test]
wide_character_handling()388 fn wide_character_handling() {
389 assert_eq!(wrap("Hello, World!", 15), vec!["Hello, World!"]);
390 assert_eq!(
391 wrap(
392 "Hello, World!",
393 Options::new(15).word_separator(WordSeparator::AsciiSpace)
394 ),
395 vec!["Hello,", "World!"]
396 );
397
398 // Wide characters are allowed to break if the
399 // unicode-linebreak feature is enabled.
400 #[cfg(feature = "unicode-linebreak")]
401 assert_eq!(
402 wrap(
403 "Hello, World!",
404 Options::new(15).word_separator(WordSeparator::UnicodeBreakProperties),
405 ),
406 vec!["Hello, W", "orld!"]
407 );
408 }
409
410 #[test]
indent_empty_line()411 fn indent_empty_line() {
412 // Previously, indentation was not applied to empty lines.
413 // However, this is somewhat inconsistent and undesirable if
414 // the indentation is something like a border ("| ") which you
415 // want to apply to all lines, empty or not.
416 let options = Options::new(10).initial_indent("!!!");
417 assert_eq!(wrap("", &options), vec!["!!!"]);
418 }
419
420 #[test]
indent_single_line()421 fn indent_single_line() {
422 let options = Options::new(10).initial_indent(">>>"); // No trailing space
423 assert_eq!(wrap("foo", &options), vec![">>>foo"]);
424 }
425
426 #[test]
indent_first_emoji()427 fn indent_first_emoji() {
428 let options = Options::new(10).initial_indent("");
429 assert_eq!(
430 wrap("x x x x x x x x x x x x x", &options),
431 vec!["x x x", "x x x x x", "x x x x x"]
432 );
433 }
434
435 #[test]
indent_multiple_lines()436 fn indent_multiple_lines() {
437 let options = Options::new(6).initial_indent("* ").subsequent_indent(" ");
438 assert_eq!(
439 wrap("foo bar baz", &options),
440 vec!["* foo", " bar", " baz"]
441 );
442 }
443
444 #[test]
only_initial_indent_multiple_lines()445 fn only_initial_indent_multiple_lines() {
446 let options = Options::new(10).initial_indent(" ");
447 assert_eq!(wrap("foo\nbar\nbaz", &options), vec![" foo", "bar", "baz"]);
448 }
449
450 #[test]
only_subsequent_indent_multiple_lines()451 fn only_subsequent_indent_multiple_lines() {
452 let options = Options::new(10).subsequent_indent(" ");
453 assert_eq!(
454 wrap("foo\nbar\nbaz", &options),
455 vec!["foo", " bar", " baz"]
456 );
457 }
458
459 #[test]
indent_break_words()460 fn indent_break_words() {
461 let options = Options::new(5).initial_indent("* ").subsequent_indent(" ");
462 assert_eq!(wrap("foobarbaz", &options), vec!["* foo", " bar", " baz"]);
463 }
464
465 #[test]
initial_indent_break_words()466 fn initial_indent_break_words() {
467 // This is a corner-case showing how the long word is broken
468 // according to the width of the subsequent lines. The first
469 // fragment of the word no longer fits on the first line,
470 // which ends up being pure indentation.
471 let options = Options::new(5).initial_indent("-->");
472 assert_eq!(wrap("foobarbaz", &options), vec!["-->", "fooba", "rbaz"]);
473 }
474
475 #[test]
hyphens()476 fn hyphens() {
477 assert_eq!(wrap("foo-bar", 5), vec!["foo-", "bar"]);
478 }
479
480 #[test]
trailing_hyphen()481 fn trailing_hyphen() {
482 let options = Options::new(5).break_words(false);
483 assert_eq!(wrap("foobar-", &options), vec!["foobar-"]);
484 }
485
486 #[test]
multiple_hyphens()487 fn multiple_hyphens() {
488 assert_eq!(wrap("foo-bar-baz", 5), vec!["foo-", "bar-", "baz"]);
489 }
490
491 #[test]
hyphens_flag()492 fn hyphens_flag() {
493 let options = Options::new(5).break_words(false);
494 assert_eq!(
495 wrap("The --foo-bar flag.", &options),
496 vec!["The", "--foo-", "bar", "flag."]
497 );
498 }
499
500 #[test]
repeated_hyphens()501 fn repeated_hyphens() {
502 let options = Options::new(4).break_words(false);
503 assert_eq!(wrap("foo--bar", &options), vec!["foo--bar"]);
504 }
505
506 #[test]
hyphens_alphanumeric()507 fn hyphens_alphanumeric() {
508 assert_eq!(wrap("Na2-CH4", 5), vec!["Na2-", "CH4"]);
509 }
510
511 #[test]
hyphens_non_alphanumeric()512 fn hyphens_non_alphanumeric() {
513 let options = Options::new(5).break_words(false);
514 assert_eq!(wrap("foo(-)bar", &options), vec!["foo(-)bar"]);
515 }
516
517 #[test]
multiple_splits()518 fn multiple_splits() {
519 assert_eq!(wrap("foo-bar-baz", 9), vec!["foo-bar-", "baz"]);
520 }
521
522 #[test]
forced_split()523 fn forced_split() {
524 let options = Options::new(5).break_words(false);
525 assert_eq!(wrap("foobar-baz", &options), vec!["foobar-", "baz"]);
526 }
527
528 #[test]
multiple_unbroken_words_issue_193()529 fn multiple_unbroken_words_issue_193() {
530 let options = Options::new(3).break_words(false);
531 assert_eq!(
532 wrap("small large tiny", &options),
533 vec!["small", "large", "tiny"]
534 );
535 assert_eq!(
536 wrap("small large tiny", &options),
537 vec!["small", "large", "tiny"]
538 );
539 }
540
541 #[test]
very_narrow_lines_issue_193()542 fn very_narrow_lines_issue_193() {
543 let options = Options::new(1).break_words(false);
544 assert_eq!(wrap("fooo x y", &options), vec!["fooo", "x", "y"]);
545 assert_eq!(wrap("fooo x y", &options), vec!["fooo", "x", "y"]);
546 }
547
548 #[test]
simple_hyphens()549 fn simple_hyphens() {
550 let options = Options::new(8).word_splitter(WordSplitter::HyphenSplitter);
551 assert_eq!(wrap("foo bar-baz", &options), vec!["foo bar-", "baz"]);
552 }
553
554 #[test]
no_hyphenation()555 fn no_hyphenation() {
556 let options = Options::new(8).word_splitter(WordSplitter::NoHyphenation);
557 assert_eq!(wrap("foo bar-baz", &options), vec!["foo", "bar-baz"]);
558 }
559
560 #[test]
561 #[cfg(feature = "hyphenation")]
auto_hyphenation_double_hyphenation()562 fn auto_hyphenation_double_hyphenation() {
563 let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap();
564 let options = Options::new(10);
565 assert_eq!(
566 wrap("Internationalization", &options),
567 vec!["Internatio", "nalization"]
568 );
569
570 let options = Options::new(10).word_splitter(WordSplitter::Hyphenation(dictionary));
571 assert_eq!(
572 wrap("Internationalization", &options),
573 vec!["Interna-", "tionaliza-", "tion"]
574 );
575 }
576
577 #[test]
578 #[cfg(feature = "hyphenation")]
auto_hyphenation_issue_158()579 fn auto_hyphenation_issue_158() {
580 let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap();
581 let options = Options::new(10);
582 assert_eq!(
583 wrap("participation is the key to success", &options),
584 vec!["participat", "ion is", "the key to", "success"]
585 );
586
587 let options = Options::new(10).word_splitter(WordSplitter::Hyphenation(dictionary));
588 assert_eq!(
589 wrap("participation is the key to success", &options),
590 vec!["partici-", "pation is", "the key to", "success"]
591 );
592 }
593
594 #[test]
595 #[cfg(feature = "hyphenation")]
split_len_hyphenation()596 fn split_len_hyphenation() {
597 // Test that hyphenation takes the width of the whitespace
598 // into account.
599 let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap();
600 let options = Options::new(15).word_splitter(WordSplitter::Hyphenation(dictionary));
601 assert_eq!(
602 wrap("garbage collection", &options),
603 vec!["garbage col-", "lection"]
604 );
605 }
606
607 #[test]
608 #[cfg(feature = "hyphenation")]
borrowed_lines()609 fn borrowed_lines() {
610 // Lines that end with an extra hyphen are owned, the final
611 // line is borrowed.
612 use std::borrow::Cow::{Borrowed, Owned};
613 let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap();
614 let options = Options::new(10).word_splitter(WordSplitter::Hyphenation(dictionary));
615 let lines = wrap("Internationalization", &options);
616 assert_eq!(lines, vec!["Interna-", "tionaliza-", "tion"]);
617 if let Borrowed(s) = lines[0] {
618 assert!(false, "should not have been borrowed: {:?}", s);
619 }
620 if let Borrowed(s) = lines[1] {
621 assert!(false, "should not have been borrowed: {:?}", s);
622 }
623 if let Owned(ref s) = lines[2] {
624 assert!(false, "should not have been owned: {:?}", s);
625 }
626 }
627
628 #[test]
629 #[cfg(feature = "hyphenation")]
auto_hyphenation_with_hyphen()630 fn auto_hyphenation_with_hyphen() {
631 let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap();
632 let options = Options::new(8).break_words(false);
633 assert_eq!(
634 wrap("over-caffinated", &options),
635 vec!["over-", "caffinated"]
636 );
637
638 let options = options.word_splitter(WordSplitter::Hyphenation(dictionary));
639 assert_eq!(
640 wrap("over-caffinated", &options),
641 vec!["over-", "caffi-", "nated"]
642 );
643 }
644
645 #[test]
break_words()646 fn break_words() {
647 assert_eq!(wrap("foobarbaz", 3), vec!["foo", "bar", "baz"]);
648 }
649
650 #[test]
break_words_wide_characters()651 fn break_words_wide_characters() {
652 // Even the poor man's version of `ch_width` counts these
653 // characters as wide.
654 let options = Options::new(5).word_separator(WordSeparator::AsciiSpace);
655 assert_eq!(wrap("Hello", options), vec!["He", "ll", "o"]);
656 }
657
658 #[test]
break_words_zero_width()659 fn break_words_zero_width() {
660 assert_eq!(wrap("foobar", 0), vec!["f", "o", "o", "b", "a", "r"]);
661 }
662
663 #[test]
break_long_first_word()664 fn break_long_first_word() {
665 assert_eq!(wrap("testx y", 4), vec!["test", "x y"]);
666 }
667
668 #[test]
wrap_preserves_line_breaks_trims_whitespace()669 fn wrap_preserves_line_breaks_trims_whitespace() {
670 assert_eq!(wrap(" ", 80), vec![""]);
671 assert_eq!(wrap(" \n ", 80), vec!["", ""]);
672 assert_eq!(wrap(" \n \n \n ", 80), vec!["", "", "", ""]);
673 }
674
675 #[test]
wrap_colored_text()676 fn wrap_colored_text() {
677 // The words are much longer than 6 bytes, but they remain
678 // intact after filling the text.
679 let green_hello = "\u{1b}[0m\u{1b}[32mHello\u{1b}[0m";
680 let blue_world = "\u{1b}[0m\u{1b}[34mWorld!\u{1b}[0m";
681 assert_eq!(
682 wrap(&format!("{} {}", green_hello, blue_world), 6),
683 vec![green_hello, blue_world],
684 );
685 }
686 }
687