Skip to content

Commit

Permalink
Replace the core::WrapAlgorithm enum with a WrapAlgorithm trait
Browse files Browse the repository at this point in the history
This introduces a `textwrap::wrap_algorithms` module where the
`wrap_first_fit` and `wrap_optimal_fit` functions now live.

A new `WrapAlgorithm` trait replaces the old enum. This allows for
arbitrary wrapping algorithms to be plugged into the library. We
should also be able to change the return type from `Vec<&[Word]>` to
`Iterator<Item = Word>` and thus implement streaming wrapping for the
first-fit in the future.
  • Loading branch information
mgeisler committed May 16, 2021
1 parent f056656 commit 99e3d8d
Show file tree
Hide file tree
Showing 13 changed files with 478 additions and 318 deletions.
24 changes: 18 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,25 @@ This file lists the most important changes made in each release of

## Unreleased

This is a major feature release which adds a new generic type
parameter to the `Options` struct. This new parameter lets you specify
how words are found in the text.
This is a major feature release which adds new generic type parameters
to the `Options` struct. These parameters lets you statically configure the wrapping algorithm and the word separator:

* `wrap_algorithms::WrapAlgorithm`: this trait replaces the old
`core::WrapAlgorithm` enum. The enum variants are now two structs:
`wrap_algorithms::FirstFit` and `wrap_algorithms::OptimalFit`.

* `WordSeparator`: this new trait lets you specify how words are
separated in the text. Until now, Textwrap would simply split on
spaces. While this works okay for Western languages, it fails to
take emojis and East-Asian languages into account.

The new `AsciiSpace` and `UnicodeBreakProperties` structs implement
the trait. The latter is available if the new optional
`unicode-linebreak` Cargo feature is enabled.

Common usages of textwrap stays unchanged, but if you previously
spelled out the full type for `Options`, you now need to take th extra
type parameter into account. This means that
spelled out the full type for `Options`, you now need to take the
extra type parameters into account. This means that

