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

Allow using trait objects with fill & wrap. #219

Merged
merged 1 commit into from
Nov 17, 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
99 changes: 99 additions & 0 deletions examples/multi-layouts.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
use textwrap::{NoHyphenation, Options};

#[cfg(feature = "hyphenation")]
use hyphenation::{Language, Load, Standard};

// Pretend this is an external crate
mod library {
use textwrap::{Options, WordSplitter};

#[derive(Debug, Default)]
pub struct Layout<'a> {
// Use trait-objects which can be easily converted from any concrete `Options`
styles: Vec<Box<Options<'a, dyn WordSplitter + 'a>>>,
}

impl<'a> Layout<'a> {
pub fn new() -> Self {
Default::default()
}

// Similar signature like `wrap` has, so it takes (nearly) everything that `warp` takes.
pub fn add<S: WordSplitter + 'a, T: Into<Options<'a, S>>>(&mut self, option: T) {
self.styles.push(Box::new(option.into()));
}

pub fn print(&self, text: &str) {
// Now we can easily go over all the arbitrary Options and use them for layouting.
for opt in self.styles.iter() {
println!();

// the debug output of the hyphenation is very long
//println!("Options: {:#?}", opt);

println!("{:0width$}", 0, width = opt.width);

// Just use the textwrap functions as usual.
// However, we have to first coerce it into a trait-object
let dyn_opt: &Options<'a, dyn WordSplitter> = opt;
println!("{}", textwrap::fill(text, dyn_opt));
}
}
}
}

pub fn main() {
// pretend we are a user of the library module above

// Just some options (see below for usage)
let some_opt = Options::new(25).initial_indent("----");

// The struct from the 'library' that we are using
let mut layout = library::Layout::new();

// Add some arbitrary options. We can use here the same as for `fill` & `wrap`.

// Plain Options
layout.add(Options::new(20));

// usize
layout.add(30);

// Customized Options
let opt = Options::new(30);
let opt = opt.subsequent_indent("****");
layout.add(opt.clone()); // notice, here we pass opt by-value instead of by-reference

// We can use boxed splitters too (however, we have to coerce the Options)
let opt: Options = opt.splitter(Box::new(NoHyphenation));
layout.add(opt);

// We can also pass-in references, however, those need to outlive the local
// `layout`, so here, it must be declared before `layout` (drop order).
layout.add(&some_opt);

// If you like, you can download more dictionaries from
// https://github.com/tapeinosyne/hyphenation/tree/master/dictionaries
// Place the dictionaries in the examples/ directory. Here we
// just load the embedded en-us dictionary.
#[cfg(feature = "hyphenation")]
for lang in &[Language::EnglishUS] {
let dictionary = Standard::from_embedded(*lang).or_else(|_| {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples")
.join(format!("{}.standard.bincode", lang.code()));
Standard::from_path(*lang, &path)
});

if let Ok(dict) = dictionary {
layout.add(Options::with_splitter(25, dict));
}
}

let example = "Memory safety without garbage collection. \
Concurrency without data races. \
Zero-cost abstractions.";

// Printout above text in all different layouts
layout.print(example);
}
72 changes: 70 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ pub mod core;

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

impl<'a, S> From<&'a Options<'a, S>> for Options<'a, &'a S> {
impl<'a, S: ?Sized> From<&'a Options<'a, S>> for Options<'a, &'a S> {
fn from(options: &'a Options<'a, S>) -> Self {
Self {
width: options.width,
Expand Down Expand Up @@ -1239,4 +1239,72 @@ mod tests {
fill_inplace(&mut text, 10);
assert_eq!(text, "foo bar \nbaz");
}

#[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"]
);
}
}
2 changes: 1 addition & 1 deletion src/splitting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ impl<S: WordSplitter + ?Sized> WordSplitter for Box<S> {
}
}
*/
impl<T: WordSplitter> WordSplitter for &T {
impl<T: ?Sized + WordSplitter> WordSplitter for &T {
fn split_points(&self, word: &str) -> Vec<usize> {
(*self).split_points(word)
}
Expand Down