//! Functions for filling text. use crate::{wrap, wrap_algorithms, Options, WordSeparator}; /// Fill a line of text at a given width. /// /// The result is a [`String`], complete with newlines between each /// line. Use [`wrap()`] if you need access to the individual lines. /// /// The easiest way to use this function is to pass an integer for /// `width_or_options`: /// /// ``` /// use textwrap::fill; /// /// assert_eq!( /// fill("Memory safety without garbage collection.", 15), /// "Memory safety\nwithout garbage\ncollection." /// ); /// ``` /// /// If you need to customize the wrapping, you can pass an [`Options`] /// instead of an `usize`: /// /// ``` /// use textwrap::{fill, Options}; /// /// let options = Options::new(15) /// .initial_indent("- ") /// .subsequent_indent(" "); /// assert_eq!( /// fill("Memory safety without garbage collection.", &options), /// "- Memory safety\n without\n garbage\n collection." /// ); /// ``` pub fn fill<'a, Opt>(text: &str, width_or_options: Opt) -> String where Opt: Into>, { let options = width_or_options.into(); if text.len() < options.width && !text.contains('\n') && options.initial_indent.is_empty() { String::from(text.trim_end_matches(' ')) } else { fill_slow_path(text, options) } } /// Slow path for fill. /// /// This is taken when `text` is longer than `options.width`. pub(crate) fn fill_slow_path(text: &str, options: Options<'_>) -> String { // This will avoid reallocation in simple cases (no // indentation, no hyphenation). let mut result = String::with_capacity(text.len()); let line_ending_str = options.line_ending.as_str(); for (i, line) in wrap(text, options).iter().enumerate() { if i > 0 { result.push_str(line_ending_str); } result.push_str(line); } result } /// Fill `text` in-place without reallocating the input string. /// /// This function works by modifying the input string: some `' '` /// characters will be replaced by `'\n'` characters. The rest of the /// text remains untouched. /// /// Since we can only replace existing whitespace in the input with /// `'\n'` (there is no space for `"\r\n"`), we cannot do hyphenation /// nor can we split words longer than the line width. We also need to /// use `AsciiSpace` as the word separator since we need `' '` /// characters between words in order to replace some of them with a /// `'\n'`. Indentation is also ruled out. In other words, /// `fill_inplace(width)` behaves as if you had called [`fill()`] with /// these options: /// /// ``` /// # use textwrap::{core, LineEnding, Options, WordSplitter, WordSeparator, WrapAlgorithm}; /// # let width = 80; /// Options::new(width) /// .break_words(false) /// .line_ending(LineEnding::LF) /// .word_separator(WordSeparator::AsciiSpace) /// .wrap_algorithm(WrapAlgorithm::FirstFit) /// .word_splitter(WordSplitter::NoHyphenation); /// ``` /// /// The wrap algorithm is /// [`WrapAlgorithm::FirstFit`](crate::WrapAlgorithm::FirstFit) since /// this is the fastest algorithm — and the main reason to use /// `fill_inplace` is to get the string broken into newlines as fast /// as possible. /// /// A last difference is that (unlike [`fill()`]) `fill_inplace` can /// leave trailing whitespace on lines. This is because we wrap by /// inserting a `'\n'` at the final whitespace in the input string: /// /// ``` /// let mut text = String::from("Hello World!"); /// textwrap::fill_inplace(&mut text, 10); /// assert_eq!(text, "Hello \nWorld!"); /// ``` /// /// If we didn't do this, the word `World!` would end up being /// indented. You can avoid this if you make sure that your input text /// has no double spaces. /// /// # Performance /// /// In benchmarks, `fill_inplace` is about twice as fast as /// [`fill()`]. Please see the [`linear` /// benchmark](https://github.com/mgeisler/textwrap/blob/master/benchmarks/linear.rs) /// for details. pub fn fill_inplace(text: &mut String, width: usize) { let mut indices = Vec::new(); let mut offset = 0; for line in text.split('\n') { let words = WordSeparator::AsciiSpace .find_words(line) .collect::>(); let wrapped_words = wrap_algorithms::wrap_first_fit(&words, &[width as f64]); let mut line_offset = offset; for words in &wrapped_words[..wrapped_words.len() - 1] { let line_len = words .iter() .map(|word| word.len() + word.whitespace.len()) .sum::(); line_offset += line_len; // We've advanced past all ' ' characters -- want to move // one ' ' backwards and insert our '\n' there. indices.push(line_offset - 1); } // Advance past entire line, plus the '\n' which was removed // by the split call above. offset += line.len() + 1; } let mut bytes = std::mem::take(text).into_bytes(); for idx in indices { bytes[idx] = b'\n'; } *text = String::from_utf8(bytes).unwrap(); } #[cfg(test)] mod tests { use super::*; use crate::WrapAlgorithm; #[test] fn fill_simple() { assert_eq!(fill("foo bar baz", 10), "foo bar\nbaz"); } #[test] fn fill_unicode_boundary() { // https://github.com/mgeisler/textwrap/issues/390 fill("\u{1b}!Ͽ", 10); } #[test] fn non_breaking_space() { let options = Options::new(5).break_words(false); assert_eq!(fill("foo bar baz", &options), "foo bar baz"); } #[test] fn non_breaking_hyphen() { let options = Options::new(5).break_words(false); assert_eq!(fill("foo‑bar‑baz", &options), "foo‑bar‑baz"); } #[test] fn fill_preserves_line_breaks_trims_whitespace() { assert_eq!(fill(" ", 80), ""); assert_eq!(fill(" \n ", 80), "\n"); assert_eq!(fill(" \n \n \n ", 80), "\n\n\n"); } #[test] fn preserve_line_breaks() { assert_eq!(fill("", 80), ""); assert_eq!(fill("\n", 80), "\n"); assert_eq!(fill("\n\n\n", 80), "\n\n\n"); assert_eq!(fill("test\n", 80), "test\n"); assert_eq!(fill("test\n\na\n\n", 80), "test\n\na\n\n"); assert_eq!( fill( "1 3 5 7\n1 3 5 7", Options::new(7).wrap_algorithm(WrapAlgorithm::FirstFit) ), "1 3 5 7\n1 3 5 7" ); assert_eq!( fill( "1 3 5 7\n1 3 5 7", Options::new(5).wrap_algorithm(WrapAlgorithm::FirstFit) ), "1 3 5\n7\n1 3 5\n7" ); } #[test] fn break_words_line_breaks() { assert_eq!(fill("ab\ncdefghijkl", 5), "ab\ncdefg\nhijkl"); assert_eq!(fill("abcdefgh\nijkl", 5), "abcde\nfgh\nijkl"); } #[test] fn break_words_empty_lines() { assert_eq!( fill("foo\nbar", &Options::new(2).break_words(false)), "foo\nbar" ); } #[test] fn fill_inplace_empty() { let mut text = String::from(""); fill_inplace(&mut text, 80); assert_eq!(text, ""); } #[test] fn fill_inplace_simple() { let mut text = String::from("foo bar baz"); fill_inplace(&mut text, 10); assert_eq!(text, "foo bar\nbaz"); } #[test] fn fill_inplace_multiple_lines() { let mut text = String::from("Some text to wrap over multiple lines"); fill_inplace(&mut text, 12); assert_eq!(text, "Some text to\nwrap over\nmultiple\nlines"); } #[test] fn fill_inplace_long_word() { let mut text = String::from("Internationalization is hard"); fill_inplace(&mut text, 10); assert_eq!(text, "Internationalization\nis hard"); } #[test] fn fill_inplace_no_hyphen_splitting() { let mut text = String::from("A well-chosen example"); fill_inplace(&mut text, 10); assert_eq!(text, "A\nwell-chosen\nexample"); } #[test] fn fill_inplace_newlines() { let mut text = String::from("foo bar\n\nbaz\n\n\n"); fill_inplace(&mut text, 10); assert_eq!(text, "foo bar\n\nbaz\n\n\n"); } #[test] fn fill_inplace_newlines_reset_line_width() { let mut text = String::from("1 3 5\n1 3 5 7 9\n1 3 5 7 9 1 3"); fill_inplace(&mut text, 10); assert_eq!(text, "1 3 5\n1 3 5 7 9\n1 3 5 7 9\n1 3"); } #[test] fn fill_inplace_leading_whitespace() { let mut text = String::from(" foo bar baz"); fill_inplace(&mut text, 10); assert_eq!(text, " foo bar\nbaz"); } #[test] fn fill_inplace_trailing_whitespace() { let mut text = String::from("foo bar baz "); fill_inplace(&mut text, 10); assert_eq!(text, "foo bar\nbaz "); } #[test] fn fill_inplace_interior_whitespace() { // To avoid an unwanted indentation of "baz", it is important // to replace the final ' ' with '\n'. let mut text = String::from("foo bar baz"); fill_inplace(&mut text, 10); assert_eq!(text, "foo bar \nbaz"); } }