1use alloc::borrow::{Cow, ToOwned};
4use alloc::vec::Vec;
5
6use crate::Options;
7use crate::core::{Word, break_words, display_width};
8use crate::word_splitters::split_words;
9
10pub fn wrap<'a, Opt>(text: &str, width_or_options: Opt) -> Vec<Cow<'_, str>>
182where
183 Opt: Into<Options<'a>>,
184{
185 let options: Options = width_or_options.into();
186 let line_ending_str = options.line_ending.as_str();
187
188 let mut lines = Vec::new();
189 for line in text.split(line_ending_str) {
190 wrap_single_line(line, &options, &mut lines);
191 }
192
193 lines
194}
195
196pub(crate) fn wrap_single_line<'a>(
197 line: &'a str,
198 options: &Options<'_>,
199 lines: &mut Vec<Cow<'a, str>>,
200) {
201 let indent = if lines.is_empty() {
202 options.initial_indent
203 } else {
204 options.subsequent_indent
205 };
206 if line.len() < options.width && indent.is_empty() {
207 if options.preserve_trailing_space {
208 lines.push(Cow::from(line));
209 } else {
210 lines.push(Cow::from(line.trim_end_matches(' ')));
211 }
212 } else {
213 wrap_single_line_slow_path(line, options, lines)
214 }
215}
216
217pub(crate) fn wrap_single_line_slow_path<'a>(
221 line: &'a str,
222 options: &Options<'_>,
223 lines: &mut Vec<Cow<'a, str>>,
224) {
225 let initial_width = options
226 .width
227 .saturating_sub(display_width(options.initial_indent));
228 let subsequent_width = options
229 .width
230 .saturating_sub(display_width(options.subsequent_indent));
231 let line_widths = [initial_width, subsequent_width];
232
233 let words = options.word_separator.find_words(line);
234 let split_words = split_words(words, &options.word_splitter);
235 let broken_words = if options.break_words {
236 let mut broken_words = break_words(split_words, line_widths[1]);
237 if !options.initial_indent.is_empty() {
238 broken_words.insert(0, Word::from(""));
244 }
245 broken_words
246 } else {
247 split_words.collect::<Vec<_>>()
248 };
249
250 let wrapped_words = options.wrap_algorithm.wrap(&broken_words, &line_widths);
251
252 let mut idx = 0;
253 for words in wrapped_words {
254 let last_word = match words.last() {
255 None => {
256 lines.push(Cow::from(""));
257 continue;
258 }
259 Some(word) => word,
260 };
261
262 let len = words
266 .iter()
267 .map(|word| word.len() + word.whitespace.len())
268 .sum::<usize>();
269
270 let mut result = if lines.is_empty() && !options.initial_indent.is_empty() {
273 Cow::Owned(options.initial_indent.to_owned())
274 } else if !lines.is_empty() && !options.subsequent_indent.is_empty() {
275 Cow::Owned(options.subsequent_indent.to_owned())
276 } else {
277 Cow::from("")
281 };
282
283 if last_word.penalty.is_empty() && options.preserve_trailing_space {
284 result += &line[idx..idx + len];
285 } else {
286 result += &line[idx..idx + len - last_word.whitespace.len()];
287 }
288
289 if !last_word.penalty.is_empty() {
290 result.to_mut().push_str(last_word.penalty);
291 }
292
293 lines.push(result);
294
295 idx += len;
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305 use crate::{WordSeparator, WordSplitter, WrapAlgorithm};
306 #[cfg(not(feature = "std"))]
307 use alloc::{format, vec};
308
309 #[cfg(feature = "hyphenation")]
310 use hyphenation::{Language, Load, Standard};
311
312 #[test]
313 fn no_wrap() {
314 assert_eq!(wrap("foo", 10), vec!["foo"]);
315 }
316
317 #[test]
318 fn wrap_simple() {
319 assert_eq!(wrap("foo bar baz", 5), vec!["foo", "bar", "baz"]);
320 }
321
322 #[test]
323 fn to_be_or_not() {
324 assert_eq!(
325 wrap(
326 "To be, or not to be, that is the question.",
327 Options::new(10).wrap_algorithm(WrapAlgorithm::FirstFit)
328 ),
329 vec!["To be, or", "not to be,", "that is", "the", "question."]
330 );
331 }
332
333 #[test]
334 fn multiple_words_on_first_line() {
335 assert_eq!(wrap("foo bar baz", 10), vec!["foo bar", "baz"]);
336 }
337
338 #[test]
339 fn long_word() {
340 assert_eq!(wrap("foo", 0), vec!["f", "o", "o"]);
341 }
342
343 #[test]
344 fn long_words() {
345 assert_eq!(wrap("foo bar", 0), vec!["f", "o", "o", "b", "a", "r"]);
346 }
347
348 #[test]
349 fn max_width() {
350 assert_eq!(wrap("foo bar", usize::MAX), vec!["foo bar"]);
351
352 let text = "Hello there! This is some English text. \
353 It should not be wrapped given the extents below.";
354 assert_eq!(wrap(text, usize::MAX), vec![text]);
355 }
356
357 #[test]
358 fn leading_whitespace() {
359 assert_eq!(wrap(" foo bar", 6), vec![" foo", "bar"]);
360 }
361
362 #[test]
363 fn leading_whitespace_empty_first_line() {
364 assert_eq!(wrap(" foobar baz", 6), vec!["", "foobar", "baz"]);
369 }
370
371 #[test]
372 fn trailing_whitespace() {
373 assert_eq!(wrap("foo bar baz ", 5), vec!["foo", "bar", "baz"]);
377 }
378
379 #[test]
380 fn preserve_trailing_whitespace() {
381 let options = Options::new(5).preserve_trailing_space(true);
382 assert_eq!(
385 wrap("foo bar baz ", &options),
386 vec!["foo ", "bar ", "baz "]
387 );
388 }
389
390 #[test]
391 fn issue_99() {
392 assert_eq!(
395 wrap("aaabbbccc x yyyzzzwww", 9),
396 vec!["aaabbbccc", "x", "yyyzzzwww"]
397 );
398 }
399
400 #[test]
401 fn issue_129() {
402 let options = Options::new(1).word_separator(WordSeparator::AsciiSpace);
405 assert_eq!(wrap("x – x", options), vec!["x", "–", "x"]);
406 }
407
408 #[test]
409 fn wide_character_handling() {
410 assert_eq!(wrap("Hello, World!", 15), vec!["Hello, World!"]);
411 assert_eq!(
412 wrap(
413 "Hello, World!",
414 Options::new(15).word_separator(WordSeparator::AsciiSpace)
415 ),
416 vec!["Hello,", "World!"]
417 );
418
419 #[cfg(feature = "unicode-linebreak")]
422 assert_eq!(
423 wrap(
424 "Hello, World!",
425 Options::new(15).word_separator(WordSeparator::UnicodeBreakProperties),
426 ),
427 vec!["Hello, W", "orld!"]
428 );
429 }
430
431 #[test]
432 #[cfg(feature = "unicode-width")]
433 fn wrap_wide_characters_unicode_width() {
434 let options = Options::new(8);
437 assert_eq!(wrap("안녕하세요", &options), vec!["안녕하세", "요"]);
438 }
439
440 #[test]
441 fn indent_empty_line() {
442 let options = Options::new(10).initial_indent("!!!");
447 assert_eq!(wrap("", &options), vec!["!!!"]);
448 }
449
450 #[test]
451 fn indent_single_line() {
452 let options = Options::new(10).initial_indent(">>>"); assert_eq!(wrap("foo", &options), vec![">>>foo"]);
454 }
455
456 #[test]
457 fn indent_first_emoji() {
458 let options = Options::new(10).initial_indent("👉👉");
459 assert_eq!(
460 wrap("x x x x x x x x x x x x x", &options),
461 vec!["👉👉x x x", "x x x x x", "x x x x x"]
462 );
463 }
464
465 #[test]
466 fn indent_multiple_lines() {
467 let options = Options::new(6).initial_indent("* ").subsequent_indent(" ");
468 assert_eq!(
469 wrap("foo bar baz", &options),
470 vec!["* foo", " bar", " baz"]
471 );
472 }
473
474 #[test]
475 fn only_initial_indent_multiple_lines() {
476 let options = Options::new(10).initial_indent(" ");
477 assert_eq!(wrap("foo\nbar\nbaz", &options), vec![" foo", "bar", "baz"]);
478 }
479
480 #[test]
481 fn only_subsequent_indent_multiple_lines() {
482 let options = Options::new(10).subsequent_indent(" ");
483 assert_eq!(
484 wrap("foo\nbar\nbaz", &options),
485 vec!["foo", " bar", " baz"]
486 );
487 }
488
489 #[test]
490 fn indent_break_words() {
491 let options = Options::new(5).initial_indent("* ").subsequent_indent(" ");
492 assert_eq!(wrap("foobarbaz", &options), vec!["* foo", " bar", " baz"]);
493 }
494
495 #[test]
496 fn initial_indent_break_words() {
497 let options = Options::new(5).initial_indent("-->");
502 assert_eq!(wrap("foobarbaz", &options), vec!["-->", "fooba", "rbaz"]);
503 }
504
505 #[test]
506 fn hyphens() {
507 assert_eq!(wrap("foo-bar", 5), vec!["foo-", "bar"]);
508 }
509
510 #[test]
511 fn trailing_hyphen() {
512 let options = Options::new(5).break_words(false);
513 assert_eq!(wrap("foobar-", &options), vec!["foobar-"]);
514 }
515
516 #[test]
517 fn multiple_hyphens() {
518 assert_eq!(wrap("foo-bar-baz", 5), vec!["foo-", "bar-", "baz"]);
519 }
520
521 #[test]
522 fn hyphens_flag() {
523 let options = Options::new(5).break_words(false);
524 assert_eq!(
525 wrap("The --foo-bar flag.", &options),
526 vec!["The", "--foo-", "bar", "flag."]
527 );
528 }
529
530 #[test]
531 fn repeated_hyphens() {
532 let options = Options::new(4).break_words(false);
533 assert_eq!(wrap("foo--bar", &options), vec!["foo--bar"]);
534 }
535
536 #[test]
537 fn hyphens_alphanumeric() {
538 assert_eq!(wrap("Na2-CH4", 5), vec!["Na2-", "CH4"]);
539 }
540
541 #[test]
542 fn hyphens_non_alphanumeric() {
543 let options = Options::new(5).break_words(false);
544 assert_eq!(wrap("foo(-)bar", &options), vec!["foo(-)bar"]);
545 }
546
547 #[test]
548 fn multiple_splits() {
549 assert_eq!(wrap("foo-bar-baz", 9), vec!["foo-bar-", "baz"]);
550 }
551
552 #[test]
553 fn forced_split() {
554 let options = Options::new(5).break_words(false);
555 assert_eq!(wrap("foobar-baz", &options), vec!["foobar-", "baz"]);
556 }
557
558 #[test]
559 fn multiple_unbroken_words_issue_193() {
560 let options = Options::new(3).break_words(false);
561 assert_eq!(
562 wrap("small large tiny", &options),
563 vec!["small", "large", "tiny"]
564 );
565 assert_eq!(
566 wrap("small large tiny", &options),
567 vec!["small", "large", "tiny"]
568 );
569 }
570
571 #[test]
572 fn very_narrow_lines_issue_193() {
573 let options = Options::new(1).break_words(false);
574 assert_eq!(wrap("fooo x y", &options), vec!["fooo", "x", "y"]);
575 assert_eq!(wrap("fooo x y", &options), vec!["fooo", "x", "y"]);
576 }
577
578 #[test]
579 fn simple_hyphens() {
580 let options = Options::new(8).word_splitter(WordSplitter::HyphenSplitter);
581 assert_eq!(wrap("foo bar-baz", &options), vec!["foo bar-", "baz"]);
582 }
583
584 #[test]
585 fn no_hyphenation() {
586 let options = Options::new(8).word_splitter(WordSplitter::NoHyphenation);
587 assert_eq!(wrap("foo bar-baz", &options), vec!["foo", "bar-baz"]);
588 }
589
590 #[test]
591 #[cfg(feature = "hyphenation")]
592 fn auto_hyphenation_double_hyphenation() {
593 let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap();
594 let options = Options::new(10);
595 assert_eq!(
596 wrap("Internationalization", &options),
597 vec!["Internatio", "nalization"]
598 );
599
600 let options = Options::new(10).word_splitter(WordSplitter::Hyphenation(dictionary));
601 assert_eq!(
602 wrap("Internationalization", &options),
603 vec!["Interna-", "tionaliza-", "tion"]
604 );
605 }
606
607 #[test]
608 #[cfg(feature = "hyphenation")]
609 fn auto_hyphenation_issue_158() {
610 let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap();
611 let options = Options::new(10);
612 assert_eq!(
613 wrap("participation is the key to success", &options),
614 vec!["participat", "ion is", "the key to", "success"]
615 );
616
617 let options = Options::new(10).word_splitter(WordSplitter::Hyphenation(dictionary));
618 assert_eq!(
619 wrap("participation is the key to success", &options),
620 vec!["partici-", "pation is", "the key to", "success"]
621 );
622 }
623
624 #[test]
625 #[cfg(feature = "hyphenation")]
626 fn split_len_hyphenation() {
627 let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap();
630 let options = Options::new(15).word_splitter(WordSplitter::Hyphenation(dictionary));
631 assert_eq!(
632 wrap("garbage collection", &options),
633 vec!["garbage col-", "lection"]
634 );
635 }
636
637 #[test]
638 #[cfg(feature = "hyphenation")]
639 fn borrowed_lines() {
640 use alloc::borrow::Cow::{Borrowed, Owned};
643 let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap();
644 let options = Options::new(10).word_splitter(WordSplitter::Hyphenation(dictionary));
645 let lines = wrap("Internationalization", &options);
646 assert_eq!(lines, vec!["Interna-", "tionaliza-", "tion"]);
647 if let Borrowed(s) = lines[0] {
648 assert!(false, "should not have been borrowed: {:?}", s);
649 }
650 if let Borrowed(s) = lines[1] {
651 assert!(false, "should not have been borrowed: {:?}", s);
652 }
653 if let Owned(ref s) = lines[2] {
654 assert!(false, "should not have been owned: {:?}", s);
655 }
656 }
657
658 #[test]
659 #[cfg(feature = "hyphenation")]
660 fn auto_hyphenation_with_hyphen() {
661 let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap();
662 let options = Options::new(8).break_words(false);
663 assert_eq!(
664 wrap("over-caffinated", &options),
665 vec!["over-", "caffinated"]
666 );
667
668 let options = options.word_splitter(WordSplitter::Hyphenation(dictionary));
669 assert_eq!(
670 wrap("over-caffinated", &options),
671 vec!["over-", "caffi-", "nated"]
672 );
673 }
674
675 #[test]
676 fn break_words() {
677 assert_eq!(wrap("foobarbaz", 3), vec!["foo", "bar", "baz"]);
678 }
679
680 #[test]
681 fn break_words_wide_characters() {
682 let options = Options::new(5).word_separator(WordSeparator::AsciiSpace);
685 assert_eq!(wrap("Hello", options), vec!["He", "ll", "o"]);
686 }
687
688 #[test]
689 fn break_words_zero_width() {
690 assert_eq!(wrap("foobar", 0), vec!["f", "o", "o", "b", "a", "r"]);
691 }
692
693 #[test]
694 fn break_long_first_word() {
695 assert_eq!(wrap("testx y", 4), vec!["test", "x y"]);
696 }
697
698 #[test]
699 fn wrap_preserves_line_breaks_trims_whitespace() {
700 assert_eq!(wrap(" ", 80), vec![""]);
701 assert_eq!(wrap(" \n ", 80), vec!["", ""]);
702 assert_eq!(wrap(" \n \n \n ", 80), vec!["", "", "", ""]);
703 }
704
705 #[test]
706 fn wrap_preserves_line_breaks_and_trailing_whitespace() {
707 let options = Options::new(80).preserve_trailing_space(true);
708 assert_eq!(wrap(" ", &options), vec![" "]);
709 assert_eq!(wrap(" \n ", &options), vec![" ", " "]);
710 assert_eq!(wrap(" \n \n \n ", &options), vec![" ", " ", " ", " "]);
711 }
712
713 #[test]
714 fn preserve_trailing_space_borrows_spaces() {
715 use alloc::borrow::Cow::Owned;
716 let lines = wrap(
717 "foo bar baz",
718 Options::new(10).preserve_trailing_space(true),
719 );
720 assert_eq!(lines, vec!["foo bar ", "baz"]);
721 if let Owned(ref s) = lines[0] {
722 assert!(false, "should not have been owned: {:?}", s);
723 }
724 }
725
726 #[test]
727 fn wrap_colored_text() {
728 let green_hello = "\u{1b}[0m\u{1b}[32mHello\u{1b}[0m";
731 let blue_world = "\u{1b}[0m\u{1b}[34mWorld!\u{1b}[0m";
732 assert_eq!(
733 wrap(&format!("{} {}", green_hello, blue_world), 6),
734 vec![green_hello, blue_world],
735 );
736 }
737}