use std::borrow::Cow;
use crate::core::{break_words, display_width, Word};
use crate::word_splitters::split_words;
use crate::Options;
pub fn wrap<'a, Opt>(text: &str, width_or_options: Opt) -> Vec<Cow<'_, str>>
where
Opt: Into<Options<'a>>,
{
let options: Options = width_or_options.into();
let line_ending_str = options.line_ending.as_str();
let mut lines = Vec::new();
for line in text.split(line_ending_str) {
wrap_single_line(line, &options, &mut lines);
}
lines
}
pub(crate) fn wrap_single_line<'a>(
line: &'a str,
options: &Options<'_>,
lines: &mut Vec<Cow<'a, str>>,
) {
let indent = if lines.is_empty() {
options.initial_indent
} else {
options.subsequent_indent
};
if line.len() < options.width && indent.is_empty() {
lines.push(Cow::from(line.trim_end_matches(' ')));
} else {
wrap_single_line_slow_path(line, options, lines)
}
}
pub(crate) fn wrap_single_line_slow_path<'a>(
line: &'a str,
options: &Options<'_>,
lines: &mut Vec<Cow<'a, str>>,
) {
let initial_width = options
.width
.saturating_sub(display_width(options.initial_indent));
let subsequent_width = options
.width
.saturating_sub(display_width(options.subsequent_indent));
let line_widths = [initial_width, subsequent_width];
let words = options.word_separator.find_words(line);
let split_words = split_words(words, &options.word_splitter);
let broken_words = if options.break_words {
let mut broken_words = break_words(split_words, line_widths[1]);
if !options.initial_indent.is_empty() {
broken_words.insert(0, Word::from(""));
}
broken_words
} else {
split_words.collect::<Vec<_>>()
};
let wrapped_words = options.wrap_algorithm.wrap(&broken_words, &line_widths);
let mut idx = 0;
for words in wrapped_words {
let last_word = match words.last() {
None => {
lines.push(Cow::from(""));
continue;
}
Some(word) => word,
};
let len = words
.iter()
.map(|word| word.len() + word.whitespace.len())
.sum::<usize>()
- last_word.whitespace.len();
let mut result = if lines.is_empty() && !options.initial_indent.is_empty() {
Cow::Owned(options.initial_indent.to_owned())
} else if !lines.is_empty() && !options.subsequent_indent.is_empty() {
Cow::Owned(options.subsequent_indent.to_owned())
} else {
Cow::from("")
};
result += &line[idx..idx + len];
if !last_word.penalty.is_empty() {
result.to_mut().push_str(last_word.penalty);
}
lines.push(result);
idx += len + last_word.whitespace.len();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{WordSeparator, WordSplitter, WrapAlgorithm};
#[cfg(feature = "hyphenation")]
use hyphenation::{Language, Load, Standard};
#[test]
fn no_wrap() {
assert_eq!(wrap("foo", 10), vec!["foo"]);
}
#[test]
fn wrap_simple() {
assert_eq!(wrap("foo bar baz", 5), vec!["foo", "bar", "baz"]);
}
#[test]
fn to_be_or_not() {
assert_eq!(
wrap(
"To be, or not to be, that is the question.",
Options::new(10).wrap_algorithm(WrapAlgorithm::FirstFit)
),
vec!["To be, or", "not to be,", "that is", "the", "question."]
);
}
#[test]
fn multiple_words_on_first_line() {
assert_eq!(wrap("foo bar baz", 10), vec!["foo bar", "baz"]);
}
#[test]
fn long_word() {
assert_eq!(wrap("foo", 0), vec!["f", "o", "o"]);
}
#[test]
fn long_words() {
assert_eq!(wrap("foo bar", 0), vec!["f", "o", "o", "b", "a", "r"]);
}
#[test]
fn max_width() {
assert_eq!(wrap("foo bar", usize::MAX), vec!["foo bar"]);
let text = "Hello there! This is some English text. \
It should not be wrapped given the extents below.";
assert_eq!(wrap(text, usize::MAX), vec![text]);
}
#[test]
fn leading_whitespace() {
assert_eq!(wrap(" foo bar", 6), vec![" foo", "bar"]);
}
#[test]
fn leading_whitespace_empty_first_line() {
assert_eq!(wrap(" foobar baz", 6), vec!["", "foobar", "baz"]);
}
#[test]
fn trailing_whitespace() {
assert_eq!(wrap("foo bar baz ", 5), vec!["foo", "bar", "baz"]);
}
#[test]
fn issue_99() {
assert_eq!(
wrap("aaabbbccc x yyyzzzwww", 9),
vec!["aaabbbccc", "x", "yyyzzzwww"]
);
}
#[test]
fn issue_129() {
let options = Options::new(1).word_separator(WordSeparator::AsciiSpace);
assert_eq!(wrap("x – x", options), vec!["x", "–", "x"]);
}
#[test]
fn wide_character_handling() {
assert_eq!(wrap("Hello, World!", 15), vec!["Hello, World!"]);
assert_eq!(
wrap(
"Hello, World!",
Options::new(15).word_separator(WordSeparator::AsciiSpace)
),
vec!["Hello,", "World!"]
);
#[cfg(feature = "unicode-linebreak")]
assert_eq!(
wrap(
"Hello, World!",
Options::new(15).word_separator(WordSeparator::UnicodeBreakProperties),
),
vec!["Hello, W", "orld!"]
);
}
#[test]
fn indent_empty_line() {
let options = Options::new(10).initial_indent("!!!");
assert_eq!(wrap("", &options), vec!["!!!"]);
}
#[test]
fn indent_single_line() {
let options = Options::new(10).initial_indent(">>>"); assert_eq!(wrap("foo", &options), vec![">>>foo"]);
}
#[test]
fn indent_first_emoji() {
let options = Options::new(10).initial_indent("👉👉");
assert_eq!(
wrap("x x x x x x x x x x x x x", &options),
vec!["👉👉x x x", "x x x x x", "x x x x x"]
);
}
#[test]
fn indent_multiple_lines() {
let options = Options::new(6).initial_indent("* ").subsequent_indent(" ");
assert_eq!(
wrap("foo bar baz", &options),
vec!["* foo", " bar", " baz"]
);
}
#[test]
fn only_initial_indent_multiple_lines() {
let options = Options::new(10).initial_indent(" ");
assert_eq!(wrap("foo\nbar\nbaz", &options), vec![" foo", "bar", "baz"]);
}
#[test]
fn only_subsequent_indent_multiple_lines() {
let options = Options::new(10).subsequent_indent(" ");
assert_eq!(
wrap("foo\nbar\nbaz", &options),
vec!["foo", " bar", " baz"]
);
}
#[test]
fn indent_break_words() {
let options = Options::new(5).initial_indent("* ").subsequent_indent(" ");
assert_eq!(wrap("foobarbaz", &options), vec!["* foo", " bar", " baz"]);
}
#[test]
fn initial_indent_break_words() {
let options = Options::new(5).initial_indent("-->");
assert_eq!(wrap("foobarbaz", &options), vec!["-->", "fooba", "rbaz"]);
}
#[test]
fn hyphens() {
assert_eq!(wrap("foo-bar", 5), vec!["foo-", "bar"]);
}
#[test]
fn trailing_hyphen() {
let options = Options::new(5).break_words(false);
assert_eq!(wrap("foobar-", &options), vec!["foobar-"]);
}
#[test]
fn multiple_hyphens() {
assert_eq!(wrap("foo-bar-baz", 5), vec!["foo-", "bar-", "baz"]);
}
#[test]
fn hyphens_flag() {
let options = Options::new(5).break_words(false);
assert_eq!(
wrap("The --foo-bar flag.", &options),
vec!["The", "--foo-", "bar", "flag."]
);
}
#[test]
fn repeated_hyphens() {
let options = Options::new(4).break_words(false);
assert_eq!(wrap("foo--bar", &options), vec!["foo--bar"]);
}
#[test]
fn hyphens_alphanumeric() {
assert_eq!(wrap("Na2-CH4", 5), vec!["Na2-", "CH4"]);
}
#[test]
fn hyphens_non_alphanumeric() {
let options = Options::new(5).break_words(false);
assert_eq!(wrap("foo(-)bar", &options), vec!["foo(-)bar"]);
}
#[test]
fn multiple_splits() {
assert_eq!(wrap("foo-bar-baz", 9), vec!["foo-bar-", "baz"]);
}
#[test]
fn forced_split() {
let options = Options::new(5).break_words(false);
assert_eq!(wrap("foobar-baz", &options), vec!["foobar-", "baz"]);
}
#[test]
fn multiple_unbroken_words_issue_193() {
let options = Options::new(3).break_words(false);
assert_eq!(
wrap("small large tiny", &options),
vec!["small", "large", "tiny"]
);
assert_eq!(
wrap("small large tiny", &options),
vec!["small", "large", "tiny"]
);
}
#[test]
fn very_narrow_lines_issue_193() {
let options = Options::new(1).break_words(false);
assert_eq!(wrap("fooo x y", &options), vec!["fooo", "x", "y"]);
assert_eq!(wrap("fooo x y", &options), vec!["fooo", "x", "y"]);
}
#[test]
fn simple_hyphens() {
let options = Options::new(8).word_splitter(WordSplitter::HyphenSplitter);
assert_eq!(wrap("foo bar-baz", &options), vec!["foo bar-", "baz"]);
}
#[test]
fn no_hyphenation() {
let options = Options::new(8).word_splitter(WordSplitter::NoHyphenation);
assert_eq!(wrap("foo bar-baz", &options), vec!["foo", "bar-baz"]);
}
#[test]
#[cfg(feature = "hyphenation")]
fn auto_hyphenation_double_hyphenation() {
let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap();
let options = Options::new(10);
assert_eq!(
wrap("Internationalization", &options),
vec!["Internatio", "nalization"]
);
let options = Options::new(10).word_splitter(WordSplitter::Hyphenation(dictionary));
assert_eq!(
wrap("Internationalization", &options),
vec!["Interna-", "tionaliza-", "tion"]
);
}
#[test]
#[cfg(feature = "hyphenation")]
fn auto_hyphenation_issue_158() {
let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap();
let options = Options::new(10);
assert_eq!(
wrap("participation is the key to success", &options),
vec!["participat", "ion is", "the key to", "success"]
);
let options = Options::new(10).word_splitter(WordSplitter::Hyphenation(dictionary));
assert_eq!(
wrap("participation is the key to success", &options),
vec!["partici-", "pation is", "the key to", "success"]
);
}
#[test]
#[cfg(feature = "hyphenation")]
fn split_len_hyphenation() {
let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap();
let options = Options::new(15).word_splitter(WordSplitter::Hyphenation(dictionary));
assert_eq!(
wrap("garbage collection", &options),
vec!["garbage col-", "lection"]
);
}
#[test]
#[cfg(feature = "hyphenation")]
fn borrowed_lines() {
use std::borrow::Cow::{Borrowed, Owned};
let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap();
let options = Options::new(10).word_splitter(WordSplitter::Hyphenation(dictionary));
let lines = wrap("Internationalization", &options);
assert_eq!(lines, vec!["Interna-", "tionaliza-", "tion"]);
if let Borrowed(s) = lines[0] {
assert!(false, "should not have been borrowed: {:?}", s);
}
if let Borrowed(s) = lines[1] {
assert!(false, "should not have been borrowed: {:?}", s);
}
if let Owned(ref s) = lines[2] {
assert!(false, "should not have been owned: {:?}", s);
}
}
#[test]
#[cfg(feature = "hyphenation")]
fn auto_hyphenation_with_hyphen() {
let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap();
let options = Options::new(8).break_words(false);
assert_eq!(
wrap("over-caffinated", &options),
vec!["over-", "caffinated"]
);
let options = options.word_splitter(WordSplitter::Hyphenation(dictionary));
assert_eq!(
wrap("over-caffinated", &options),
vec!["over-", "caffi-", "nated"]
);
}
#[test]
fn break_words() {
assert_eq!(wrap("foobarbaz", 3), vec!["foo", "bar", "baz"]);
}
#[test]
fn break_words_wide_characters() {
let options = Options::new(5).word_separator(WordSeparator::AsciiSpace);
assert_eq!(wrap("Hello", options), vec!["He", "ll", "o"]);
}
#[test]
fn break_words_zero_width() {
assert_eq!(wrap("foobar", 0), vec!["f", "o", "o", "b", "a", "r"]);
}
#[test]
fn break_long_first_word() {
assert_eq!(wrap("testx y", 4), vec!["test", "x y"]);
}
#[test]
fn wrap_preserves_line_breaks_trims_whitespace() {
assert_eq!(wrap(" ", 80), vec![""]);
assert_eq!(wrap(" \n ", 80), vec!["", ""]);
assert_eq!(wrap(" \n \n \n ", 80), vec!["", "", "", ""]);
}
#[test]
fn wrap_colored_text() {
let green_hello = "\u{1b}[0m\u{1b}[32mHello\u{1b}[0m";
let blue_world = "\u{1b}[0m\u{1b}[34mWorld!\u{1b}[0m";
assert_eq!(
wrap(&format!("{} {}", green_hello, blue_world), 6),
vec![green_hello, blue_world],
);
}
}