aboutsummaryrefslogtreecommitdiff
path: root/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib.rs')
-rw-r--r--src/lib.rs763
1 files changed, 405 insertions, 358 deletions
diff --git a/src/lib.rs b/src/lib.rs
index 6d68309..ee6d5d8 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -7,35 +7,47 @@
//! you want to format dynamic output nicely so it looks good in a
//! terminal. A quick example:
//!
-//! ```
-//! # #[cfg(feature = "smawk")] {
-//! let text = "textwrap: a small library for wrapping text.";
-//! assert_eq!(textwrap::wrap(text, 18),
-//! vec!["textwrap: a",
-//! "small library for",
-//! "wrapping text."]);
-//! # }
+//! ```no_run
+//! fn main() {
+//! let text = "textwrap: a small library for wrapping text.";
+//! println!("{}", textwrap::fill(text, 18));
+//! }
//! ```
//!
-//! The [`wrap`] function returns the individual lines, use [`fill`]
-//! is you want the lines joined with `'\n'` to form a `String`.
+//! 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:
//!
-//! ```
-//! #[cfg(feature = "hyphenation")] {
+//! ```no_run
+//! # #[cfg(feature = "hyphenation")]
//! use hyphenation::{Language, Load, Standard};
-//! use textwrap::{wrap, Options, WordSplitter};
+//! use textwrap::{fill, Options};
//!
-//! let text = "textwrap: a small library for wrapping text.";
-//! let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap();
-//! let options = Options::new(18).word_splitter(WordSplitter::Hyphenation(dictionary));
-//! assert_eq!(wrap(text, &options),
-//! vec!["textwrap: a small",
-//! "library for wrap-",
-//! "ping text."]);
+//! # #[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
@@ -74,12 +86,12 @@
//! into a bullet list:
//!
//! ```
-//! let before = "\
+//! let before = "
//! foo
//! bar
//! baz
//! ";
-//! let after = "\
+//! let after = "
//! * foo
//! * bar
//! * baz
@@ -112,22 +124,12 @@
//! 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.15.0.svg">
+//! <img src="https://raw.githubusercontent.com/mgeisler/textwrap/master/images/textwrap-0.13.4.svg">
//!
//! ## 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 [`WordSeparator`] 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
@@ -140,34 +142,11 @@
//! 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.
+//! 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
-//! [`wrap_algorithms::wrap_first_fit`].
-//!
-//! With Rust 1.59.0, the size impact of the above features on your
-//! binary is as follows:
-//!
-//! | Configuration | Binary Size | Delta |
-//! | :--- | ---: | ---: |
-//! | quick-and-dirty implementation | 289 KB | — KB |
-//! | textwrap without default features | 301 KB | 12 KB |
-//! | textwrap with smawk | 317 KB | 28 KB |
-//! | textwrap with unicode-width | 313 KB | 24 KB |
-//! | textwrap with unicode-linebreak | 395 KB | 106 KB |
-//!
-//! The above sizes are the stripped sizes and the binary is compiled
-//! in release mode with this profile:
-//!
-//! ```toml
-//! [profile.release]
-//! lto = true
-//! codegen-units = 1
-//! ```
-//!
-//! See the [binary-sizes demo] if you want to reproduce these
-//! results.
+//! [`core::wrap_first_fit`].
//!
//! ## Optional Features
//!
@@ -178,61 +157,34 @@
//! [`Options::with_termwidth`] constructor for details.
//!
//! * `hyphenation`: enables language-sensitive hyphenation via the
-//! [hyphenation] crate. See the [`word_splitters::WordSplitter`]
-//! trait for details.
+//! [hyphenation] crate. See the [`WordSplitter`] trait for details.
//!
-//! [unicode-linebreak]: https://docs.rs/unicode-linebreak/
//! [unicode-width]: https://docs.rs/unicode-width/
//! [smawk]: https://docs.rs/smawk/
-//! [binary-sizes demo]: https://github.com/mgeisler/textwrap/tree/master/examples/binary-sizes
//! [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.15.0")]
+#![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)]
-// Make `cargo test` execute the README doctests.
-#[cfg(doctest)]
-#[doc = include_str!("../README.md")]
-mod readme_doctest {}
-
use std::borrow::Cow;
mod indentation;
-pub use crate::indentation::{dedent, indent};
-
-mod word_separators;
-pub use word_separators::WordSeparator;
-
-pub mod word_splitters;
-pub use word_splitters::WordSplitter;
+pub use crate::indentation::dedent;
+pub use crate::indentation::indent;
-pub mod wrap_algorithms;
-pub use wrap_algorithms::WrapAlgorithm;
+mod splitting;
+pub use crate::splitting::{HyphenSplitter, NoHyphenation, WordSplitter};
pub mod core;
-#[cfg(feature = "unicode-linebreak")]
-macro_rules! DefaultWordSeparator {
- () => {
- WordSeparator::UnicodeBreakProperties
- };
-}
-
-#[cfg(not(feature = "unicode-linebreak"))]
-macro_rules! DefaultWordSeparator {
- () => {
- WordSeparator::AsciiSpace
- };
-}
-
-/// Holds configuration options for wrapping and filling text.
+/// Holds settings for wrapping and filling text.
#[derive(Debug, Clone)]
-pub struct Options<'a> {
+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
@@ -245,44 +197,42 @@ pub struct Options<'a> {
/// 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: WrapAlgorithm,
- /// The line breaking algorithm to use, see
- /// [`word_separators::WordSeparator`] trait for an overview and
- /// possible implementations.
- pub word_separator: WordSeparator,
+ /// 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.
- pub word_splitter: WordSplitter,
+ /// language-aware machine hyphenation. Please see the
+ /// [`WordSplitter`] trait for details.
+ pub splitter: S,
}
-impl<'a> From<&'a Options<'a>> for Options<'a> {
- fn from(options: &'a Options<'a>) -> Self {
+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,
- word_separator: options.word_separator,
wrap_algorithm: options.wrap_algorithm,
- word_splitter: options.word_splitter.clone(),
+ splitter: &options.splitter,
}
}
}
-impl<'a> From<usize> for Options<'a> {
+impl<'a> From<usize> for Options<'a, HyphenSplitter> {
fn from(width: usize) -> Self {
Options::new(width)
}
}
-impl<'a> Options<'a> {
- /// Creates a new [`Options`] with the specified width. Equivalent to
+/// 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, WordSplitter, WordSeparator, WrapAlgorithm};
+ /// # use textwrap::{Options, HyphenSplitter, WordSplitter};
/// # let width = 80;
/// # let actual = Options::new(width);
/// # let expected =
@@ -291,37 +241,75 @@ impl<'a> Options<'a> {
/// initial_indent: "",
/// subsequent_indent: "",
/// break_words: true,
- /// #[cfg(feature = "unicode-linebreak")]
- /// word_separator: WordSeparator::UnicodeBreakProperties,
- /// #[cfg(not(feature = "unicode-linebreak"))]
- /// word_separator: WordSeparator::AsciiSpace,
/// #[cfg(feature = "smawk")]
- /// wrap_algorithm: WrapAlgorithm::new_optimal_fit(),
+ /// wrap_algorithm: textwrap::core::WrapAlgorithm::OptimalFit,
/// #[cfg(not(feature = "smawk"))]
- /// wrap_algorithm: WrapAlgorithm::FirstFit,
- /// word_splitter: WordSplitter::HyphenSplitter,
+ /// 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.word_splitter, expected.word_splitter);
+ /// # 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;
/// ```
///
- /// Note that the default word separator and wrap algorithms
- /// changes based on the available Cargo features. The best
- /// available algorithms are used by default.
+ /// 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 {
- width,
- initial_indent: "",
- subsequent_indent: "",
- break_words: true,
- word_separator: DefaultWordSeparator!(),
- wrap_algorithm: WrapAlgorithm::new(),
- word_splitter: WordSplitter::HyphenSplitter,
- }
+ Options::with_splitter(width, HyphenSplitter)
}
/// Creates a new [`Options`] with `width` set to the current
@@ -347,7 +335,90 @@ impl<'a> Options<'a> {
}
}
-impl<'a> Options<'a> {
+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.
///
@@ -357,7 +428,7 @@ impl<'a> Options<'a> {
/// initial indentation and wrapping each paragraph by itself:
///
/// ```
- /// use textwrap::{wrap, Options};
+ /// use textwrap::{Options, wrap};
///
/// let options = Options::new(16).initial_indent(" ");
/// assert_eq!(wrap("This is a little example.", options),
@@ -382,7 +453,7 @@ impl<'a> Options<'a> {
/// single paragraph as a bullet list:
///
/// ```
- /// use textwrap::{wrap, Options};
+ /// use textwrap::{Options, wrap};
///
/// let options = Options::new(12)
/// .initial_indent("* ")
@@ -436,74 +507,50 @@ impl<'a> Options<'a> {
}
}
- /// 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: WordSeparator) -> Options<'a> {
- 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.
+ /// See [`core::WrapAlgorithm`] for details on the choices.
///
/// [`self.wrap_algorithm`]: #structfield.wrap_algorithm
- pub fn wrap_algorithm(self, wrap_algorithm: WrapAlgorithm) -> Options<'a> {
+ pub fn wrap_algorithm(self, wrap_algorithm: core::WrapAlgorithm) -> Self {
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,
+ wrap_algorithm,
+ ..self
}
}
- /// Change [`self.word_splitter`]. The
- /// [`word_splitters::WordSplitter`] is used to fit part of a word
- /// into the current line when wrapping text.
+ /// Change [`self.splitter`]. The [`WordSplitter`] is used to fit
+ /// part of a word into the current line when wrapping text.
///
- /// # Examples
+ /// 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::{Options, WordSplitter};
- /// let opt = Options::new(80);
- /// assert_eq!(opt.word_splitter, WordSplitter::HyphenSplitter);
- /// let opt = opt.word_splitter(WordSplitter::NoHyphenation);
- /// assert_eq!(opt.word_splitter, WordSplitter::NoHyphenation);
+ /// 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.word_splitter`]: #structfield.word_splitter
- pub fn word_splitter(self, word_splitter: WordSplitter) -> Options<'a> {
+ /// [`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,
- word_separator: self.word_separator,
wrap_algorithm: self.wrap_algorithm,
- word_splitter,
+ 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.
+/// 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
///
@@ -511,10 +558,11 @@ impl<'a> Options<'a> {
/// with a two column margin to the left and the right:
///
/// ```no_run
-/// use textwrap::{termwidth, Options};
+/// 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(" ");
/// ```
@@ -558,9 +606,10 @@ pub fn termwidth() -> usize {
/// "- Memory safety\n without\n garbage\n collection."
/// );
/// ```
-pub fn fill<'a, Opt>(text: &str, width_or_options: Opt) -> String
+pub fn fill<'a, S, Opt>(text: &str, width_or_options: Opt) -> String
where
- Opt: Into<Options<'a>>,
+ S: WordSplitter,
+ Opt: Into<Options<'a, S>>,
{
// This will avoid reallocation in simple cases (no
// indentation, no hyphenation).
@@ -570,7 +619,7 @@ where
if i > 0 {
result.push('\n');
}
- result.push_str(line);
+ result.push_str(&line);
}
result
@@ -622,7 +671,7 @@ where
/// assert_eq!(options.initial_indent, "* ");
/// assert_eq!(options.subsequent_indent, " ");
/// ```
-pub fn unfill(text: &str) -> (String, Options<'_>) {
+pub fn unfill<'a>(text: &'a str) -> (String, Options<'a, HyphenSplitter>) {
let trimmed = text.trim_end_matches('\n');
let prefix_chars: &[_] = &[' ', '-', '+', '*', '>', '#', '/'];
@@ -679,47 +728,20 @@ pub fn unfill(text: &str) -> (String, Options<'_>) {
/// ```
/// use textwrap::refill;
///
-/// // Some loosely wrapped text. The "> " prefix is recognized automatically.
/// let text = "\
-/// > Memory
-/// > safety without garbage
-/// > collection.
+/// > Memory safety without
+/// > garbage collection.
/// ";
-///
-/// assert_eq!(refill(text, 20), "\
+/// assert_eq!(refill(text, 15), "\
/// > Memory safety
-/// > without garbage
+/// > 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, Opt>(filled_text: &str, new_width_or_options: Opt) -> String
+pub fn refill<'a, S, Opt>(filled_text: &str, new_width_or_options: Opt) -> String
where
- Opt: Into<Options<'a>>,
+ S: WordSplitter,
+ Opt: Into<Options<'a, S>>,
{
let trimmed = filled_text.trim_end_matches('\n');
let (text, options) = unfill(trimmed);
@@ -735,9 +757,8 @@ where
///
/// 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.
+/// `&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`:
@@ -785,7 +806,8 @@ where
/// narrow column with room for only 10 characters looks like this:
///
/// ```
-/// # use textwrap::{WrapAlgorithm::FirstFit, Options, wrap};
+/// # 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));
@@ -809,12 +831,11 @@ where
///
/// ```
/// # #[cfg(feature = "smawk")] {
-/// # use textwrap::{Options, WrapAlgorithm, wrap};
+/// # 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(WrapAlgorithm::new_optimal_fit())
-/// # );
+/// # 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
@@ -824,7 +845,7 @@ where
/// # "); }
/// ```
///
-/// Please see [`WrapAlgorithm`] for details on the choices.
+/// Please see [`core::WrapAlgorithm`] for details.
///
/// # Examples
///
@@ -855,55 +876,10 @@ where
/// ]
/// );
/// ```
-///
-/// ## 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, Opt>(text: &str, width_or_options: Opt) -> Vec<Cow<'_, str>>
+pub fn wrap<'a, S, Opt>(text: &str, width_or_options: Opt) -> Vec<Cow<'_, str>>
where
- Opt: Into<Options<'a>>,
+ S: WordSplitter,
+ Opt: Into<Options<'a, S>>,
{
let options = width_or_options.into();
@@ -916,8 +892,8 @@ where
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 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() {
@@ -933,8 +909,13 @@ where
split_words.collect::<Vec<_>>()
};
- let line_widths = [initial_width, subsequent_width];
- let wrapped_words = options.wrap_algorithm.wrap(&broken_words, &line_widths);
+ #[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 {
@@ -971,7 +952,7 @@ where
result += &line[idx..idx + len];
if !last_word.penalty.is_empty() {
- result.to_mut().push_str(last_word.penalty);
+ result.to_mut().push_str(&last_word.penalty);
}
lines.push(result);
@@ -988,7 +969,7 @@ where
/// Wrap text into columns with a given total width.
///
-/// The `left_gap`, `middle_gap` and `right_gap` arguments specify the
+/// 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
@@ -1043,16 +1024,17 @@ where
/// "| example text, | columns. | shorter than |",
/// "| which is | Notice how | the others. |",
/// "| wrapped into | the final | |"]);
-pub fn wrap_columns<'a, Opt>(
+pub fn wrap_columns<'a, S, Opt>(
text: &str,
columns: usize,
total_width_or_options: Opt,
left_gap: &str,
- middle_gap: &str,
+ mid_gap: &str,
right_gap: &str,
) -> Vec<String>
where
- Opt: Into<Options<'a>>,
+ S: WordSplitter,
+ Opt: Into<Options<'a, S>>,
{
assert!(columns > 0);
@@ -1062,7 +1044,7 @@ where
.width
.saturating_sub(core::display_width(left_gap))
.saturating_sub(core::display_width(right_gap))
- .saturating_sub(core::display_width(middle_gap) * (columns - 1));
+ .saturating_sub(core::display_width(mid_gap) * (columns - 1));
let column_width = std::cmp::max(inner_width / columns, 1);
options.width = column_width;
@@ -1076,8 +1058,8 @@ where
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)));
+ line.push_str(&column_line);
+ line.push_str(&" ".repeat(column_width - core::display_width(&column_line)));
}
None => {
line.push_str(&" ".repeat(column_width));
@@ -1086,7 +1068,7 @@ where
if column_no == columns - 1 {
line.push_str(&last_column_padding);
} else {
- line.push_str(middle_gap);
+ line.push_str(mid_gap);
}
}
line.push_str(right_gap);
@@ -1104,27 +1086,24 @@ where
///
/// 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:
+/// 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::{core, Options, WordSplitter, WordSeparator, WrapAlgorithm};
+/// # use textwrap::{Options, NoHyphenation};
/// # let width = 80;
/// Options {
/// width: width,
/// initial_indent: "",
/// subsequent_indent: "",
/// break_words: false,
-/// word_separator: WordSeparator::AsciiSpace,
-/// wrap_algorithm: WrapAlgorithm::FirstFit,
-/// word_splitter: WordSplitter::NoHyphenation,
+/// wrap_algorithm: textwrap::core::WrapAlgorithm::FirstFit,
+/// splitter: NoHyphenation,
/// };
/// ```
///
-/// The wrap algorithm is [`WrapAlgorithm::FirstFit`] since this
+/// 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.
@@ -1154,10 +1133,8 @@ pub fn fill_inplace(text: &mut String, width: usize) {
let mut offset = 0;
for line in text.split('\n') {
- let words = WordSeparator::AsciiSpace
- .find_words(line)
- .collect::<Vec<_>>();
- let wrapped_words = wrap_algorithms::wrap_first_fit(&words, &[width as f64]);
+ 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] {
@@ -1187,7 +1164,6 @@ pub fn fill_inplace(text: &mut String, width: usize) {
#[cfg(test)]
mod tests {
use super::*;
-
#[cfg(feature = "hyphenation")]
use hyphenation::{Language, Load, Standard};
@@ -1201,8 +1177,8 @@ mod tests {
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")
+ opt_usize.splitter.split_points("hello-world"),
+ opt_options.splitter.split_points("hello-world")
);
}
@@ -1221,7 +1197,7 @@ mod tests {
assert_eq!(
wrap(
"To be, or not to be, that is the question.",
- Options::new(10).wrap_algorithm(WrapAlgorithm::FirstFit)
+ Options::new(10).wrap_algorithm(core::WrapAlgorithm::FirstFit)
),
vec!["To be, or", "not to be,", "that is", "the", "question."]
);
@@ -1244,11 +1220,7 @@ mod tests {
#[test]
fn max_width() {
- assert_eq!(wrap("foo bar", usize::MAX), vec!["foo bar"]);
-
- let text = "Hello there! This is some English text. \
- It should not be wrapped given the extents below.";
- assert_eq!(wrap(text, usize::MAX), vec![text]);
+ assert_eq!(wrap("foo bar", usize::max_value()), vec!["foo bar"]);
}
#[test]
@@ -1257,15 +1229,6 @@ mod tests {
}
#[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
@@ -1287,31 +1250,17 @@ mod tests {
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(WordSeparator::AsciiSpace);
- assert_eq!(wrap("x – x", options), vec!["x", "–", "x"]);
+ 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!",
- Options::new(15).word_separator(WordSeparator::AsciiSpace)
- ),
+ wrap("Hello, World!", 15),
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(WordSeparator::UnicodeBreakProperties)
- ),
- vec!["Hello, W", "orld!"]
- );
}
#[test]
@@ -1331,6 +1280,7 @@ mod tests {
}
#[test]
+ #[cfg(feature = "unicode-width")]
fn indent_first_emoji() {
let options = Options::new(10).initial_indent("👉👉");
assert_eq!(
@@ -1438,20 +1388,32 @@ mod tests {
}
#[test]
- fn simple_hyphens() {
- let options = Options::new(8).word_splitter(WordSplitter::HyphenSplitter);
+ 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() {
- let options = Options::new(8).word_splitter(WordSplitter::NoHyphenation);
+ 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() {
+ fn auto_hyphenation_double_hyphenation_static() {
let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap();
let options = Options::new(10);
assert_eq!(
@@ -1459,7 +1421,24 @@ mod tests {
vec!["Internatio", "nalization"]
);
- let options = Options::new(10).word_splitter(WordSplitter::Hyphenation(dictionary));
+ 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"]
@@ -1476,7 +1455,7 @@ mod tests {
vec!["participat", "ion is", "the key to", "success"]
);
- let options = Options::new(10).word_splitter(WordSplitter::Hyphenation(dictionary));
+ 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"]
@@ -1486,10 +1465,10 @@ mod tests {
#[test]
#[cfg(feature = "hyphenation")]
fn split_len_hyphenation() {
- // Test that hyphenation takes the width of the whitespace
+ // 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(WordSplitter::Hyphenation(dictionary));
+ let options = Options::new(15).splitter(dictionary);
assert_eq!(
wrap("garbage collection", &options),
vec!["garbage col-", "lection"]
@@ -1503,9 +1482,8 @@ mod tests {
// line is borrowed.
use std::borrow::Cow::{Borrowed, Owned};
let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap();
- let options = Options::new(10).word_splitter(WordSplitter::Hyphenation(dictionary));
+ let options = Options::new(10).splitter(dictionary);
let lines = wrap("Internationalization", &options);
- assert_eq!(lines, vec!["Interna-", "tionaliza-", "tion"]);
if let Borrowed(s) = lines[0] {
assert!(false, "should not have been borrowed: {:?}", s);
}
@@ -1527,7 +1505,7 @@ mod tests {
vec!["over-", "caffinated"]
);
- let options = options.word_splitter(WordSplitter::Hyphenation(dictionary));
+ let options = options.splitter(dictionary);
assert_eq!(
wrap("over-caffinated", &options),
vec!["over-", "caffi-", "nated"]
@@ -1543,8 +1521,7 @@ mod tests {
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(WordSeparator::AsciiSpace);
- assert_eq!(wrap("Hello", options), vec!["He", "ll", "o"]);
+ assert_eq!(wrap("Hello", 5), vec!["He", "ll", "o"]);
}
#[test]
@@ -1581,14 +1558,14 @@ mod tests {
assert_eq!(
fill(
"1 3 5 7\n1 3 5 7",
- Options::new(7).wrap_algorithm(WrapAlgorithm::FirstFit)
+ 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(WrapAlgorithm::FirstFit)
+ Options::new(5).wrap_algorithm(core::WrapAlgorithm::FirstFit)
),
"1 3 5\n7\n1 3 5\n7"
);
@@ -1631,9 +1608,11 @@ mod tests {
}
#[test]
- fn fill_unicode_boundary() {
- // https://github.com/mgeisler/textwrap/issues/390
- fill("\u{1b}!Ͽ", 10);
+ 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]
@@ -1772,6 +1751,74 @@ mod tests {
}
#[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!["| |"]);
}