//! The textwrap library provides functions for word wrapping and //! indenting text. //! //! # Wrapping Text //! //! Wrapping text can be very useful in command-line programs where //! you want to format dynamic output nicely so it looks good in a //! terminal. A quick example: //! //! ```no_run //! fn main() { //! let text = "textwrap: a small library for wrapping text."; //! println!("{}", textwrap::fill(text, 18)); //! } //! ``` //! //! When you run this program, it will display the following output: //! //! ```text //! textwrap: a small //! library for //! wrapping text. //! ``` //! //! If you enable the `hyphenation` Cargo feature, you can get //! automatic hyphenation for a number of languages: //! //! ```no_run //! # #[cfg(feature = "hyphenation")] //! use hyphenation::{Language, Load, Standard}; //! use textwrap::{fill, Options}; //! //! # #[cfg(feature = "hyphenation")] //! fn main() { //! let text = "textwrap: a small library for wrapping text."; //! let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap(); //! let options = Options::new(18).word_splitter(dictionary); //! println!("{}", fill(text, &options)); //! } //! //! # #[cfg(not(feature = "hyphenation"))] //! # fn main() { } //! ``` //! //! The program will now output: //! //! ```text //! textwrap: a small //! library for wrap- //! ping text. //! ``` //! //! See also the [`unfill`] and [`refill`] functions which allow you to //! manipulate already wrapped text. //! //! ## Wrapping Strings at Compile Time //! //! If your strings are known at compile time, please take a look at //! the procedural macros from the [textwrap-macros] crate. //! //! ## Displayed Width vs Byte Size //! //! To word wrap text, one must know the width of each word so one can //! know when to break lines. This library will by default measure the //! width of text using the _displayed width_, not the size in bytes. //! The `unicode-width` Cargo feature controls this. //! //! This is important for non-ASCII text. ASCII characters such as `a` //! and `!` are simple and take up one column each. This means that //! the displayed width is equal to the string length in bytes. //! However, non-ASCII characters and symbols take up more than one //! byte when UTF-8 encoded: `é` is `0xc3 0xa9` (two bytes) and `⚙` is //! `0xe2 0x9a 0x99` (three bytes) in UTF-8, respectively. //! //! This is why we take care to use the displayed width instead of the //! byte count when computing line lengths. All functions in this //! library handle Unicode characters like this when the //! `unicode-width` Cargo feature is enabled (it is enabled by //! default). //! //! # Indentation and Dedentation //! //! The textwrap library also offers functions for adding a prefix to //! every line of a string and to remove leading whitespace. As an //! example, the [`indent`] function allows you to turn lines of text //! into a bullet list: //! //! ``` //! let before = "\ //! foo //! bar //! baz //! "; //! let after = "\ //! * foo //! * bar //! * baz //! "; //! assert_eq!(textwrap::indent(before, "* "), after); //! ``` //! //! Removing leading whitespace is done with [`dedent`]: //! //! ``` //! let before = " //! Some //! indented //! text //! "; //! let after = " //! Some //! indented //! text //! "; //! assert_eq!(textwrap::dedent(before), after); //! ``` //! //! # Cargo Features //! //! The textwrap library can be slimmed down as needed via a number of //! Cargo features. This means you only pay for the features you //! actually use. //! //! The full dependency graph, where dashed lines indicate optional //! dependencies, is shown below: //! //! //! //! ## Default Features //! //! These features are enabled by default: //! //! * `unicode-linebreak`: enables finding words using the //! [unicode-linebreak] crate, which implements the line breaking //! algorithm described in [Unicode Standard Annex //! #14](https://www.unicode.org/reports/tr14/). //! //! This feature can be disabled if you are happy to find words //! separated by ASCII space characters only. People wrapping text //! with emojis or East-Asian characters will want most likely want //! to enable this feature. See the //! [`word_separators::WordSeparator`] trait for details. //! //! * `unicode-width`: enables correct width computation of non-ASCII //! characters via the [unicode-width] crate. Without this feature, //! every [`char`] is 1 column wide, except for emojis which are 2 //! columns wide. See the [`core::display_width`] function for //! details. //! //! This feature can be disabled if you only need to wrap ASCII //! text, or if the functions in [`core`] are used directly with //! [`core::Fragment`]s for which the widths have been computed in //! other ways. //! //! * `smawk`: enables linear-time wrapping of the whole paragraph via //! the [smawk] crate. See the [`wrap_algorithms::wrap_optimal_fit`] //! function for details on the optimal-fit algorithm. //! //! This feature can be disabled if you only ever intend to use //! [`wrap_algorithms::wrap_first_fit`]. //! //! ## Optional Features //! //! These Cargo features enable new functionality: //! //! * `terminal_size`: enables automatic detection of the terminal //! width via the [terminal_size] crate. See the //! [`Options::with_termwidth`] constructor for details. //! //! * `hyphenation`: enables language-sensitive hyphenation via the //! [hyphenation] crate. See the [`word_splitters::WordSplitter`] trait for details. //! //! [unicode-linebreak]: https://docs.rs/unicode-linebreak/ //! [unicode-width]: https://docs.rs/unicode-width/ //! [smawk]: https://docs.rs/smawk/ //! [textwrap-macros]: https://docs.rs/textwrap-macros/ //! [terminal_size]: https://docs.rs/terminal_size/ //! [hyphenation]: https://docs.rs/hyphenation/ #![doc(html_root_url = "https://docs.rs/textwrap/0.14.0")] #![forbid(unsafe_code)] // See https://github.com/mgeisler/textwrap/issues/210 #![deny(missing_docs)] #![deny(missing_debug_implementations)] #![allow(clippy::redundant_field_names)] use std::borrow::Cow; mod indentation; pub use crate::indentation::dedent; pub use crate::indentation::indent; pub mod word_separators; pub mod word_splitters; pub mod wrap_algorithms; pub mod core; // These private macros lets us hide the actual WrapAlgorithm and // WordSeperator used in the function signatures below. #[cfg(feature = "smawk")] macro_rules! DefaultWrapAlgorithm { () => { wrap_algorithms::OptimalFit }; } #[cfg(not(feature = "smawk"))] macro_rules! DefaultWrapAlgorithm { () => { wrap_algorithms::FirstFit }; } #[cfg(feature = "unicode-linebreak")] macro_rules! DefaultWordSeparator { () => { word_separators::UnicodeBreakProperties }; } #[cfg(not(feature = "unicode-linebreak"))] macro_rules! DefaultWordSeparator { () => { word_separators::AsciiSpace }; } /// Holds settings for wrapping and filling text. #[derive(Debug, Clone)] pub struct Options< 'a, WrapAlgo = Box, WordSep = Box, WordSplit = Box, > { /// The width in columns at which the text will be wrapped. pub width: usize, /// Indentation used for the first line of output. See the /// [`Options::initial_indent`] method. pub initial_indent: &'a str, /// Indentation used for subsequent lines of output. See the /// [`Options::subsequent_indent`] method. pub subsequent_indent: &'a str, /// Allow long words to be broken if they cannot fit on a line. /// When set to `false`, some lines may be longer than /// `self.width`. See the [`Options::break_words`] method. pub break_words: bool, /// Wrapping algorithm to use, see the implementations of the /// [`wrap_algorithms::WrapAlgorithm`] trait for details. pub wrap_algorithm: WrapAlgo, /// The line breaking algorithm to use, see /// [`word_separators::WordSeparator`] trait for an overview and /// possible implementations. pub word_separator: WordSep, /// The method for splitting words. This can be used to prohibit /// splitting words on hyphens, or it can be used to implement /// language-aware machine hyphenation. Please see the /// [`word_splitters::WordSplitter`] trait for details. pub word_splitter: WordSplit, } impl<'a, WrapAlgo, WordSep, WordSplit> From<&'a Options<'a, WrapAlgo, WordSep, WordSplit>> for Options<'a, WrapAlgo, WordSep, WordSplit> where WrapAlgo: Clone, WordSep: Clone, WordSplit: Clone, { fn from(options: &'a Options<'a, WrapAlgo, WordSep, WordSplit>) -> Self { Self { width: options.width, initial_indent: options.initial_indent, subsequent_indent: options.subsequent_indent, break_words: options.break_words, word_separator: options.word_separator.clone(), wrap_algorithm: options.wrap_algorithm.clone(), word_splitter: options.word_splitter.clone(), } } } impl<'a> From for Options< 'a, DefaultWrapAlgorithm!(), DefaultWordSeparator!(), word_splitters::HyphenSplitter, > { fn from(width: usize) -> Self { Options::new(width) } } /// Constructors for boxed Options, specifically. impl<'a> Options<'a, DefaultWrapAlgorithm!(), DefaultWordSeparator!(), word_splitters::HyphenSplitter> { /// Creates a new [`Options`] with the specified width and static /// dispatch using the [`word_splitters::HyphenSplitter`]. /// Equivalent to /// /// ``` /// # use textwrap::word_splitters::{HyphenSplitter, WordSplitter}; /// # use textwrap::Options; /// # let width = 80; /// # let actual = Options::new(width); /// # let expected = /// Options { /// width: width, /// initial_indent: "", /// subsequent_indent: "", /// break_words: true, /// #[cfg(feature = "unicode-linebreak")] /// word_separator: textwrap::word_separators::UnicodeBreakProperties, /// #[cfg(not(feature = "unicode-linebreak"))] /// word_separator: textwrap::word_separators::AsciiSpace, /// #[cfg(feature = "smawk")] /// wrap_algorithm: textwrap::wrap_algorithms::OptimalFit, /// #[cfg(not(feature = "smawk"))] /// wrap_algorithm: textwrap::wrap_algorithms::FirstFit, /// word_splitter: textwrap::word_splitters::HyphenSplitter, /// } /// # ; /// # assert_eq!(actual.width, expected.width); /// # assert_eq!(actual.initial_indent, expected.initial_indent); /// # assert_eq!(actual.subsequent_indent, expected.subsequent_indent); /// # assert_eq!(actual.break_words, expected.break_words); /// ``` /// /// Note that the default word separator and wrap algorithms /// changes based on the available Cargo features. The best /// available algorithm is used by default. /// /// Static dispatch means here, that the word splitter is stored as-is /// and the type is known at compile-time. Thus the returned value /// is actually a `Options`. /// /// Dynamic dispatch on the other hand, means that the word /// separator and/or word splitter is stored as a trait object /// such as a `Box`. This way /// the word splitter's inner type can be changed without changing /// the type of this struct, which then would be just `Options` as /// a short cut for `Options, Box>`. /// /// The value and type of the word splitter can be choose from the /// start using the [`Options::with_word_splitter`] constructor or /// changed afterwards using the [`Options::word_splitter`] /// method. Whether static or dynamic dispatch is used, depends on /// whether these functions are given a boxed /// [`word_splitters::WordSplitter`] or not. Take for example: /// /// ``` /// use textwrap::Options; /// use textwrap::word_splitters::{HyphenSplitter, NoHyphenation}; /// # use textwrap::word_splitters::WordSplitter; /// # use textwrap::word_separators::AsciiSpace; /// # let width = 80; /// /// // uses HyphenSplitter with static dispatch /// // the actual type: Options /// let opt = Options::new(width); /// /// // uses NoHyphenation with static dispatch /// // the actual type: Options /// let opt = Options::new(width).word_splitter(NoHyphenation); /// /// // uses HyphenSplitter with dynamic dispatch /// // the actual type: Options> /// let opt: Options<_, _, _> = Options::new(width).word_splitter(Box::new(HyphenSplitter)); /// /// // uses NoHyphenation with dynamic dispatch /// // the actual type: Options> /// let opt: Options<_, _, _> = Options::new(width).word_splitter(Box::new(NoHyphenation)); /// ``` /// /// Notice that the last two variables have the same type, despite /// the different `WordSplitter` in use. Thus dynamic dispatch /// allows to change the word splitter at run-time without /// changing the variables type. pub const fn new(width: usize) -> Self { Options::with_word_splitter(width, word_splitters::HyphenSplitter) } /// Creates a new [`Options`] with `width` set to the current /// terminal width. If the terminal width cannot be determined /// (typically because the standard input and output is not /// connected to a terminal), a width of 80 characters will be /// used. Other settings use the same defaults as /// [`Options::new`]. /// /// Equivalent to: /// /// ```no_run /// use textwrap::{termwidth, Options}; /// /// let options = Options::new(termwidth()); /// ``` /// /// **Note:** Only available when the `terminal_size` feature is /// enabled. #[cfg(feature = "terminal_size")] pub fn with_termwidth() -> Self { Self::new(termwidth()) } } impl<'a, WordSplit> Options<'a, DefaultWrapAlgorithm!(), DefaultWordSeparator!(), WordSplit> { /// Creates a new [`Options`] with the specified width and /// word splitter. Equivalent to /// /// ``` /// # use textwrap::Options; /// # use textwrap::word_splitters::{NoHyphenation, HyphenSplitter}; /// # const word_splitter: NoHyphenation = NoHyphenation; /// # const width: usize = 80; /// # let actual = Options::with_word_splitter(width, word_splitter); /// # let expected = /// Options { /// width: width, /// initial_indent: "", /// subsequent_indent: "", /// break_words: true, /// #[cfg(feature = "unicode-linebreak")] /// word_separator: textwrap::word_separators::UnicodeBreakProperties, /// #[cfg(not(feature = "unicode-linebreak"))] /// word_separator: textwrap::word_separators::AsciiSpace, /// #[cfg(feature = "smawk")] /// wrap_algorithm: textwrap::wrap_algorithms::OptimalFit, /// #[cfg(not(feature = "smawk"))] /// wrap_algorithm: textwrap::wrap_algorithms::FirstFit, /// word_splitter: word_splitter, /// } /// # ; /// # assert_eq!(actual.width, expected.width); /// # assert_eq!(actual.initial_indent, expected.initial_indent); /// # assert_eq!(actual.subsequent_indent, expected.subsequent_indent); /// # assert_eq!(actual.break_words, expected.break_words); /// ``` /// /// This constructor allows to specify the word splitter to be /// used. It is like a short-cut for /// `Options::new(w).word_splitter(s)`, but this function is a /// `const fn`. The given word splitter may be in a [`Box`], which /// then can be coerced into a trait object for dynamic dispatch: /// /// ``` /// use textwrap::Options; /// use textwrap::word_splitters::{HyphenSplitter, NoHyphenation, WordSplitter}; /// # const width: usize = 80; /// /// // This opt contains a boxed trait object as splitter. /// // The type annotation is important, otherwise it will be not a trait object /// let mut opt: Options<_, _, Box> /// = Options::with_word_splitter(width, Box::new(NoHyphenation)); /// // Its type is actually: `Options>`: /// let opt_coerced: Options<_, _, Box> = opt; /// /// // Thus, it can be overridden with a different word splitter. /// opt = Options::with_word_splitter(width, Box::new(HyphenSplitter)); /// // Now, containing a `HyphenSplitter` instead. /// ``` /// /// Since the word splitter is given by value, which determines /// the generic type parameter, it can be used to produce both an /// [`Options`] with static and dynamic dispatch, respectively. /// While dynamic dispatch allows to change the type of the inner /// word splitter at run time as seen above, static dispatch /// especially can store the word splitter directly, without the /// need for a box. This in turn allows it to be used in constant /// and static context: /// /// ``` /// use textwrap::word_splitters::HyphenSplitter; use textwrap::{ Options}; /// use textwrap::word_separators::AsciiSpace; /// use textwrap::wrap_algorithms::FirstFit; /// # const width: usize = 80; /// /// # #[cfg(all(not(feature = "smawk"), not(feature = "unicode-linebreak")))] { /// const FOO: Options = /// Options::with_word_splitter(width, HyphenSplitter); /// static BAR: Options = FOO; /// # } /// ``` pub const fn with_word_splitter(width: usize, word_splitter: WordSplit) -> Self { Options { width, initial_indent: "", subsequent_indent: "", break_words: true, word_separator: DefaultWordSeparator!(), wrap_algorithm: DefaultWrapAlgorithm!(), word_splitter: word_splitter, } } } impl<'a, WrapAlgo, WordSep, WordSplit> Options<'a, WrapAlgo, WordSep, WordSplit> { /// Change [`self.initial_indent`]. The initial indentation is /// used on the very first line of output. /// /// # Examples /// /// Classic paragraph indentation can be achieved by specifying an /// initial indentation and wrapping each paragraph by itself: /// /// ``` /// use textwrap::{Options, wrap}; /// /// let options = Options::new(16).initial_indent(" "); /// assert_eq!(wrap("This is a little example.", options), /// vec![" This is a", /// "little example."]); /// ``` /// /// [`self.initial_indent`]: #structfield.initial_indent pub fn initial_indent(self, indent: &'a str) -> Self { Options { initial_indent: indent, ..self } } /// Change [`self.subsequent_indent`]. The subsequent indentation /// is used on lines following the first line of output. /// /// # Examples /// /// Combining initial and subsequent indentation lets you format a /// single paragraph as a bullet list: /// /// ``` /// use textwrap::{Options, wrap}; /// /// let options = Options::new(12) /// .initial_indent("* ") /// .subsequent_indent(" "); /// #[cfg(feature = "smawk")] /// assert_eq!(wrap("This is a little example.", options), /// vec!["* This is", /// " a little", /// " example."]); /// /// // Without the `smawk` feature, the wrapping is a little different: /// #[cfg(not(feature = "smawk"))] /// assert_eq!(wrap("This is a little example.", options), /// vec!["* This is a", /// " little", /// " example."]); /// ``` /// /// [`self.subsequent_indent`]: #structfield.subsequent_indent pub fn subsequent_indent(self, indent: &'a str) -> Self { Options { subsequent_indent: indent, ..self } } /// Change [`self.break_words`]. This controls if words longer /// than `self.width` can be broken, or if they will be left /// sticking out into the right margin. /// /// # Examples /// /// ``` /// use textwrap::{wrap, Options}; /// /// let options = Options::new(4).break_words(true); /// assert_eq!(wrap("This is a little example.", options), /// vec!["This", /// "is a", /// "litt", /// "le", /// "exam", /// "ple."]); /// ``` /// /// [`self.break_words`]: #structfield.break_words pub fn break_words(self, setting: bool) -> Self { Options { break_words: setting, ..self } } /// Change [`self.word_separator`]. /// /// See [`word_separators::WordSeparator`] for details on the choices. /// /// [`self.word_separator`]: #structfield.word_separator pub fn word_separator( self, word_separator: NewWordSep, ) -> Options<'a, WrapAlgo, NewWordSep, WordSplit> { Options { width: self.width, initial_indent: self.initial_indent, subsequent_indent: self.subsequent_indent, break_words: self.break_words, word_separator: word_separator, wrap_algorithm: self.wrap_algorithm, word_splitter: self.word_splitter, } } /// Change [`self.wrap_algorithm`]. /// /// See the [`wrap_algorithms::WrapAlgorithm`] trait for details on /// the choices. /// /// [`self.wrap_algorithm`]: #structfield.wrap_algorithm pub fn wrap_algorithm( self, wrap_algorithm: NewWrapAlgo, ) -> Options<'a, NewWrapAlgo, WordSep, WordSplit> { Options { width: self.width, initial_indent: self.initial_indent, subsequent_indent: self.subsequent_indent, break_words: self.break_words, word_separator: self.word_separator, wrap_algorithm: wrap_algorithm, word_splitter: self.word_splitter, } } /// Change [`self.word_splitter`]. The /// [`word_splitters::WordSplitter`] is used to fit part of a word /// into the current line when wrapping text. /// /// This function may return a different type than `Self`. That is /// the case when the given `splitter` is of a different type the /// the currently stored one in the `splitter` field. Take for /// example: /// /// ``` /// use textwrap::word_splitters::{HyphenSplitter, NoHyphenation}; /// use textwrap::Options; /// // The default type returned by `new`: /// let opt: Options<_, _, HyphenSplitter> = Options::new(80); /// // Setting a different word splitter changes the type /// let opt: Options<_, _, NoHyphenation> = opt.word_splitter(NoHyphenation); /// ``` /// /// [`self.word_splitter`]: #structfield.word_splitter pub fn word_splitter( self, word_splitter: NewWordSplit, ) -> Options<'a, WrapAlgo, WordSep, NewWordSplit> { Options { width: self.width, initial_indent: self.initial_indent, subsequent_indent: self.subsequent_indent, break_words: self.break_words, word_separator: self.word_separator, wrap_algorithm: self.wrap_algorithm, word_splitter, } } } /// Return the current terminal width. /// /// If the terminal width cannot be determined (typically because the /// standard output is not connected to a terminal), a default width /// of 80 characters will be used. /// /// # Examples /// /// Create an [`Options`] for wrapping at the current terminal width /// with a two column margin to the left and the right: /// /// ```no_run /// use textwrap::{termwidth, Options}; /// use textwrap::word_splitters::NoHyphenation; /// /// let width = termwidth() - 4; // Two columns on each side. /// let options = Options::new(width) /// .word_splitter(NoHyphenation) /// .initial_indent(" ") /// .subsequent_indent(" "); /// ``` /// /// **Note:** Only available when the `terminal_size` Cargo feature is /// enabled. #[cfg(feature = "terminal_size")] pub fn termwidth() -> usize { terminal_size::terminal_size().map_or(80, |(terminal_size::Width(w), _)| w.into()) } /// Fill a line of text at a given width. /// /// The result is a [`String`], complete with newlines between each /// line. Use the [`wrap`] function 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, WrapAlgo, WordSep, WordSplit, Opt>(text: &str, width_or_options: Opt) -> String where WrapAlgo: wrap_algorithms::WrapAlgorithm, WordSep: word_separators::WordSeparator, WordSplit: word_splitters::WordSplitter, Opt: Into>, { // This will avoid reallocation in simple cases (no // indentation, no hyphenation). let mut result = String::with_capacity(text.len()); for (i, line) in wrap(text, width_or_options).iter().enumerate() { if i > 0 { result.push('\n'); } result.push_str(&line); } result } /// Unpack a paragraph of already-wrapped text. /// /// This function attempts to recover the original text from a single /// paragraph of text produced by the [`fill`] function. This means /// that it turns /// /// ```text /// textwrap: a small /// library for /// wrapping text. /// ``` /// /// back into /// /// ```text /// textwrap: a small library for wrapping text. /// ``` /// /// In addition, it will recognize a common prefix among the lines. /// The prefix of the first line is returned in /// [`Options::initial_indent`] and the prefix (if any) of the the /// other lines is returned in [`Options::subsequent_indent`]. /// /// In addition to `' '`, the prefixes can consist of characters used /// for unordered lists (`'-'`, `'+'`, and `'*'`) and block quotes /// (`'>'`) in Markdown as well as characters often used for inline /// comments (`'#'` and `'/'`). /// /// The text must come from a single wrapped paragraph. This means /// that there can be no `"\n\n"` within the text. /// /// # Examples /// /// ``` /// use textwrap::unfill; /// /// let (text, options) = unfill("\ /// * This is an /// example of /// a list item. /// "); /// /// assert_eq!(text, "This is an example of a list item.\n"); /// assert_eq!(options.initial_indent, "* "); /// assert_eq!(options.subsequent_indent, " "); /// ``` pub fn unfill( text: &str, ) -> ( String, Options<'_, DefaultWrapAlgorithm!(), DefaultWordSeparator!(), word_splitters::HyphenSplitter>, ) { let trimmed = text.trim_end_matches('\n'); let prefix_chars: &[_] = &[' ', '-', '+', '*', '>', '#', '/']; let mut options = Options::new(0); for (idx, line) in trimmed.split('\n').enumerate() { options.width = std::cmp::max(options.width, core::display_width(line)); let without_prefix = line.trim_start_matches(prefix_chars); let prefix = &line[..line.len() - without_prefix.len()]; if idx == 0 { options.initial_indent = prefix; } else if idx == 1 { options.subsequent_indent = prefix; } else if idx > 1 { for ((idx, x), y) in prefix.char_indices().zip(options.subsequent_indent.chars()) { if x != y { options.subsequent_indent = &prefix[..idx]; break; } } if prefix.len() < options.subsequent_indent.len() { options.subsequent_indent = prefix; } } } let mut unfilled = String::with_capacity(text.len()); for (idx, line) in trimmed.split('\n').enumerate() { if idx == 0 { unfilled.push_str(&line[options.initial_indent.len()..]); } else { unfilled.push(' '); unfilled.push_str(&line[options.subsequent_indent.len()..]); } } unfilled.push_str(&text[trimmed.len()..]); (unfilled, options) } /// Refill a paragraph of wrapped text with a new width. /// /// This function will first use the [`unfill`] function to remove /// newlines from the text. Afterwards the text is filled again using /// the [`fill`] function. /// /// The `new_width_or_options` argument specify the new width and can /// specify other options as well — except for /// [`Options::initial_indent`] and [`Options::subsequent_indent`], /// which are deduced from `filled_text`. /// /// # Examples /// /// ``` /// use textwrap::refill; /// /// // Some loosely wrapped text. The "> " prefix is recognized automatically. /// let text = "\ /// > Memory /// > safety without garbage /// > collection. /// "; /// /// assert_eq!(refill(text, 20), "\ /// > Memory safety /// > without garbage /// > collection. /// "); /// /// assert_eq!(refill(text, 40), "\ /// > Memory safety without garbage /// > collection. /// "); /// /// assert_eq!(refill(text, 60), "\ /// > Memory safety without garbage collection. /// "); /// ``` /// /// You can also reshape bullet points: /// /// ``` /// use textwrap::refill; /// /// let text = "\ /// - This is my /// list item. /// "; /// /// assert_eq!(refill(text, 20), "\ /// - This is my list /// item. /// "); /// ``` pub fn refill<'a, WrapAlgo, WordSep, WordSplit, Opt>( filled_text: &str, new_width_or_options: Opt, ) -> String where WrapAlgo: wrap_algorithms::WrapAlgorithm, WordSep: word_separators::WordSeparator, WordSplit: word_splitters::WordSplitter, Opt: Into>, { let trimmed = filled_text.trim_end_matches('\n'); let (text, options) = unfill(trimmed); let mut new_options = new_width_or_options.into(); new_options.initial_indent = options.initial_indent; new_options.subsequent_indent = options.subsequent_indent; let mut refilled = fill(&text, new_options); refilled.push_str(&filled_text[trimmed.len()..]); refilled } /// Wrap a line of text at a given width. /// /// The result is a vector of lines, each line is of type [`Cow<'_, /// str>`](Cow), which means that the line will borrow from the input /// `&str` if possible. The lines do not have trailing whitespace, /// including a final `'\n'`. Please use the [`fill`] function if you /// need a [`String`] instead. /// /// The easiest way to use this function is to pass an integer for /// `width_or_options`: /// /// ``` /// use textwrap::wrap; /// /// let lines = wrap("Memory safety without garbage collection.", 15); /// assert_eq!(lines, &[ /// "Memory safety", /// "without garbage", /// "collection.", /// ]); /// ``` /// /// If you need to customize the wrapping, you can pass an [`Options`] /// instead of an `usize`: /// /// ``` /// use textwrap::{wrap, Options}; /// /// let options = Options::new(15) /// .initial_indent("- ") /// .subsequent_indent(" "); /// let lines = wrap("Memory safety without garbage collection.", &options); /// assert_eq!(lines, &[ /// "- Memory safety", /// " without", /// " garbage", /// " collection.", /// ]); /// ``` /// /// # Optimal-Fit Wrapping /// /// By default, `wrap` will try to ensure an even right margin by /// finding breaks which avoid short lines. We call this an /// “optimal-fit algorithm” since the line breaks are computed by /// considering all possible line breaks. The alternative is a /// “first-fit algorithm” which simply accumulates words until they no /// longer fit on the line. /// /// As an example, using the first-fit algorithm to wrap the famous /// Hamlet quote “To be, or not to be: that is the question” in a /// narrow column with room for only 10 characters looks like this: /// /// ``` /// # use textwrap::{wrap_algorithms::FirstFit, Options, wrap}; /// # /// # let lines = wrap("To be, or not to be: that is the question", /// # Options::new(10).wrap_algorithm(FirstFit)); /// # assert_eq!(lines.join("\n") + "\n", "\ /// To be, or /// not to be: /// that is /// the /// question /// # "); /// ``` /// /// Notice how the second to last line is quite narrow because /// “question” was too large to fit? The greedy first-fit algorithm /// doesn’t look ahead, so it has no other option than to put /// “question” onto its own line. /// /// With the optimal-fit wrapping algorithm, the previous lines are /// shortened slightly in order to make the word “is” go into the /// second last line: /// /// ``` /// # #[cfg(feature = "smawk")] { /// # use textwrap::{Options, wrap}; /// # use textwrap::wrap_algorithms::OptimalFit; /// # /// # let lines = wrap("To be, or not to be: that is the question", /// # Options::new(10).wrap_algorithm(OptimalFit)); /// # assert_eq!(lines.join("\n") + "\n", "\ /// To be, /// or not to /// be: that /// is the /// question /// # "); } /// ``` /// /// Please see the [`wrap_algorithms::WrapAlgorithm`] trait for details. /// /// # Examples /// /// The returned iterator yields lines of type `Cow<'_, str>`. If /// possible, the wrapped lines will borrow from the input string. As /// an example, a hanging indentation, the first line can borrow from /// the input, but the subsequent lines become owned strings: /// /// ``` /// use std::borrow::Cow::{Borrowed, Owned}; /// use textwrap::{wrap, Options}; /// /// let options = Options::new(15).subsequent_indent("...."); /// let lines = wrap("Wrapping text all day long.", &options); /// let annotated = lines /// .iter() /// .map(|line| match line { /// Borrowed(text) => format!("[Borrowed] {}", text), /// Owned(text) => format!("[Owned] {}", text), /// }) /// .collect::>(); /// assert_eq!( /// annotated, /// &[ /// "[Borrowed] Wrapping text", /// "[Owned] ....all day", /// "[Owned] ....long.", /// ] /// ); /// ``` /// /// ## Leading and Trailing Whitespace /// /// As a rule, leading whitespace (indentation) is preserved and /// trailing whitespace is discarded. /// /// In more details, when wrapping words into lines, words are found /// by splitting the input text on space characters. One or more /// spaces (shown here as “␣”) are attached to the end of each word: /// /// ```text /// "Foo␣␣␣bar␣baz" -> ["Foo␣␣␣", "bar␣", "baz"] /// ``` /// /// These words are then put into lines. The interword whitespace is /// preserved, unless the lines are wrapped so that the `"Foo␣␣␣"` /// word falls at the end of a line: /// /// ``` /// use textwrap::wrap; /// /// assert_eq!(wrap("Foo bar baz", 10), vec!["Foo bar", "baz"]); /// assert_eq!(wrap("Foo bar baz", 8), vec!["Foo", "bar baz"]); /// ``` /// /// Notice how the trailing whitespace is removed in both case: in the /// first example, `"bar␣"` becomes `"bar"` and in the second case /// `"Foo␣␣␣"` becomes `"Foo"`. /// /// Leading whitespace is preserved when the following word fits on /// the first line. To understand this, consider how words are found /// in a text with leading spaces: /// /// ```text /// "␣␣foo␣bar" -> ["␣␣", "foo␣", "bar"] /// ``` /// /// When put into lines, the indentation is preserved if `"foo"` fits /// on the first line, otherwise you end up with an empty line: /// /// ``` /// use textwrap::wrap; /// /// assert_eq!(wrap(" foo bar", 8), vec![" foo", "bar"]); /// assert_eq!(wrap(" foo bar", 4), vec!["", "foo", "bar"]); /// ``` pub fn wrap<'a, WrapAlgo, WordSep, WordSplit, Opt>( text: &str, width_or_options: Opt, ) -> Vec> where WrapAlgo: wrap_algorithms::WrapAlgorithm, WordSep: word_separators::WordSeparator, WordSplit: word_splitters::WordSplitter, Opt: Into>, { let options = width_or_options.into(); let initial_width = options .width .saturating_sub(core::display_width(options.initial_indent)); let subsequent_width = options .width .saturating_sub(core::display_width(options.subsequent_indent)); let mut lines = Vec::new(); for line in text.split('\n') { let words = options.word_separator.find_words(line); let split_words = word_splitters::split_words(words, &options.word_splitter); let broken_words = if options.break_words { let mut broken_words = core::break_words(split_words, subsequent_width); if !options.initial_indent.is_empty() { // Without this, the first word will always go into // the first line. However, since we break words based // on the _second_ line width, it can be wrong to // unconditionally put the first word onto the first // line. An empty zero-width word fixed this. broken_words.insert(0, core::Word::from("")); } broken_words } else { split_words.collect::>() }; let line_widths = [initial_width, subsequent_width]; 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, }; // We assume here that all words are contiguous in `line`. // That is, the sum of their lengths should add up to the // length of `line`. let len = words .iter() .map(|word| word.len() + word.whitespace.len()) .sum::() - last_word.whitespace.len(); // The result is owned if we have indentation, otherwise // we can simply borrow an empty string. 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 { // We can use an empty string here since string // concatenation for `Cow` preserves a borrowed value // when either side is empty. Cow::from("") }; result += &line[idx..idx + len]; if !last_word.penalty.is_empty() { result.to_mut().push_str(&last_word.penalty); } lines.push(result); // Advance by the length of `result`, plus the length of // `last_word.whitespace` -- even if we had a penalty, we // need to skip over the whitespace. idx += len + last_word.whitespace.len(); } } lines } /// Wrap text into columns with a given total width. /// /// The `left_gap`, `middle_gap` and `right_gap` arguments specify the /// strings to insert before, between, and after the columns. The /// total width of all columns and all gaps is specified using the /// `total_width_or_options` argument. This argument can simply be an /// integer if you want to use default settings when wrapping, or it /// can be a [`Options`] value if you want to customize the wrapping. /// /// If the columns are narrow, it is recommended to set /// [`Options::break_words`] to `true` to prevent words from /// protruding into the margins. /// /// The per-column width is computed like this: /// /// ``` /// # let (left_gap, middle_gap, right_gap) = ("", "", ""); /// # let columns = 2; /// # let options = textwrap::Options::new(80); /// let inner_width = options.width /// - textwrap::core::display_width(left_gap) /// - textwrap::core::display_width(right_gap) /// - textwrap::core::display_width(middle_gap) * (columns - 1); /// let column_width = inner_width / columns; /// ``` /// /// The `text` is wrapped using [`wrap`] and the given `options` /// argument, but the width is overwritten to the computed /// `column_width`. /// /// # Panics /// /// Panics if `columns` is zero. /// /// # Examples /// /// ``` /// use textwrap::wrap_columns; /// /// let text = "\ /// This is an example text, which is wrapped into three columns. \ /// Notice how the final column can be shorter than the others."; /// /// #[cfg(feature = "smawk")] /// assert_eq!(wrap_columns(text, 3, 50, "| ", " | ", " |"), /// vec!["| This is | into three | column can be |", /// "| an example | columns. | shorter than |", /// "| text, which | Notice how | the others. |", /// "| is wrapped | the final | |"]); /// /// // Without the `smawk` feature, the middle column is a little more uneven: /// #[cfg(not(feature = "smawk"))] /// assert_eq!(wrap_columns(text, 3, 50, "| ", " | ", " |"), /// vec!["| This is an | three | column can be |", /// "| example text, | columns. | shorter than |", /// "| which is | Notice how | the others. |", /// "| wrapped into | the final | |"]); pub fn wrap_columns<'a, WrapAlgo, WordSep, WordSplit, Opt>( text: &str, columns: usize, total_width_or_options: Opt, left_gap: &str, middle_gap: &str, right_gap: &str, ) -> Vec where WrapAlgo: wrap_algorithms::WrapAlgorithm, WordSep: word_separators::WordSeparator, WordSplit: word_splitters::WordSplitter, Opt: Into>, { assert!(columns > 0); let mut options = total_width_or_options.into(); let inner_width = options .width .saturating_sub(core::display_width(left_gap)) .saturating_sub(core::display_width(right_gap)) .saturating_sub(core::display_width(middle_gap) * (columns - 1)); let column_width = std::cmp::max(inner_width / columns, 1); options.width = column_width; let last_column_padding = " ".repeat(inner_width % column_width); let wrapped_lines = wrap(text, options); let lines_per_column = wrapped_lines.len() / columns + usize::from(wrapped_lines.len() % columns > 0); let mut lines = Vec::new(); for line_no in 0..lines_per_column { let mut line = String::from(left_gap); for column_no in 0..columns { match wrapped_lines.get(line_no + column_no * lines_per_column) { Some(column_line) => { line.push_str(&column_line); line.push_str(&" ".repeat(column_width - core::display_width(&column_line))); } None => { line.push_str(&" ".repeat(column_width)); } } if column_no == columns - 1 { line.push_str(&last_column_padding); } else { line.push_str(middle_gap); } } line.push_str(right_gap); lines.push(line); } lines } /// 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'`, 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, Options}; /// # use textwrap::{word_separators, word_splitters, wrap_algorithms}; /// # let width = 80; /// Options { /// width: width, /// initial_indent: "", /// subsequent_indent: "", /// break_words: false, /// word_separator: word_separators::AsciiSpace, /// wrap_algorithm: wrap_algorithms::FirstFit, /// word_splitter: word_splitters::NoHyphenation, /// }; /// ``` /// /// The wrap algorithm is [`wrap_algorithms::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/benches/linear.rs) /// for details. pub fn fill_inplace(text: &mut String, width: usize) { use word_separators::WordSeparator; let mut indices = Vec::new(); let mut offset = 0; for line in text.split('\n') { let words = word_separators::AsciiSpace .find_words(line) .collect::>(); let wrapped_words = wrap_algorithms::wrap_first_fit(&words, &[width]); 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::word_splitters::WordSplitter; use crate::{word_splitters, wrap_algorithms}; #[cfg(feature = "hyphenation")] use hyphenation::{Language, Load, Standard}; #[test] fn options_agree_with_usize() { let opt_usize = Options::from(42_usize); let opt_options = Options::new(42); assert_eq!(opt_usize.width, opt_options.width); assert_eq!(opt_usize.initial_indent, opt_options.initial_indent); assert_eq!(opt_usize.subsequent_indent, opt_options.subsequent_indent); assert_eq!(opt_usize.break_words, opt_options.break_words); assert_eq!( opt_usize.word_splitter.split_points("hello-world"), opt_options.word_splitter.split_points("hello-world") ); } #[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(wrap_algorithms::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_value()), vec!["foo bar"]); } #[test] fn leading_whitespace() { assert_eq!(wrap(" foo bar", 6), vec![" foo", "bar"]); } #[test] fn leading_whitespace_empty_first_line() { // If there is no space for the first word, the first line // will be empty. This is because the string is split into // words like [" ", "foobar ", "baz"], which puts "foobar " on // the second line. We never output trailing whitespace assert_eq!(wrap(" foobar baz", 6), vec!["", "foobar", "baz"]); } #[test] fn trailing_whitespace() { // Whitespace is only significant inside a line. After a line // gets too long and is broken, the first word starts in // column zero and is not indented. assert_eq!(wrap("foo bar baz ", 5), vec!["foo", "bar", "baz"]); } #[test] fn issue_99() { // We did not reset the in_whitespace flag correctly and did // not handle single-character words after a line break. assert_eq!( wrap("aaabbbccc x yyyzzzwww", 9), vec!["aaabbbccc", "x", "yyyzzzwww"] ); } #[test] fn issue_129() { // The dash is an em-dash which takes up four bytes. We used // to panic since we tried to index into the character. let options = Options::new(1).word_separator(word_separators::AsciiSpace); assert_eq!(wrap("x – x", options), vec!["x", "–", "x"]); } #[test] #[cfg(feature = "unicode-width")] fn wide_character_handling() { assert_eq!(wrap("Hello, World!", 15), vec!["Hello, World!"]); assert_eq!( wrap( "Hello, World!", Options::new(15).word_separator(word_separators::AsciiSpace) ), vec!["Hello,", "World!"] ); // Wide characters are allowed to break if the // unicode-linebreak feature is enabled. #[cfg(feature = "unicode-linebreak")] assert_eq!( wrap( "Hello, World!", Options::new(15).word_separator(word_separators::UnicodeBreakProperties) ), vec!["Hello, W", "orld!"] ); } #[test] fn empty_line_is_indented() { // Previously, indentation was not applied to empty lines. // However, this is somewhat inconsistent and undesirable if // the indentation is something like a border ("| ") which you // want to apply to all lines, empty or not. let options = Options::new(10).initial_indent("!!!"); assert_eq!(fill("", &options), "!!!"); } #[test] fn indent_single_line() { let options = Options::new(10).initial_indent(">>>"); // No trailing space assert_eq!(fill("foo", &options), ">>>foo"); } #[test] #[cfg(feature = "unicode-width")] 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 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() { // This is a corner-case showing how the long word is broken // according to the width of the subsequent lines. The first // fragment of the word no longer fits on the first line, // which ends up being pure indentation. 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_static() { let options = Options::new(8).word_splitter(word_splitters::HyphenSplitter); assert_eq!(wrap("foo bar-baz", &options), vec!["foo bar-", "baz"]); } #[test] fn simple_hyphens_dynamic() { let options: Options<_, _> = Options::new(8).word_splitter(Box::new(word_splitters::HyphenSplitter)); assert_eq!(wrap("foo bar-baz", &options), vec!["foo bar-", "baz"]); } #[test] fn no_hyphenation_static() { let options = Options::new(8).word_splitter(word_splitters::NoHyphenation); assert_eq!(wrap("foo bar-baz", &options), vec!["foo", "bar-baz"]); } #[test] fn no_hyphenation_dynamic() { let options: Options<_, _> = Options::new(8).word_splitter(Box::new(word_splitters::NoHyphenation)); assert_eq!(wrap("foo bar-baz", &options), vec!["foo", "bar-baz"]); } #[test] #[cfg(feature = "hyphenation")] fn auto_hyphenation_double_hyphenation_static() { 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(dictionary); assert_eq!( wrap("Internationalization", &options), vec!["Interna-", "tionaliza-", "tion"] ); } #[test] #[cfg(feature = "hyphenation")] fn auto_hyphenation_double_hyphenation_dynamic() { let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap(); let mut options: Options<_, _, Box> = Options::new(10).word_splitter(Box::new(word_splitters::HyphenSplitter)); assert_eq!( wrap("Internationalization", &options), vec!["Internatio", "nalization"] ); options = Options::new(10).word_splitter(Box::new(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(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() { // Test that hyphenation takes the width of the wihtespace // into account. let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap(); let options = Options::new(15).word_splitter(dictionary); assert_eq!( wrap("garbage collection", &options), vec!["garbage col-", "lection"] ); } #[test] #[cfg(feature = "hyphenation")] fn borrowed_lines() { // Lines that end with an extra hyphen are owned, the final // line is borrowed. use std::borrow::Cow::{Borrowed, Owned}; let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap(); let options = Options::new(10).word_splitter(dictionary); let lines = wrap("Internationalization", &options); 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(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() { // Even the poor man's version of `ch_width` counts these // characters as wide. let options = Options::new(5).word_separator(word_separators::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 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 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(wrap_algorithms::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(wrap_algorithms::FirstFit) ), "1 3 5\n7\n1 3 5\n7" ); } #[test] fn preserve_line_breaks_with_whitespace() { assert_eq!(fill(" ", 80), ""); assert_eq!(fill(" \n ", 80), "\n"); assert_eq!(fill(" \n \n \n ", 80), "\n\n\n"); } #[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_simple() { assert_eq!(fill("foo bar baz", 10), "foo bar\nbaz"); } #[test] fn fill_colored_text() { // The words are much longer than 6 bytes, but they remain // intact after filling the 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!( fill(&(String::from(green_hello) + " " + &blue_world), 6), String::from(green_hello) + "\n" + &blue_world ); } #[test] #[cfg(not(feature = "smawk"))] #[cfg(not(feature = "unicode-linebreak"))] fn cloning_works() { static OPT: Options< wrap_algorithms::FirstFit, word_separators::AsciiSpace, word_splitters::HyphenSplitter, > = Options::with_word_splitter(80, word_splitters::HyphenSplitter); #[allow(clippy::clone_on_copy)] let opt = OPT.clone(); assert_eq!(opt.width, 80); } #[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"); } #[test] fn unfill_simple() { let (text, options) = unfill("foo\nbar"); assert_eq!(text, "foo bar"); assert_eq!(options.width, 3); } #[test] fn unfill_trailing_newlines() { let (text, options) = unfill("foo\nbar\n\n\n"); assert_eq!(text, "foo bar\n\n\n"); assert_eq!(options.width, 3); } #[test] fn unfill_initial_indent() { let (text, options) = unfill(" foo\nbar\nbaz"); assert_eq!(text, "foo bar baz"); assert_eq!(options.width, 5); assert_eq!(options.initial_indent, " "); } #[test] fn unfill_differing_indents() { let (text, options) = unfill(" foo\n bar\n baz"); assert_eq!(text, "foo bar baz"); assert_eq!(options.width, 7); assert_eq!(options.initial_indent, " "); assert_eq!(options.subsequent_indent, " "); } #[test] fn unfill_list_item() { let (text, options) = unfill("* foo\n bar\n baz"); assert_eq!(text, "foo bar baz"); assert_eq!(options.width, 5); assert_eq!(options.initial_indent, "* "); assert_eq!(options.subsequent_indent, " "); } #[test] fn unfill_multiple_char_prefix() { let (text, options) = unfill(" // foo bar\n // baz\n // quux"); assert_eq!(text, "foo bar baz quux"); assert_eq!(options.width, 14); assert_eq!(options.initial_indent, " // "); assert_eq!(options.subsequent_indent, " // "); } #[test] fn unfill_block_quote() { let (text, options) = unfill("> foo\n> bar\n> baz"); assert_eq!(text, "foo bar baz"); assert_eq!(options.width, 5); assert_eq!(options.initial_indent, "> "); assert_eq!(options.subsequent_indent, "> "); } #[test] fn unfill_whitespace() { assert_eq!(unfill("foo bar").0, "foo bar"); } #[test] fn trait_object_vec() { // Create a vector of Options containing trait-objects. let mut vector: Vec< Options< _, Box, Box, >, > = Vec::new(); // Expected result from each options let mut results = Vec::new(); let opt_full_type: Options< _, Box, Box, > = Options::new(10) .word_splitter(Box::new(word_splitters::HyphenSplitter) as Box) .word_separator(Box::new(word_separators::AsciiSpace) as Box); vector.push(opt_full_type); results.push(vec!["over-", "caffinated"]); // Actually: Options, Box> let opt_abbreviated_type = Options::new(10) .break_words(false) .word_splitter(Box::new(word_splitters::NoHyphenation) as Box) .word_separator(Box::new(word_separators::AsciiSpace) as Box); vector.push(opt_abbreviated_type); results.push(vec!["over-caffinated"]); #[cfg(feature = "hyphenation")] { let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap(); let opt_hyp = Options::new(8) .word_splitter(Box::new(dictionary) as Box) .word_separator(Box::new(word_separators::AsciiSpace) as Box); vector.push(opt_hyp); results.push(vec!["over-", "caffi-", "nated"]); } // Test each entry for (opt, expected) in vector.into_iter().zip(results) { assert_eq!(wrap("over-caffinated", opt), expected); } } #[test] fn wrap_columns_empty_text() { assert_eq!(wrap_columns("", 1, 10, "| ", "", " |"), vec!["| |"]); } #[test] fn wrap_columns_single_column() { assert_eq!( wrap_columns("Foo", 3, 30, "| ", " | ", " |"), vec!["| Foo | | |"] ); } #[test] fn wrap_columns_uneven_columns() { // The gaps take up a total of 5 columns, so the columns are // (21 - 5)/4 = 4 columns wide: assert_eq!( wrap_columns("Foo Bar Baz Quux", 4, 21, "|", "|", "|"), vec!["|Foo |Bar |Baz |Quux|"] ); // As the total width increases, the last column absorbs the // excess width: assert_eq!( wrap_columns("Foo Bar Baz Quux", 4, 24, "|", "|", "|"), vec!["|Foo |Bar |Baz |Quux |"] ); // Finally, when the width is 25, the columns can be resized // to a width of (25 - 5)/4 = 5 columns: assert_eq!( wrap_columns("Foo Bar Baz Quux", 4, 25, "|", "|", "|"), vec!["|Foo |Bar |Baz |Quux |"] ); } #[test] #[cfg(feature = "unicode-width")] fn wrap_columns_with_emojis() { assert_eq!( wrap_columns( "Words and a few emojis 😍 wrapped in ⓶ columns", 2, 30, "✨ ", " ⚽ ", " 👀" ), vec![ "✨ Words ⚽ wrapped in 👀", "✨ and a few ⚽ ⓶ columns 👀", "✨ emojis 😍 ⚽ 👀" ] ); } #[test] fn wrap_columns_big_gaps() { // The column width shrinks to 1 because the gaps take up all // the space. assert_eq!( wrap_columns("xyz", 2, 10, "----> ", " !!! ", " <----"), vec![ "----> x !!! z <----", // "----> y !!! <----" ] ); } #[test] #[should_panic] fn wrap_columns_panic_with_zero_columns() { wrap_columns("", 0, 10, "", "", ""); } }