diff options
Diffstat (limited to 'src/lib.rs')
-rw-r--r-- | src/lib.rs | 763 |
1 files changed, 405 insertions, 358 deletions
@@ -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!["| |"]); } |