```rust
let options: Options<HyphenSplitter> = Options::new(80);
Expand All @@ -20,7 +32,7 @@ let options: Options<HyphenSplitter> = Options::new(80);
need to change to

```rust
let options: Options<AsciiSpace, HyphenSplitter> = Options::new(80);
let options: Options<wrap_algorithms::FirstFit, AsciiSpace, HyphenSplitter> = Options::new(80);
```

You won’t see any chance if you call `wrap` directly with a width or
Expand Down
6 changes: 3 additions & 3 deletions benches/linear.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ pub fn benchmark(c: &mut Criterion) {
#[cfg(feature = "unicode-linebreak")]
{
let options = textwrap::Options::new(LINE_LENGTH)
.wrap_algorithm(textwrap::core::WrapAlgorithm::OptimalFit)
.wrap_algorithm(textwrap::wrap_algorithms::OptimalFit)
.word_separator(textwrap::UnicodeBreakProperties);
group.bench_with_input(
BenchmarkId::new("fill_optimal_fit_unicode", length),
Expand All @@ -43,7 +43,7 @@ pub fn benchmark(c: &mut Criterion) {
}

let options = textwrap::Options::new(LINE_LENGTH)
.wrap_algorithm(textwrap::core::WrapAlgorithm::OptimalFit)
.wrap_algorithm(textwrap::wrap_algorithms::OptimalFit)
.word_separator(textwrap::AsciiSpace);
group.bench_with_input(
BenchmarkId::new("fill_optimal_fit_ascii", length),
Expand All @@ -55,7 +55,7 @@ pub fn benchmark(c: &mut Criterion) {
}

let options = textwrap::Options::new(LINE_LENGTH)
.wrap_algorithm(textwrap::core::WrapAlgorithm::FirstFit)
.wrap_algorithm(textwrap::wrap_algorithms::FirstFit)
.word_separator(textwrap::AsciiSpace);
group.bench_with_input(
BenchmarkId::new("fill_first_fit", length),
Expand Down
22 changes: 15 additions & 7 deletions examples/interactive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ mod unix_only {
use termion::raw::{IntoRawMode, RawTerminal};
use termion::screen::AlternateScreen;
use termion::{color, cursor, style};
#[cfg(feature = "smawk")]
use textwrap::core::WrapAlgorithm::{FirstFit, OptimalFit};
use textwrap::wrap_algorithms;
use textwrap::{wrap, AsciiSpace, Options, WordSeparator};
use textwrap::{HyphenSplitter, NoHyphenation, WordSplitter};

Expand Down Expand Up @@ -57,7 +56,12 @@ mod unix_only {

fn draw_text<'a>(
text: &str,
options: &Options<'a, Box<dyn WordSeparator>, Box<dyn WordSplitter>>,
options: &Options<
'a,
Box<dyn wrap_algorithms::WrapAlgorithm>,
Box<dyn WordSeparator>,
Box<dyn WordSplitter>,
>,
splitter_label: &str,
stdout: &mut RawTerminal<io::Stdout>,
) -> Result<(), io::Error> {
Expand Down Expand Up @@ -229,6 +233,11 @@ mod unix_only {
}

pub fn main() -> Result<(), io::Error> {
let mut wrap_algorithms: Vec<Box<dyn wrap_algorithms::WrapAlgorithm>> =
vec![Box::new(wrap_algorithms::FirstFit)];
#[cfg(feature = "smawk")]
wrap_algorithms.push(Box::new(wrap_algorithms::OptimalFit));

let mut splitters: Vec<Box<dyn WordSplitter>> =
vec![Box::new(HyphenSplitter), Box::new(NoHyphenation)];
let mut splitter_labels: Vec<String> =
Expand All @@ -255,6 +264,7 @@ mod unix_only {

let mut options = Options::new(35)
.break_words(false)
.wrap_algorithm(wrap_algorithms.remove(0))
.splitter(splitters.remove(0))
.word_separator(Box::new(AsciiSpace) as Box<dyn WordSeparator>);
let mut splitter_label = splitter_labels.remove(0);
Expand Down Expand Up @@ -291,10 +301,8 @@ mod unix_only {
Key::Ctrl('b') => options.break_words = !options.break_words,
#[cfg(feature = "smawk")]
Key::Ctrl('o') => {
options.wrap_algorithm = match options.wrap_algorithm {
OptimalFit => FirstFit,
FirstFit => OptimalFit,
}
std::mem::swap(&mut options.wrap_algorithm, &mut wrap_algorithms[0]);
wrap_algorithms.rotate_left(1);
}
Key::Ctrl('s') => {
// We always keep the next splitter at position 0.
Expand Down
4 changes: 2 additions & 2 deletions examples/wasm/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;

use textwrap::{core, WordSeparator};
use textwrap::{core, wrap_algorithms, WordSeparator};

#[wasm_bindgen]
extern "C" {
Expand Down Expand Up @@ -168,7 +168,7 @@ pub fn draw_wrapped_text(
.collect::<Vec<_>>();

let line_lengths = [width * PRECISION];
let wrapped_words = core::wrap_first_fit(&canvas_words, &line_lengths);
let wrapped_words = wrap_algorithms::wrap_first_fit(&canvas_words, &line_lengths);

for words_in_line in wrapped_words {
lineno += 1;
Expand Down
4 changes: 2 additions & 2 deletions fuzz/fuzz_targets/fill_first_fit.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use textwrap::core::WrapAlgorithm::FirstFit;
use textwrap::wrap_algorithms;
use textwrap::Options;

fuzz_target!(|input: (String, usize)| {
let options = Options::new(input.1).wrap_algorithm(FirstFit);
let options = Options::new(input.1).wrap_algorithm(wrap_algorithms::FirstFit);
let _ = textwrap::fill(&input.0, &options);
});
4 changes: 2 additions & 2 deletions fuzz/fuzz_targets/fill_optimal_fit.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use textwrap::core::WrapAlgorithm::OptimalFit;
use textwrap::wrap_algorithms;
use textwrap::Options;

fuzz_target!(|input: (String, usize)| {
let options = Options::new(input.1).wrap_algorithm(OptimalFit);
let options = Options::new(input.1).wrap_algorithm(wrap_algorithms::OptimalFit);
let _ = textwrap::fill(&input.0, &options);
});
Loading

0 comments on commit 99e3d8d

Please sign in to comment.