blob: ee6d5d850f9d7606ff46b18a22ddf33df15e719d [file] [log] [blame] [edit]
//! 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).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:
//!
//! <img src="https://raw.githubusercontent.com/mgeisler/textwrap/master/images/textwrap-0.13.4.svg">
//!
//! ## Default Features
//!
//! These features are enabled by default:
//!
//! * `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 [`core::wrap_optimal_fit`] function
//! for details on the optimal-fit algorithm.
//!
//! This feature can be disabled if you only ever intend to use
//! [`core::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 [`WordSplitter`] trait for details.
//!
//! [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.13.4")]
#![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;
mod splitting;
pub use crate::splitting::{HyphenSplitter, NoHyphenation, WordSplitter};
pub mod core;
/// Holds settings for wrapping and filling text.
#[derive(Debug, Clone)]
pub struct Options<'a, S: ?Sized = Box<dyn WordSplitter>> {
/// 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,
/// Wraping algorithm to use, see [`core::WrapAlgorithm`] for
/// details.
pub wrap_algorithm: core::WrapAlgorithm,
/// 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
/// [`WordSplitter`] trait for details.
pub splitter: S,
}
impl<'a, S: ?Sized> From<&'a Options<'a, S>> for Options<'a, &'a S> {
fn from(options: &'a Options<'a, S>) -> Self {
Self {
width: options.width,
initial_indent: options.initial_indent,
subsequent_indent: options.subsequent_indent,
break_words: options.break_words,
wrap_algorithm: options.wrap_algorithm,
splitter: &options.splitter,
}
}
}
impl<'a> From<usize> for Options<'a, HyphenSplitter> {
fn from(width: usize) -> Self {
Options::new(width)
}
}
/// Constructors for boxed Options, specifically.
impl<'a> Options<'a, HyphenSplitter> {
/// Creates a new [`Options`] with the specified width and static
/// dispatch using the [`HyphenSplitter`]. Equivalent to
///
/// ```
/// # use textwrap::{Options, HyphenSplitter, WordSplitter};
/// # let width = 80;
/// # let actual = Options::new(width);
/// # let expected =
/// Options {
/// width: width,
/// initial_indent: "",
/// subsequent_indent: "",
/// break_words: true,
/// #[cfg(feature = "smawk")]
/// wrap_algorithm: textwrap::core::WrapAlgorithm::OptimalFit,
/// #[cfg(not(feature = "smawk"))]
/// wrap_algorithm: textwrap::core::WrapAlgorithm::FirstFit,
/// splitter: 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);
/// # assert_eq!(actual.wrap_algorithm, expected.wrap_algorithm);
/// # let expected_coerced: Options<'static, HyphenSplitter> = expected;
/// ```
///
/// Note that the default wrap algorithm changes based on the
/// `smawk` Cargo feature. The best available algorithm is used by
/// default.
///
/// Static dispatch mean here, that the splitter is stored as-is
/// and the type is known at compile-time. Thus the returned value
/// is actually a `Options<HyphenSplitter>`.
///
/// Dynamic dispatch on the other hand, mean that the splitter is
/// stored as a trait object for instance in a `Box<dyn
/// WordSplitter>`. This way the 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<dyn
/// WordSplitter>>`.
///
/// The value and type of the splitter can be choose from the
/// start using the [`Options::with_splitter`] constructor or
/// changed afterwards using the [`Options::splitter`] method.
/// Whether static or dynamic dispatch is used, depends on whether
/// these functions are given a boxed [`WordSplitter`] or not.
/// Take for example:
///
/// ```
/// use textwrap::{HyphenSplitter, NoHyphenation, Options};
/// # use textwrap::{WordSplitter};
/// # let width = 80;
///
/// // uses HyphenSplitter with static dispatch
/// // the actual type: Options<HyphenSplitter>
/// let opt = Options::new(width);
/// # let opt_coerce: Options<HyphenSplitter> = opt;
///
/// // uses NoHyphenation with static dispatch
/// // the actual type: Options<NoHyphenation>
/// let opt = Options::new(width).splitter(NoHyphenation);
/// # let opt_coerce: Options<NoHyphenation> = opt;
///
/// // uses HyphenSplitter with dynamic dispatch
/// // the actual type: Options<Box<dyn WordSplitter>>
/// let opt: Options = Options::new(width).splitter(Box::new(HyphenSplitter));
/// # let opt_coerce: Options<Box<dyn WordSplitter>> = opt;
///
/// // uses NoHyphenation with dynamic dispatch
/// // the actual type: Options<Box<dyn WordSplitter>>
/// let opt: Options = Options::new(width).splitter(Box::new(NoHyphenation));
/// # let opt_coerce: Options<Box<dyn WordSplitter>> = opt;
/// ```
///
/// Notice that the last two variables have the same type, despite
/// the different `WordSplitter` in use. Thus dynamic dispatch
/// allows to change the splitter at run-time without changing the
/// variables type.
pub const fn new(width: usize) -> Self {
Options::with_splitter(width, 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, S> Options<'a, S> {
/// Creates a new [`Options`] with the specified width and
/// splitter. Equivalent to
///
/// ```
/// # use textwrap::{Options, NoHyphenation, HyphenSplitter};
/// # const splitter: NoHyphenation = NoHyphenation;
/// # const width: usize = 80;
/// # const actual: Options<'static, NoHyphenation> = Options::with_splitter(width, splitter);
/// # let expected =
/// Options {
/// width: width,
/// initial_indent: "",
/// subsequent_indent: "",
/// break_words: true,
/// #[cfg(feature = "smawk")]
/// wrap_algorithm: textwrap::core::WrapAlgorithm::OptimalFit,
/// #[cfg(not(feature = "smawk"))]
/// wrap_algorithm: textwrap::core::WrapAlgorithm::FirstFit,
/// splitter: 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);
/// # assert_eq!(actual.wrap_algorithm, expected.wrap_algorithm);
/// # let expected_coerced: Options<'static, NoHyphenation> = expected;
/// ```
///
/// This constructor allows to specify the splitter to be used. It
/// is like a short-cut for `Options::new(w).splitter(s)`, but
/// this function is a `const fn`. The given splitter may be in a
/// [`Box`], which then can be coerced into a trait object for
/// dynamic dispatch:
///
/// ```
/// use textwrap::{HyphenSplitter, NoHyphenation, Options};
/// # use textwrap::{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 = Options::with_splitter(width, Box::new(NoHyphenation));
/// // Its type is actually: `Options<Box<dyn WordSplitter>>`:
/// let opt_coerced: Options<Box<dyn WordSplitter>> = opt;
///
/// // Thus, it can be overridden with a different splitter.
/// opt = Options::with_splitter(width, Box::new(HyphenSplitter));
/// // Now, containing a `HyphenSplitter` instead.
/// ```
///
/// Since the 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
/// splitter at run time as seen above, static dispatch especially
/// can store the splitter directly, without the need for a box.
/// This in turn allows it to be used in constant and static
/// context:
///
/// ```
/// use textwrap::{HyphenSplitter, Options};
/// # const width: usize = 80;
///
/// const FOO: Options<HyphenSplitter> = Options::with_splitter(width, HyphenSplitter);
/// static BAR: Options<HyphenSplitter> = FOO;
/// ```
pub const fn with_splitter(width: usize, splitter: S) -> Self {
Options {
width,
initial_indent: "",
subsequent_indent: "",
break_words: true,
#[cfg(feature = "smawk")]
wrap_algorithm: core::WrapAlgorithm::OptimalFit,
#[cfg(not(feature = "smawk"))]
wrap_algorithm: core::WrapAlgorithm::FirstFit,
splitter: splitter,
}
}
}
impl<'a, S: WordSplitter> Options<'a, S> {
/// 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.wrap_algorithm`].
///
/// See [`core::WrapAlgorithm`] for details on the choices.
///
/// [`self.wrap_algorithm`]: #structfield.wrap_algorithm
pub fn wrap_algorithm(self, wrap_algorithm: core::WrapAlgorithm) -> Self {
Options {
wrap_algorithm,
..self
}
}
/// Change [`self.splitter`]. The [`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::{HyphenSplitter, NoHyphenation, Options};
/// // The default type returned by `new` is `Options<HyphenSplitter>`
/// let opt: Options<HyphenSplitter> = Options::new(80);
/// // Setting a different splitter changes the type
/// let opt: Options<NoHyphenation> = opt.splitter(NoHyphenation);
/// ```
///
/// [`self.splitter`]: #structfield.splitter
pub fn splitter<T>(self, splitter: T) -> Options<'a, T> {
Options {
width: self.width,
initial_indent: self.initial_indent,
subsequent_indent: self.subsequent_indent,
break_words: self.break_words,
wrap_algorithm: self.wrap_algorithm,
splitter: 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, NoHyphenation, Options};
///
/// let width = termwidth() - 4; // Two columns on each side.
/// let options = Options::new(width)
/// .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, S, Opt>(text: &str, width_or_options: Opt) -> String
where
S: WordSplitter,
Opt: Into<Options<'a, S>>,
{
// 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<'a>(text: &'a str) -> (String, Options<'a, 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;
///
/// let text = "\
/// > Memory safety without
/// > garbage collection.
/// ";
/// assert_eq!(refill(text, 15), "\
/// > Memory safety
/// > without
/// > garbage
/// > collection.
/// ");
pub fn refill<'a, S, Opt>(filled_text: &str, new_width_or_options: Opt) -> String
where
S: WordSplitter,
Opt: Into<Options<'a, S>>,
{
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 a trailing `'\n'`. 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::{Options, wrap};
/// # use textwrap::core::WrapAlgorithm::FirstFit;
/// #
/// # 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::core::WrapAlgorithm::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 [`core::WrapAlgorithm`] 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::<Vec<_>>();
/// assert_eq!(
/// annotated,
/// &[
/// "[Borrowed] Wrapping text",
/// "[Owned] ....all day",
/// "[Owned] ....long.",
/// ]
/// );
/// ```
pub fn wrap<'a, S, Opt>(text: &str, width_or_options: Opt) -> Vec<Cow<'_, str>>
where
S: WordSplitter,
Opt: Into<Options<'a, S>>,
{
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 = core::find_words(line);
let split_words = core::split_words(words, &options);
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::<Vec<_>>()
};
#[rustfmt::skip]
let line_lengths = |i| if i == 0 { initial_width } else { subsequent_width };
let wrapped_words = match options.wrap_algorithm {
#[cfg(feature = "smawk")]
core::WrapAlgorithm::OptimalFit => core::wrap_optimal_fit(&broken_words, line_lengths),
core::WrapAlgorithm::FirstFit => core::wrap_first_fit(&broken_words, line_lengths),
};
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::<usize>()
- 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`, `mid_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, S, Opt>(
text: &str,
columns: usize,
total_width_or_options: Opt,
left_gap: &str,
mid_gap: &str,
right_gap: &str,
) -> Vec<String>
where
S: WordSplitter,
Opt: Into<Options<'a, S>>,
{
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(mid_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(mid_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. Indentation is also ruled out. In other
/// words, `fill_inplace(width)` behaves as if you had called [`fill`]
/// with these options:
///
/// ```
/// # use textwrap::{Options, NoHyphenation};
/// # let width = 80;
/// Options {
/// width: width,
/// initial_indent: "",
/// subsequent_indent: "",
/// break_words: false,
/// wrap_algorithm: textwrap::core::WrapAlgorithm::FirstFit,
/// splitter: NoHyphenation,
/// };
/// ```
///
/// The wrap algorithm is [`core::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/benches/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 = core::find_words(line).collect::<Vec<_>>();
let wrapped_words = core::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::<usize>();
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::*;
#[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.splitter.split_points("hello-world"),
opt_options.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(core::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_value()), vec!["foo bar"]);
}
#[test]
fn leading_whitespace() {
assert_eq!(wrap(" foo bar", 6), vec![" foo", "bar"]);
}
#[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.
assert_eq!(wrap("x – x", 1), 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!", 15),
vec!["Hello,", "World!"]
);
}
#[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).splitter(HyphenSplitter);
assert_eq!(wrap("foo bar-baz", &options), vec!["foo bar-", "baz"]);
}
#[test]
fn simple_hyphens_dynamic() {
let options: Options = Options::new(8).splitter(Box::new(HyphenSplitter));
assert_eq!(wrap("foo bar-baz", &options), vec!["foo bar-", "baz"]);
}
#[test]
fn no_hyphenation_static() {
let options = Options::new(8).splitter(NoHyphenation);
assert_eq!(wrap("foo bar-baz", &options), vec!["foo", "bar-baz"]);
}
#[test]
fn no_hyphenation_dynamic() {
let options: Options = Options::new(8).splitter(Box::new(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).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 = Options::new(10).splitter(Box::new(HyphenSplitter));
assert_eq!(
wrap("Internationalization", &options),
vec!["Internatio", "nalization"]
);
options = Options::new(10).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).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).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).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.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.
assert_eq!(wrap("Hello", 5), 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(core::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(core::WrapAlgorithm::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]
fn cloning_works() {
static OPT: Options<HyphenSplitter> = Options::with_splitter(80, 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() {
let opt_a: Options<NoHyphenation> = Options::with_splitter(20, NoHyphenation);
let opt_b: Options<HyphenSplitter> = 10.into();
let mut dyn_opt: &Options<dyn WordSplitter> = &opt_a;
assert_eq!(wrap("foo bar-baz", dyn_opt), vec!["foo bar-baz"]);
// Just assign a totally different option
dyn_opt = &opt_b;
assert_eq!(wrap("foo bar-baz", dyn_opt), vec!["foo bar-", "baz"]);
}
#[test]
fn trait_object_vec() {
// Create a vector of referenced trait-objects
let mut vector: Vec<&Options<dyn WordSplitter>> = Vec::new();
// Expected result from each options
let mut results = Vec::new();
let opt_usize: Options<_> = 10.into();
vector.push(&opt_usize);
results.push(vec!["over-", "caffinated"]);
#[cfg(feature = "hyphenation")]
let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap();
#[cfg(feature = "hyphenation")]
let opt_hyp = Options::new(8).splitter(dictionary);
#[cfg(feature = "hyphenation")]
vector.push(&opt_hyp);
#[cfg(feature = "hyphenation")]
results.push(vec!["over-", "caffi-", "nated"]);
// Actually: Options<Box<dyn WordSplitter>>
let opt_box: Options = Options::new(10)
.break_words(false)
.splitter(Box::new(NoHyphenation));
vector.push(&opt_box);
results.push(vec!["over-caffinated"]);
// Test each entry
for (opt, expected) in vector.into_iter().zip(results) {
assert_eq!(
// Just all the totally different options
wrap("over-caffinated", opt),
expected
);
}
}
#[test]
fn outer_boxing() {
let mut wrapper: Box<Options<dyn WordSplitter>> = Box::new(Options::new(80));
// We must first deref the Box into a trait object and pass it by-reference
assert_eq!(wrap("foo bar baz", &*wrapper), vec!["foo bar baz"]);
// Replace the `Options` with a `usize`
wrapper = Box::new(Options::from(5));
// Deref per-se works as well, it already returns a reference
use std::ops::Deref;
assert_eq!(
wrap("foo bar baz", wrapper.deref()),
vec!["foo", "bar", "baz"]
);
}
#[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, "", "", "");
}
}