Skip to content

Commit

Permalink
Change Wrapper.splitter from T: WordSplitter to Box<dyn WordSplitter>
Browse files Browse the repository at this point in the history
Before, the Wrapper would be generic in the type of WordSplitter used.
This meant that Wrapper<NoHyphenation> would be a different type than
Wrapper<HyphenSplitter>.

The result is that you need to fix the type of WordSplitter at compile
time. This makes it hard to make interactive programs, something which
was raised in #178.

Making splitter a Box<dyn WordSplitter> makes the field less special.
We can therefore simplify the API by removing the with_splitter
constructor.

The performance is unchanged by this, even when testing with the
hyphenation feature enabled.
  • Loading branch information
mgeisler committed Sep 15, 2020
1 parent 089594d commit 6810a97
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 85 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,16 @@ If you enable the `hyphenation` feature, you get support for automatic
hyphenation for [about 70 languages][patterns] via high-quality TeX
hyphenation patterns.

Your program must load the hyphenation pattern and call
`Wrapper::with_splitter` to use it:
Your program must load the hyphenation pattern and configure
`Wrapper::splitter` to use it:

```rust
use hyphenation::{Language, Load, Standard};
use textwrap::Wrapper;

fn main() {
let hyphenator = Standard::from_embedded(Language::EnglishUS).unwrap();
let wrapper = Wrapper::with_splitter(18, hyphenator);
let wrapper = Wrapper::new(18).splitter(Box::new(hyphenator));
let text = "textwrap: a small library for wrapping text.";
println!("{}", wrapper.fill(text))
}
Expand Down
7 changes: 4 additions & 3 deletions benches/linear.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ pub fn benchmark(c: &mut Criterion) {
let mut group = c.benchmark_group("String lengths");
for length in [100, 200, 400, 800, 1600].iter() {
let text = lorem_ipsum(*length);
let mut wrapper = textwrap::Wrapper::new(LINE_LENGTH);
group.bench_with_input(BenchmarkId::new("fill", length), &text, |b, text| {
b.iter(|| textwrap::fill(text, LINE_LENGTH));
b.iter(|| wrapper.fill(text));
});
group.bench_with_input(BenchmarkId::new("wrap", length), &text, |b, text| {
b.iter(|| textwrap::wrap(text, LINE_LENGTH));
b.iter(|| wrapper.wrap(text));
});

#[cfg(feature = "hyphenation")]
Expand All @@ -37,7 +38,7 @@ pub fn benchmark(c: &mut Criterion) {
.join("benches")
.join("la.standard.bincode");
let dictionary = Standard::from_path(Language::Latin, &path).unwrap();
let wrapper = textwrap::Wrapper::with_splitter(LINE_LENGTH, dictionary);
wrapper.splitter = Box::new(dictionary);
group.bench_with_input(BenchmarkId::new("hyphenation", length), &text, |b, text| {
b.iter(|| wrapper.fill(text));
});
Expand Down
2 changes: 1 addition & 1 deletion examples/hyphenation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ fn main() {
fn main() {
let text = "textwrap: a small library for wrapping text.";
let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap();
let wrapper = textwrap::Wrapper::with_splitter(18, dictionary);
let wrapper = textwrap::Wrapper::new(18).splitter(Box::new(dictionary));
println!("{}", wrapper.fill(text));
}
24 changes: 10 additions & 14 deletions examples/layout.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
#[cfg(feature = "hyphenation")]
use hyphenation::{Language, Load};
use textwrap::Wrapper;

#[cfg(not(feature = "hyphenation"))]
fn new_wrapper<'a>() -> Wrapper<'a, textwrap::HyphenSplitter> {
Wrapper::new(0)
}

#[cfg(feature = "hyphenation")]
fn new_wrapper<'a>() -> Wrapper<'a, hyphenation::Standard> {
let dictionary = hyphenation::Standard::from_embedded(Language::EnglishUS).unwrap();
Wrapper::with_splitter(0, dictionary)
}

fn main() {
let example = "Memory safety without garbage collection. \
Concurrency without data races. \
Zero-cost abstractions.";
let mut prev_lines = vec![];
let mut wrapper = new_wrapper();

let mut wrapper = Wrapper::new(0);
#[cfg(feature = "hyphenation")]
{
use hyphenation::Load;
let language = hyphenation::Language::EnglishUS;
let dictionary = hyphenation::Standard::from_embedded(language).unwrap();
wrapper.splitter = Box::new(dictionary);
}

for width in 15..60 {
wrapper.width = width;
let lines = wrapper.wrap(example);
Expand Down
32 changes: 14 additions & 18 deletions examples/termwidth.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
#[cfg(feature = "hyphenation")]
use hyphenation::{Language, Load, Standard};
#[cfg(feature = "terminal_size")]
use textwrap::Wrapper;

Expand All @@ -10,26 +8,24 @@ fn main() {

#[cfg(feature = "terminal_size")]
fn main() {
let example = "Memory safety without garbage collection. \
Concurrency without data races. \
Zero-cost abstractions.";

#[cfg(not(feature = "hyphenation"))]
fn new_wrapper<'a>() -> (&'static str, Wrapper<'a, textwrap::HyphenSplitter>) {
("without hyphenation", Wrapper::with_termwidth())
}
let (msg, wrapper) = ("without hyphenation", Wrapper::with_termwidth());

#[cfg(feature = "hyphenation")]
fn new_wrapper<'a>() -> (&'static str, Wrapper<'a, Standard>) {
let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap();
(
"with hyphenation",
Wrapper::with_splitter(textwrap::termwidth(), dictionary),
)
}
use hyphenation::Load;

#[cfg(feature = "hyphenation")]
let (msg, wrapper) = (
"with hyphenation",
Wrapper::with_termwidth().splitter(Box::new(
hyphenation::Standard::from_embedded(hyphenation::Language::EnglishUS).unwrap(),
)),
);

let example = "Memory safety without garbage collection. \
Concurrency without data races. \
Zero-cost abstractions.";
// Create a new Wrapper -- automatically set the width to the
// current terminal width.
let (msg, wrapper) = new_wrapper();
println!("Formatted {} in {} columns:", msg, wrapper.width);
println!("----");
println!("{}", wrapper.fill(example));
Expand Down
101 changes: 57 additions & 44 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
//! fn main() {
//! let text = "textwrap: a small library for wrapping text.";
//! let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap();
//! let wrapper = Wrapper::with_splitter(18, dictionary);
//! let wrapper = Wrapper::new(18).splitter(Box::new(dictionary));
//! println!("{}", wrapper.fill(text));
//! }
//!
Expand Down Expand Up @@ -129,8 +129,8 @@ pub use crate::splitting::{HyphenSplitter, NoHyphenation, WordSplitter};
/// input string (where each single scan yields a single line) so that
/// the overall time and memory complexity is O(*n*) where *n* is the
/// length of the input string.
#[derive(Clone, Debug)]
pub struct Wrapper<'a, S: WordSplitter> {
#[derive(Debug)]
pub struct Wrapper<'a> {
/// The width in columns at which the text will be wrapped.
pub width: usize,
/// Indentation used for the first line of output.
Expand All @@ -144,19 +144,30 @@ pub struct Wrapper<'a, S: WordSplitter> {
/// The method for splitting words. If the `hyphenation` feature
/// is enabled, you can use a `hyphenation::Standard` dictionary
/// here to get language-aware hyphenation.
pub splitter: S,
pub splitter: Box<dyn WordSplitter>,
}

impl<'a> Wrapper<'a, HyphenSplitter> {
/// # Builder Functionality.
///
/// The methods here make it easy to construct a `Wrapper` using
/// chained method calls. Start with `Wrapper::new` and override
/// settings as you like.
impl<'a> Wrapper<'a> {
/// Create a new Wrapper for wrapping at the specified width. By
/// default, we allow words longer than `width` to be broken. A
/// [`HyphenSplitter`] will be used by default for splitting
/// words. See the [`WordSplitter`] trait for other options.
///
/// [`HyphenSplitter`]: struct.HyphenSplitter.html
/// [`WordSplitter`]: trait.WordSplitter.html
pub fn new(width: usize) -> Wrapper<'a, HyphenSplitter> {
Wrapper::with_splitter(width, HyphenSplitter)
pub fn new(width: usize) -> Wrapper<'a> {
Wrapper {
width: width,
initial_indent: "",
subsequent_indent: "",
break_words: true,
splitter: Box::new(HyphenSplitter),
}
}

/// Create a new Wrapper for wrapping text at the current terminal
Expand All @@ -177,26 +188,9 @@ impl<'a> Wrapper<'a, HyphenSplitter> {
/// **Note:** Only available when the `terminal_size` feature is
/// enabled.
#[cfg(feature = "terminal_size")]
pub fn with_termwidth() -> Wrapper<'a, HyphenSplitter> {
pub fn with_termwidth() -> Wrapper<'a> {
Wrapper::new(termwidth())
}
}

impl<'a, S: WordSplitter> Wrapper<'a, S> {
/// Use the given [`WordSplitter`] to create a new Wrapper for
/// wrapping at the specified width. By default, we allow words
/// longer than `width` to be broken.
///
/// [`WordSplitter`]: trait.WordSplitter.html
pub fn with_splitter(width: usize, splitter: S) -> Wrapper<'a, S> {
Wrapper {
width: width,
initial_indent: "",
subsequent_indent: "",
break_words: true,
splitter: splitter,
}
}

/// Change [`self.initial_indent`]. The initial indentation is
/// used on the very first line of output.
Expand All @@ -214,7 +208,7 @@ impl<'a, S: WordSplitter> Wrapper<'a, S> {
/// ```
///
/// [`self.initial_indent`]: #structfield.initial_indent
pub fn initial_indent(self, indent: &'a str) -> Wrapper<'a, S> {
pub fn initial_indent(self, indent: &'a str) -> Wrapper<'a> {
Wrapper {
initial_indent: indent,
..self
Expand All @@ -239,7 +233,7 @@ impl<'a, S: WordSplitter> Wrapper<'a, S> {
/// ```
///
/// [`self.subsequent_indent`]: #structfield.subsequent_indent
pub fn subsequent_indent(self, indent: &'a str) -> Wrapper<'a, S> {
pub fn subsequent_indent(self, indent: &'a str) -> Wrapper<'a> {
Wrapper {
subsequent_indent: indent,
..self
Expand All @@ -251,13 +245,31 @@ impl<'a, S: WordSplitter> Wrapper<'a, S> {
/// sticking out into the right margin.
///
/// [`self.break_words`]: #structfield.break_words
pub fn break_words(self, setting: bool) -> Wrapper<'a, S> {
pub fn break_words(self, setting: bool) -> Wrapper<'a> {
Wrapper {
break_words: setting,
..self
}
}

/// Change [`self.splitter`]. The [`WordSplitter`] is used to fit
/// part of a word into the current line when wrapping text.
///
/// [`self.splitter`]: #structfield.splitter
/// [`WordSplitter`]: trait.WordSplitter.html
pub fn splitter(self, splitter: Box<dyn WordSplitter>) -> Wrapper<'a> {
Wrapper {
splitter: splitter,
..self
}
}
}

/// # Wrapping Functionality.
///
/// The methods here are the real meat: they take strings as input and
/// give you word-wrapped strings as output.
impl<'a> Wrapper<'a> {
/// Fill a line of text at `self.width` characters.
///
/// The result is a string with newlines between each line. Use
Expand Down Expand Up @@ -422,12 +434,12 @@ impl<'a, S: WordSplitter> Wrapper<'a, S> {

/// An iterator owns a `Wrapper`.
#[derive(Debug)]
struct IntoWrapIter<'a, S: WordSplitter> {
wrapper: Wrapper<'a, S>,
struct IntoWrapIter<'a> {
wrapper: Wrapper<'a>,
inner: WrapIterImpl<'a>,
}

impl<'a, S: WordSplitter> Iterator for IntoWrapIter<'a, S> {
impl<'a> Iterator for IntoWrapIter<'a> {
type Item = Cow<'a, str>;

fn next(&mut self) -> Option<Cow<'a, str>> {
Expand All @@ -437,12 +449,12 @@ impl<'a, S: WordSplitter> Iterator for IntoWrapIter<'a, S> {

/// An iterator which borrows a `Wrapper`.
#[derive(Debug)]
struct WrapIter<'w, 'a: 'w, S: WordSplitter> {
wrapper: &'w Wrapper<'a, S>,
struct WrapIter<'w, 'a: 'w> {
wrapper: &'w Wrapper<'a>,
inner: WrapIterImpl<'a>,
}

impl<'w, 'a: 'w, S: WordSplitter> Iterator for WrapIter<'w, 'a, S> {
impl<'w, 'a: 'w> Iterator for WrapIter<'w, 'a> {
type Item = Cow<'a, str>;

fn next(&mut self) -> Option<Cow<'a, str>> {
Expand Down Expand Up @@ -480,7 +492,7 @@ struct WrapIterImpl<'a> {
}

impl<'a> WrapIterImpl<'a> {
fn new<S: WordSplitter>(wrapper: &Wrapper<'a, S>, s: &'a str) -> WrapIterImpl<'a> {
fn new(wrapper: &Wrapper<'a>, s: &'a str) -> WrapIterImpl<'a> {
WrapIterImpl {
source: s,
char_indices: s.char_indices(),
Expand All @@ -494,15 +506,15 @@ impl<'a> WrapIterImpl<'a> {
}
}

fn create_result_line<S: WordSplitter>(&self, wrapper: &Wrapper<'a, S>) -> Cow<'a, str> {
fn create_result_line(&self, wrapper: &Wrapper<'a>) -> Cow<'a, str> {
if self.start == 0 {
Cow::from(wrapper.initial_indent)
} else {
Cow::from(wrapper.subsequent_indent)
}
}

fn next<S: WordSplitter>(&mut self, wrapper: &Wrapper<'a, S>) -> Option<Cow<'a, str>> {
fn next(&mut self, wrapper: &Wrapper<'a>) -> Option<Cow<'a, str>> {
if self.finished {
return None;
}
Expand Down Expand Up @@ -650,7 +662,8 @@ impl<'a> WrapIterImpl<'a> {
/// use textwrap::{Wrapper, NoHyphenation, termwidth};
///
/// let width = termwidth() - 4; // Two columns on each side.
/// let wrapper = Wrapper::with_splitter(width, NoHyphenation)
/// let wrapper = Wrapper::new(width)
/// .splitter(Box::new(NoHyphenation))
/// .initial_indent(" ")
/// .subsequent_indent(" ");
/// ```
Expand Down Expand Up @@ -932,7 +945,7 @@ mod tests {

#[test]
fn no_hyphenation() {
let wrapper = Wrapper::with_splitter(8, NoHyphenation);
let wrapper = Wrapper::new(8).splitter(Box::new(NoHyphenation));
assert_eq!(wrapper.wrap("foo bar-baz"), vec!["foo", "bar-baz"]);
}

Expand All @@ -946,7 +959,7 @@ mod tests {
vec!["Internatio", "nalization"]
);

let wrapper = Wrapper::with_splitter(10, dictionary);
let wrapper = Wrapper::new(10).splitter(Box::new(dictionary));
assert_eq!(
wrapper.wrap("Internationalization"),
vec!["Interna-", "tionaliza-", "tion"]
Expand All @@ -963,7 +976,7 @@ mod tests {
vec!["participat", "ion is the", "key to", "success"]
);

let wrapper = Wrapper::with_splitter(10, dictionary);
let wrapper = Wrapper::new(10).splitter(Box::new(dictionary));
assert_eq!(
wrapper.wrap("participation is the key to success"),
vec!["participa-", "tion is the", "key to", "success"]
Expand All @@ -976,7 +989,7 @@ mod tests {
// Test that hyphenation takes the width of the wihtespace
// into account.
let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap();
let wrapper = Wrapper::with_splitter(15, dictionary);
let wrapper = Wrapper::new(15).splitter(Box::new(dictionary));
assert_eq!(
wrapper.wrap("garbage collection"),
vec!["garbage col-", "lection"]
Expand All @@ -990,7 +1003,7 @@ mod tests {
// line is borrowed.
use std::borrow::Cow::{Borrowed, Owned};
let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap();
let wrapper = Wrapper::with_splitter(10, dictionary);
let wrapper = Wrapper::new(10).splitter(Box::new(dictionary));
let lines = wrapper.wrap("Internationalization");
if let Borrowed(s) = lines[0] {
assert!(false, "should not have been borrowed: {:?}", s);
Expand All @@ -1010,7 +1023,7 @@ mod tests {
let wrapper = Wrapper::new(8).break_words(false);
assert_eq!(wrapper.wrap("over-caffinated"), vec!["over-", "caffinated"]);

let wrapper = Wrapper::with_splitter(8, dictionary).break_words(false);
let wrapper = wrapper.splitter(Box::new(dictionary));
assert_eq!(
wrapper.wrap("over-caffinated"),
vec!["over-", "caffi-", "nated"]
Expand Down
Loading

0 comments on commit 6810a97

Please sign in to comment.