Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace WrapOptions with Into<Options> #227

Merged
merged 4 commits into from
Nov 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 15 additions & 12 deletions src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//! advanced wrapping functionality when the `wrap` and `fill`
//! function don't do what you want.

use crate::splitting::WordSplitter;
use unicode_width::UnicodeWidthChar;
use unicode_width::UnicodeWidthStr;

Expand All @@ -28,7 +29,7 @@ fn skip_ansi_escape_sequence<I: Iterator<Item = char>>(ch: char, chars: &mut I)
}
}
}
return false;
false
}

/// A (text) fragment denotes the unit which we wrap into lines.
Expand Down Expand Up @@ -231,27 +232,29 @@ pub fn find_words(line: &str) -> impl Iterator<Item = Word> {
/// // The default splitter is HyphenSplitter:
/// let options = Options::new(80);
/// assert_eq!(
/// split_words(vec![Word::from("foo-bar")], &&options).collect::<Vec<_>>(),
/// split_words(vec![Word::from("foo-bar")], &options).collect::<Vec<_>>(),
/// vec![Word::from("foo-"), Word::from("bar")]
/// );
///
/// // The NoHyphenation splitter ignores the '-':
/// let options = Options::new(80).splitter(NoHyphenation);
/// assert_eq!(
/// split_words(vec![Word::from("foo-bar")], &&options).collect::<Vec<_>>(),
/// split_words(vec![Word::from("foo-bar")], &options).collect::<Vec<_>>(),
/// vec![Word::from("foo-bar")]
/// );
/// ```
pub fn split_words<'a, I, T: crate::WrapOptions>(
pub fn split_words<'a, I, S: WordSplitter, T: Into<crate::Options<'a, S>>>(
words: I,
options: &'a T,
options: T,
) -> impl Iterator<Item = Word<'a>>
where
I: IntoIterator<Item = Word<'a>>,
{
let options = options.into();

words.into_iter().flat_map(move |word| {
let mut prev = 0;
let mut split_points = options.split_points(&word).into_iter();
let mut split_points = options.splitter.split_points(&word).into_iter();
std::iter::from_fn(move || {
if let Some(idx) = split_points.next() {
let need_hyphen = !word[..idx].ends_with('-');
Expand Down Expand Up @@ -509,21 +512,21 @@ mod tests {

#[test]
fn split_words_no_words() {
assert_iter_eq!(split_words(vec![], &80), vec![]);
assert_iter_eq!(split_words(vec![], 80), vec![]);
}

#[test]
fn split_words_empty_word() {
assert_iter_eq!(
split_words(vec![Word::from(" ")], &80),
split_words(vec![Word::from(" ")], 80),
vec![Word::from(" ")]
);
}

#[test]
fn split_words_hyphen_splitter() {
assert_iter_eq!(
split_words(vec![Word::from("foo-bar")], &80),
split_words(vec![Word::from("foo-bar")], 80),
vec![Word::from("foo-"), Word::from("bar")]
);
}
Expand All @@ -533,7 +536,7 @@ mod tests {
// Note that `split_words` does not take the line width into
// account, that is the job of `break_words`.
assert_iter_eq!(
split_words(vec![Word::from("foobar")], &3),
split_words(vec![Word::from("foobar")], 3),
vec![Word::from("foobar")]
);
}
Expand All @@ -550,7 +553,7 @@ mod tests {

let options = Options::new(80).splitter(FixedSplitPoint);
assert_iter_eq!(
split_words(vec![Word::from("foobar")].into_iter(), &&options),
split_words(vec![Word::from("foobar")].into_iter(), &options),
vec![
Word {
word: "foo",
Expand All @@ -568,7 +571,7 @@ mod tests {
);

assert_iter_eq!(
split_words(vec![Word::from("fo-bar")].into_iter(), &&options),
split_words(vec![Word::from("fo-bar")].into_iter(), &options),
vec![
Word {
word: "fo-",
Expand Down
158 changes: 44 additions & 114 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,25 +106,6 @@ pub use crate::splitting::{HyphenSplitter, NoHyphenation, WordSplitter};

pub mod core;

/// Options for wrapping and filling text. Used with the [`wrap`] and
/// [`fill`] functions.
///
/// [`wrap`]: fn.wrap.html
/// [`fill`]: fn.fill.html
pub trait WrapOptions {
/// The width in columns at which the text will be wrapped.
fn width(&self) -> usize;
/// Indentation used for the first line of output.
fn initial_indent(&self) -> &str;
/// Indentation used for subsequent lines of output.
fn subsequent_indent(&self) -> &str;
/// Allow long words to be broken if they cannot fit on a line.
/// When set to `false`, some lines may be longer than `width`.
fn break_words(&self) -> bool;
/// Find indices where `word` can be split.
fn split_points(&self, word: &str) -> Vec<usize>;
}

/// Holds settings for wrapping and filling text.
#[derive(Debug, Clone)]
pub struct Options<'a, S = Box<dyn WordSplitter>> {
Expand All @@ -144,79 +125,27 @@ pub struct Options<'a, S = Box<dyn WordSplitter>> {
pub splitter: S,
}

/// Allows using an `Options` with [`wrap`] and [`fill`]:
///
/// ```
/// use textwrap::{fill, Options};
///
/// let options = Options::new(15).initial_indent("> ");
/// assert_eq!(fill("Wrapping with options!", &options),
/// "> Wrapping with\noptions!");
/// ```
///
/// The integer specifes the wrapping width. This is equivalent to
/// passing `Options::new(15)`.
///
/// [`wrap`]: fn.wrap.html
/// [`fill`]: fn.fill.html
impl<S: WordSplitter> WrapOptions for &Options<'_, S> {
#[inline]
fn width(&self) -> usize {
self.width
}
#[inline]
fn initial_indent(&self) -> &str {
self.initial_indent
}
#[inline]
fn subsequent_indent(&self) -> &str {
self.subsequent_indent
}
#[inline]
fn break_words(&self) -> bool {
self.break_words
}
#[inline]
fn split_points(&self, word: &str) -> Vec<usize> {
self.splitter.split_points(word)
impl<'a, S> From<&'a Options<'a, S>> for Options<'a, &'a S> {
mgeisler marked this conversation as resolved.
Show resolved Hide resolved
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,
splitter: &options.splitter,
}
}
}

/// Allows using an `usize` directly as options for [`wrap`] and
/// [`fill`]:
///
/// ```
/// use textwrap::fill;
///
/// assert_eq!(fill("Quick and easy wrapping!", 15),
/// "Quick and easy\nwrapping!");
/// ```
///
/// The integer specifes the wrapping width. This is equivalent to
/// passing `Options::new(15)`.
///
/// [`wrap`]: fn.wrap.html
/// [`fill`]: fn.fill.html
impl WrapOptions for usize {
#[inline]
fn width(&self) -> usize {
*self
}
#[inline]
fn initial_indent(&self) -> &str {
""
}
#[inline]
fn subsequent_indent(&self) -> &str {
""
}
#[inline]
fn break_words(&self) -> bool {
true
}
#[inline]
fn split_points(&self, word: &str) -> Vec<usize> {
HyphenSplitter.split_points(word)
impl<'a> From<usize> for Options<'a, HyphenSplitter> {
fn from(width: usize) -> Self {
Self {
width,
initial_indent: "",
subsequent_indent: "",
break_words: true,
splitter: HyphenSplitter,
}
}
}

Expand Down Expand Up @@ -431,7 +360,7 @@ impl<'a, S> Options<'a, S> {
initial_indent: "",
subsequent_indent: "",
break_words: true,
splitter,
splitter: splitter,
}
}
}
Expand Down Expand Up @@ -580,7 +509,7 @@ pub fn termwidth() -> usize {
/// ```
///
/// [`wrap`]: fn.wrap.html
pub fn fill<T: WrapOptions>(text: &str, options: T) -> String {
pub fn fill<'a, S: WordSplitter, T: Into<Options<'a, S>>>(text: &str, options: T) -> String {
mgeisler marked this conversation as resolved.
Show resolved Hide resolved
// This will avoid reallocation in simple cases (no
// indentation, no hyphenation).
let mut result = String::with_capacity(text.len());
Expand Down Expand Up @@ -658,21 +587,24 @@ pub fn fill<T: WrapOptions>(text: &str, options: T) -> String {
/// ```
///
/// [`fill`]: fn.fill.html
pub fn wrap<T: WrapOptions>(text: &str, options: T) -> Vec<Cow<'_, str>> {
let initial_width = options
.width()
.saturating_sub(options.initial_indent().width());
pub fn wrap<'a, S: WordSplitter, T: Into<Options<'a, S>>>(
text: &str,
options: T,
) -> Vec<Cow<'_, str>> {
let options = options.into();

let initial_width = options.width.saturating_sub(options.initial_indent.width());
let subsequent_width = options
.width()
.saturating_sub(options.subsequent_indent().width());
.width
.saturating_sub(options.subsequent_indent.width());

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 broken_words = if options.break_words {
let mut broken_words = core::break_words(split_words, subsequent_width);
if !options.initial_indent().is_empty() {
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
Expand Down Expand Up @@ -710,10 +642,10 @@ pub fn wrap<T: WrapOptions>(text: &str, options: T) -> Vec<Cow<'_, str>> {

// 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())
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
Expand Down Expand Up @@ -747,19 +679,16 @@ mod tests {

#[test]
fn options_agree_with_usize() {
let opt_usize: &dyn WrapOptions = &42;
let opt_options: &dyn WrapOptions = &&Options::new(42);
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.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.split_points("hello-world"),
opt_options.split_points("hello-world")
opt_usize.splitter.split_points("hello-world"),
opt_options.splitter.split_points("hello-world")
);
}

Expand Down Expand Up @@ -1157,6 +1086,7 @@ mod tests {
#[test]
fn cloning_works() {
static OPT: Options<HyphenSplitter> = Options::with_splitter(80, HyphenSplitter);
#[allow(clippy::clone_on_copy)]
let opt = OPT.clone();
Kestrer marked this conversation as resolved.
Show resolved Hide resolved
assert_eq!(opt.width, 80);
}
Expand Down
9 changes: 7 additions & 2 deletions src/splitting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ impl<S: WordSplitter + ?Sized> WordSplitter for Box<S> {
}
}
*/
impl<T: WordSplitter> WordSplitter for &T {
mgeisler marked this conversation as resolved.
Show resolved Hide resolved
fn split_points(&self, word: &str) -> Vec<usize> {
(*self).split_points(word)
}
}

/// Use this as a [`Options.splitter`] to avoid any kind of
/// hyphenation:
Expand All @@ -60,7 +65,7 @@ impl<S: WordSplitter + ?Sized> WordSplitter for Box<S> {
/// ```
///
/// [`Options.splitter`]: ../struct.Options.html#structfield.splitter
#[derive(Clone, Debug)]
#[derive(Clone, Copy, Debug)]
pub struct NoHyphenation;

/// `NoHyphenation` implements `WordSplitter` by not splitting the
Expand All @@ -76,7 +81,7 @@ impl WordSplitter for NoHyphenation {
///
/// You probably don't need to use this type since it's already used
/// by default by `Options::new`.
#[derive(Clone, Debug)]
#[derive(Clone, Copy, Debug)]
pub struct HyphenSplitter;

/// `HyphenSplitter` is the default `WordSplitter` used by
Expand Down