Snap for 8564071 from 79434c773aa56b8452bf2ebd142df6d722967533 to mainline-wifi-release

Change-Id: Ifc6aa0c4cd59a9307abe0f15e663ebe69142a6b8
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json
index 3237c3c..bf3e802 100644
--- a/.cargo_vcs_info.json
+++ b/.cargo_vcs_info.json
@@ -1,5 +1,6 @@
 {
   "git": {
-    "sha1": "c0a0db1a1460f8923f3cb8d8aa4366ce61237211"
-  }
-}
+    "sha1": "559e07a53bdf7de6bed5c48aacfc0ec8c8bb0c05"
+  },
+  "path_in_vcs": ""
+}
\ No newline at end of file
diff --git a/Android.bp b/Android.bp
index d988603..eb80a6c 100644
--- a/Android.bp
+++ b/Android.bp
@@ -22,10 +22,13 @@
     name: "libtextwrap",
     host_supported: true,
     crate_name: "textwrap",
+    cargo_env_compat: true,
+    cargo_pkg_version: "0.15.0",
     srcs: ["src/lib.rs"],
     edition: "2018",
     apex_available: [
         "//apex_available:platform",
+        "com.android.compos",
         "com.android.virt",
     ],
 }
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3d75e30..093b9dc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,181 @@
 This file lists the most important changes made in each release of
 `textwrap`.
 
+## Version 0.15.0 (2022-02-27)
+
+This is a major feature release with two main changes:
+
+* [#421](https://github.com/mgeisler/textwrap/pull/421): Use `f64`
+  instead of `usize` for fragment widths.
+
+  This fixes problems with overflows in the internal computations of
+  `wrap_optimal_fit` when fragments (words) or line lenghts had
+  extreme values, such as `usize::MAX`.
+
+* [#438](https://github.com/mgeisler/textwrap/pull/438): Simplify
+  `Options` by removing generic type parameters.
+
+  This change removes the new generic parameters introduced in version
+  0.14, as well as the original `WrapSplitter` parameter which has
+  been present since very early versions.
+
+  The result is a simplification of function and struct signatures
+  across the board. So what used to be
+
+  ```rust
+  let options: Options<
+      wrap_algorithms::FirstFit,
+      word_separators::AsciiSpace,
+      word_splitters::HyphenSplitter,
+  > = Options::new(80);
+  ```
+
+  if types are fully written out, is now simply
+
+  ```rust
+  let options: Options<'_> = Options::new(80);
+  ```
+
+  The anonymous lifetime represent the lifetime of the
+  `initial_indent` and `subsequent_indent` strings. The change is
+  nearly performance neutral (a 1-2% regression).
+
+Smaller improvements and changes:
+
+* [#404](https://github.com/mgeisler/textwrap/pull/404): Make
+  documentation for short last-line penalty more precise.
+* [#405](https://github.com/mgeisler/textwrap/pull/405): Cleanup and
+  simplify `Options` docstring.
+* [#411](https://github.com/mgeisler/textwrap/pull/411): Default to
+  `OptimalFit` in interactive example.
+* [#415](https://github.com/mgeisler/textwrap/pull/415): Add demo
+  program to help compute binary sizes.
+* [#423](https://github.com/mgeisler/textwrap/pull/423): Add fuzz
+  tests with fully arbitrary fragments.
+* [#424](https://github.com/mgeisler/textwrap/pull/424): Change
+  `wrap_optimal_fit` penalties to non-negative numbers.
+* [#430](https://github.com/mgeisler/textwrap/pull/430): Add
+  `debug-words` example.
+* [#432](https://github.com/mgeisler/textwrap/pull/432): Use precise
+  dependency versions in Cargo.toml.
+
+## Version 0.14.2 (2021-06-27)
+
+The 0.14.1 release included more changes than intended and has been
+yanked. The change intended for 0.14.1 is now included in 0.14.2.
+
+## Version 0.14.1 (2021-06-26)
+
+This release fixes a panic reported by @Makoto, thanks!
+
+* [#391](https://github.com/mgeisler/textwrap/pull/391): Fix panic in
+  `find_words` due to string access outside of a character boundary.
+
+## Version 0.14.0 (2021-06-05)
+
+This is a major feature release which makes Textwrap more configurable
+and flexible. The high-level API of `textwrap::wrap` and
+`textwrap::fill` remains unchanged, but low-level structs have moved
+around.
+
+The biggest change is the introduction of new generic type parameters
+to the `Options` struct. These parameters lets you statically
+configure the wrapping algorithm, the word separator, and the word
+splitter. If you previously 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);
+```
+
+changes to
+
+```rust
+let options: Options<
+    wrap_algorithms::FirstFit,
+    word_separators::AsciiSpace,
+    word_splitters::HyphenSplitter,
+> = Options::new(80);
+```
+
+This is quite a mouthful, so we suggest using type inferrence where
+possible. You won’t see any chance if you call `wrap` directly with a
+width or with an `Options` value constructed on the fly. Please open
+an issue if this causes problems for you!
+
+### New `WordSeparator` Trait
+
+* [#332](https://github.com/mgeisler/textwrap/pull/332): Add
+  `WordSeparator` trait to allow customizing how words are found in a
+  line of text. Until now, Textwrap would always assume that words are
+  separated by ASCII space characters. You can now customize this as
+  needed.
+
+* [#313](https://github.com/mgeisler/textwrap/pull/313): Add support
+  for using the Unicode line breaking algorithm to find words. This is
+  done by adding a second implementation of the new `WordSeparator`
+  trait. The implementation uses the unicode-linebreak crate, which is
+  a new optional dependency.
+
+  With this, Textwrap can be used with East-Asian languages such as
+  Chinese or Japanese where there are no spaces between words.
+  Breaking a long sequence of emojis is another example where line
+  breaks might be wanted even if there are no whitespace to be found.
+  Feedback would be appreciated for this feature.
+
+
+### Indent
+
+* [#353](https://github.com/mgeisler/textwrap/pull/353): Trim trailing
+  whitespace from `prefix` in `indent`.
+
+  Before, empty lines would get no prefix added. Now, empty lines have
+  a trimmed prefix added. This little trick makes `indent` much more
+  useful since you can now safely indent with `"# "` without creating
+  trailing whitespace in the output due to the trailing whitespace in
+  your prefix.
+
+* [#354](https://github.com/mgeisler/textwrap/pull/354): Make `indent`
+  about 20% faster by preallocating the output string.
+
+
+### Documentation
+
+* [#308](https://github.com/mgeisler/textwrap/pull/308): Document
+  handling of leading and trailing whitespace when wrapping text.
+
+### WebAssembly Demo
+
+* [#310](https://github.com/mgeisler/textwrap/pull/310): Thanks to
+  WebAssembly, you can now try out Textwrap directly in your browser.
+  Please try it out: https://mgeisler.github.io/textwrap/.
+
+### New Generic Parameters
+
+* [#331](https://github.com/mgeisler/textwrap/pull/331): Remove outer
+  boxing from `Options`.
+
+* [#357](https://github.com/mgeisler/textwrap/pull/357): Replace
+  `core::WrapAlgorithm` enum with a `wrap_algorithms::WrapAlgorithm`
+  trait. This allows for arbitrary wrapping algorithms to be plugged
+  into the library.
+
+* [#358](https://github.com/mgeisler/textwrap/pull/358): Switch
+  wrapping functions to use a slice for `line_widths`.
+
+* [#368](https://github.com/mgeisler/textwrap/pull/368): Move
+  `WordSeparator` and `WordSplitter` traits to separate modules.
+  Before, Textwrap had several top-level structs such as
+  `NoHyphenation` and `HyphenSplitter`. These implementations of
+  `WordSplitter` now lives in a dedicated `word_splitters` module.
+  Similarly, we have a new `word_separators` module for
+  implementations of `WordSeparator`.
+
+* [#369](https://github.com/mgeisler/textwrap/pull/369): Rename
+  `Options::splitter` to `Options::word_splitter` for consistency with
+  the other fields backed by traits.
+
 ## Version 0.13.4 (2021-02-23)
 
 This release removes `println!` statements which was left behind in
diff --git a/Cargo.toml b/Cargo.toml
index 61abe49..0e4c788 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,17 +3,16 @@
 # When uploading crates to the registry Cargo will automatically
 # "normalize" Cargo.toml files for maximal compatibility
 # with all versions of Cargo and also rewrite `path` dependencies
-# to registry (e.g., crates.io) dependencies
+# to registry (e.g., crates.io) dependencies.
 #
-# If you believe there's an error in this file please file an
-# issue against the rust-lang/cargo repository. If you're
-# editing this file be aware that the upstream Cargo.toml
-# will likely look very different (and much more reasonable)
+# If you are reading this file be aware that the original Cargo.toml
+# will likely look very different (and much more reasonable).
+# See Cargo.toml.orig for the original contents.
 
 [package]
 edition = "2018"
 name = "textwrap"
-version = "0.13.4"
+version = "0.15.0"
 authors = ["Martin Geisler <martin@geisler.net>"]
 exclude = [".github/", ".gitignore", "benches/", "examples/", "fuzz/", "images/"]
 description = "Powerful library for word wrapping, indenting, and dedenting strings"
@@ -26,39 +25,58 @@
 [package.metadata.docs.rs]
 all-features = true
 
+[[example]]
+name = "hyphenation"
+path = "examples/hyphenation.rs"
+required-features = ["hyphenation"]
+
+[[example]]
+name = "termwidth"
+path = "examples/termwidth.rs"
+required-features = ["terminal_size"]
+
 [[bench]]
 name = "linear"
 path = "benches/linear.rs"
 harness = false
+
+[[bench]]
+name = "indent"
+path = "benches/indent.rs"
+harness = false
 [dependencies.hyphenation]
-version = "0.8"
+version = "0.8.4"
 features = ["embed_en-us"]
 optional = true
 
 [dependencies.smawk]
-version = "0.3"
+version = "0.3.1"
 optional = true
 
 [dependencies.terminal_size]
-version = "0.1"
+version = "0.1.17"
+optional = true
+
+[dependencies.unicode-linebreak]
+version = "0.1.2"
 optional = true
 
 [dependencies.unicode-width]
-version = "0.1"
+version = "0.1.9"
 optional = true
 [dev-dependencies.criterion]
-version = "0.3"
+version = "0.3.5"
 
 [dev-dependencies.lipsum]
-version = "0.7"
+version = "0.8.0"
 
 [dev-dependencies.unic-emoji-char]
 version = "0.9.0"
 
 [dev-dependencies.version-sync]
-version = "0.9"
+version = "0.9.4"
 
 [features]
-default = ["unicode-width", "smawk"]
+default = ["unicode-linebreak", "unicode-width", "smawk"]
 [target."cfg(unix)".dev-dependencies.termion]
-version = "1.5"
+version = "1.5.6"
diff --git a/METADATA b/METADATA
index dfad3bc..8f2131c 100644
--- a/METADATA
+++ b/METADATA
@@ -7,13 +7,13 @@
   }
   url {
     type: ARCHIVE
-    value: "https://static.crates.io/crates/textwrap/textwrap-0.13.4.crate"
+    value: "https://static.crates.io/crates/textwrap/textwrap-0.15.0.crate"
   }
-  version: "0.13.4"
+  version: "0.15.0"
   license_type: NOTICE
   last_upgrade_date {
-    year: 2021
-    month: 4
-    day: 2
+    year: 2022
+    month: 3
+    day: 1
   }
 }
diff --git a/README.md b/README.md
index 39093e0..9eeea07 100644
--- a/README.md
+++ b/README.md
@@ -7,25 +7,28 @@
 
 Textwrap is a library for wrapping and indenting text. It is most
 often used by command-line programs to format dynamic output nicely so
-it looks good in a terminal. However, you can use the library to wrap
-arbitrary things by implementing the `Fragment` trait — an example
-would be wrapping text for PDF files.
+it looks good in a terminal. You can also use Textwrap to wrap text
+set in a proportional font—such as text used to generate PDF files, or
+drawn on a [HTML5 canvas using WebAssembly][wasm-demo].
 
 ## Usage
 
 To use the textwrap crate, add this to your `Cargo.toml` file:
 ```toml
 [dependencies]
-textwrap = "0.13"
+textwrap = "0.15"
 ```
 
 By default, this enables word wrapping with support for Unicode
-strings. Extra features can be enabled with Cargo features — and the
+strings. Extra features can be enabled with Cargo features—and the
 Unicode support can be disabled if needed. This allows you slim down
 the library and so you will only pay for the features you actually
-use. Please see the [_Cargo Features_ in the crate
+use.
+
+Please see the [_Cargo Features_ in the crate
 documentation](https://docs.rs/textwrap/#cargo-features) for a full
-list of the available features.
+list of the available features as well as their impact on the size of
+your binary.
 
 ## Documentation
 
@@ -33,23 +36,22 @@
 
 ## Getting Started
 
-Word wrapping is easy using the `fill` function:
+Word wrapping is easy using the `wrap` and `fill` functions:
 
 ```rust
-fn main() {
-    let text = "textwrap: an efficient and powerful library for wrapping text.";
-    println!("{}", textwrap::fill(text, 28));
+#[cfg(feature = "smawk")] {
+let text = "textwrap: an efficient and powerful library for wrapping text.";
+assert_eq!(
+    textwrap::wrap(text, 28),
+    vec![
+        "textwrap: an efficient",
+        "and powerful library for",
+        "wrapping text.",
+    ]
+);
 }
 ```
 
-The output is wrapped within 28 columns:
-
-```
-textwrap: an efficient
-and powerful library for
-wrapping text.
-```
-
 Sharp-eyed readers will notice that the first line is 22 columns wide.
 So why is the word “and” put in the second line when there is space
 for it in the first line?
@@ -57,45 +59,56 @@
 The explanation is that textwrap does not just wrap text one line at a
 time. Instead, it uses an optimal-fit algorithm which looks ahead and
 chooses line breaks which minimize the gaps left at ends of lines.
+This is controlled with the `smawk` Cargo feature, which is why the
+example is wrapped in the `cfg`-block.
 
 Without look-ahead, the first line would be longer and the text would
 look like this:
 
-```
-textwrap: an efficient and
-powerful library for
-wrapping text.
+```rust
+#[cfg(not(feature = "smawk"))] {
+let text = "textwrap: an efficient and powerful library for wrapping text.";
+assert_eq!(
+    textwrap::wrap(text, 28),
+    vec![
+        "textwrap: an efficient and",
+        "powerful library for",
+        "wrapping text.",
+    ]
+);
+}
 ```
 
 The second line is now shorter and the text is more ragged. The kind
-of wrapping can be configured via `Option::wrap_algorithm`.
+of wrapping can be configured via `Options::wrap_algorithm`.
 
 If you enable the `hyphenation` Cargo 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 configure
-`Options::splitter` to use it:
+`Options::word_splitter` to use it:
 
 ```rust
+#[cfg(feature = "hyphenation")] {
 use hyphenation::{Language, Load, Standard};
-use textwrap::Options;
+use textwrap::{fill, Options, WordSplitter};
 
-fn main() {
-    let hyphenator = Standard::from_embedded(Language::EnglishUS).unwrap();
-    let options = Options::new(28).splitter(hyphenator);
-    let text = "textwrap: an efficient and powerful library for wrapping text.";
-    println!("{}", fill(text, &options);
+let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap();
+let options = textwrap::Options::new(28).word_splitter(WordSplitter::Hyphenation(dictionary));
+let text = "textwrap: an efficient and powerful library for wrapping text.";
+
+assert_eq!(
+    textwrap::wrap(text, &options),
+    vec![
+        "textwrap: an efficient and",
+        "powerful library for wrap-",
+        "ping text."
+    ]
+);
 }
 ```
 
-The output now looks like this:
-```
-textwrap: an efficient and
-powerful library for wrap-
-ping text.
-```
-
 The US-English hyphenation patterns are embedded when you enable the
 `hyphenation` feature. They are licensed under a [permissive
 license][en-us license] and take up about 88 KB in your binary. If you
@@ -112,14 +125,20 @@
 
 The library comes with [a
 collection](https://github.com/mgeisler/textwrap/tree/master/examples)
-of small example programs that shows various features. You’re invited
-to clone the repository and try them out for yourself!
+of small example programs that shows various features.
 
-Of special note is the `interactive` example. This is a demo program
-which demonstrates most of the available features: you can enter text
-and adjust the width at which it is wrapped interactively. You can
-also adjust the `Options` used to see the effect of different
-`WordSplitter`s and wrap algorithms.
+If you want to see Textwrap in action right away, then take a look at
+[`examples/wasm/`], which shows how to wrap sans-serif, serif, and
+monospace text. It uses WebAssembly and is automatically deployed to
+https://mgeisler.github.io/textwrap/.
+
+For the command-line examples, you’re invited to clone the repository
+and try them out for yourself! Of special note is
+[`examples/interactive.rs`]. This is a demo program which demonstrates
+most of the available features: you can enter text and adjust the
+width at which it is wrapped interactively. You can also adjust the
+`Options` used to see the effect of different `WordSplitter`s and wrap
+algorithms.
 
 Run the demo with
 
@@ -142,6 +161,7 @@
 [crates-io]: https://crates.io/crates/textwrap
 [build-status]: https://github.com/mgeisler/textwrap/actions?query=workflow%3Abuild+branch%3Amaster
 [codecov]: https://codecov.io/gh/mgeisler/textwrap
+[wasm-demo]: https://mgeisler.github.io/textwrap/
 [`textwrap-macros` crate]: https://crates.io/crates/textwrap-macros
 [`hyphenation` example]: https://github.com/mgeisler/textwrap/blob/master/examples/hyphenation.rs
 [`termwidth` example]: https://github.com/mgeisler/textwrap/blob/master/examples/termwidth.rs
@@ -149,6 +169,8 @@
 [en-us license]: https://github.com/hyphenation/tex-hyphen/blob/master/hyph-utf8/tex/generic/hyph-utf8/patterns/tex/hyph-en-us.tex
 [bincode]: https://github.com/tapeinosyne/hyphenation/tree/master/dictionaries
 [`hyphenation` documentation]: http://docs.rs/hyphenation
+[`examples/wasm/`]: https://github.com/mgeisler/textwrap/tree/master/examples/wasm
+[`examples/interactive.rs`]: https://github.com/mgeisler/textwrap/tree/master/examples/interactive.rs
 [api-docs]: https://docs.rs/textwrap/
 [CHANGELOG file]: https://github.com/mgeisler/textwrap/blob/master/CHANGELOG.md
 [mit]: LICENSE
diff --git a/TEST_MAPPING b/TEST_MAPPING
index a731acb..07d379e 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -1,14 +1,66 @@
 // Generated by update_crate_tests.py for tests that depend on this crate.
 {
+  "imports": [
+    {
+      "path": "external/rust/crates/base64"
+    },
+    {
+      "path": "external/rust/crates/libsqlite3-sys"
+    },
+    {
+      "path": "external/rust/crates/tinytemplate"
+    },
+    {
+      "path": "external/rust/crates/tinyvec"
+    },
+    {
+      "path": "external/rust/crates/unicode-xid"
+    }
+  ],
   "presubmit": [
     {
+      "name": "ZipFuseTest"
+    },
+    {
+      "name": "apkdmverity.test"
+    },
+    {
+      "name": "authfs_device_test_src_lib"
+    },
+    {
+      "name": "diced_test"
+    },
+    {
       "name": "keystore2_test"
     },
     {
-      "name": "libsqlite3-sys_device_test_src_lib"
+      "name": "keystore2_vintf_test"
     },
     {
-      "name": "vpnprofilestore_test"
+      "name": "legacykeystore_test"
+    }
+  ],
+  "presubmit-rust": [
+    {
+      "name": "ZipFuseTest"
+    },
+    {
+      "name": "apkdmverity.test"
+    },
+    {
+      "name": "authfs_device_test_src_lib"
+    },
+    {
+      "name": "diced_test"
+    },
+    {
+      "name": "keystore2_test"
+    },
+    {
+      "name": "keystore2_vintf_test"
+    },
+    {
+      "name": "legacykeystore_test"
     }
   ]
 }
diff --git a/cargo2android.json b/cargo2android.json
index b933b33..a78ac00 100644
--- a/cargo2android.json
+++ b/cargo2android.json
@@ -1,6 +1,7 @@
 {
   "apex-available": [
     "//apex_available:platform",
+    "com.android.compos",
     "com.android.virt"
   ],
   "dependencies": true,
diff --git a/rustfmt.toml b/rustfmt.toml
new file mode 100644
index 0000000..c1578aa
--- /dev/null
+++ b/rustfmt.toml
@@ -0,0 +1 @@
+imports_granularity = "Module"
diff --git a/src/core.rs b/src/core.rs
index b6f5b46..0ab4ef8 100644
--- a/src/core.rs
+++ b/src/core.rs
@@ -8,20 +8,24 @@
 //! something:
 //!
 //! 1. Split your input into [`Fragment`]s. These are abstract blocks
-//!    of text or content which can be wrapped into lines. You can use
-//!    [`find_words`] to do this for text.
+//!    of text or content which can be wrapped into lines. See
+//!    [`WordSeparator`](crate::word_separators::WordSeparator) for
+//!    how to do this for text.
 //!
 //! 2. Potentially split your fragments into smaller pieces. This
-//!    allows you to implement things like hyphenation. If wrapping
-//!    text, [`split_words`] can help you do this.
+//!    allows you to implement things like hyphenation. If you use the
+//!    `Word` type, you can use [`WordSplitter`](crate::WordSplitter)
+//!    enum for this.
 //!
 //! 3. Potentially break apart fragments that are still too large to
 //!    fit on a single line. This is implemented in [`break_words`].
 //!
 //! 4. Finally take your fragments and put them into lines. There are
-//!    two algorithms for this: [`wrap_optimal_fit`] and
-//!    [`wrap_first_fit`]. The former produces better line breaks, the
-//!    latter is faster.
+//!    two algorithms for this in the
+//!    [`wrap_algorithms`](crate::wrap_algorithms) module:
+//!    [`wrap_optimal_fit`](crate::wrap_algorithms::wrap_optimal_fit)
+//!    and [`wrap_first_fit`](crate::wrap_algorithms::wrap_first_fit).
+//!    The former produces better line breaks, the latter is faster.
 //!
 //! 5. Iterate through the slices returned by the wrapping functions
 //!    and construct your lines of output.
@@ -30,13 +34,6 @@
 //! the functionality here is not sufficient or if you have ideas for
 //! improving it. We would love to hear from you!
 
-use crate::{Options, WordSplitter};
-
-#[cfg(feature = "smawk")]
-mod optimal_fit;
-#[cfg(feature = "smawk")]
-pub use optimal_fit::wrap_optimal_fit;
-
 /// The CSI or “Control Sequence Introducer” introduces an ANSI escape
 /// sequence. This is typically used for colored text and will be
 /// ignored when computing the text width.
@@ -48,7 +45,7 @@
 /// `chars` provide the following characters. The `chars` will be
 /// modified if `ch` is the start of an ANSI escape sequence.
 #[inline]
-fn skip_ansi_escape_sequence<I: Iterator<Item = char>>(ch: char, chars: &mut I) -> bool {
+pub(crate) fn skip_ansi_escape_sequence<I: Iterator<Item = char>>(ch: char, chars: &mut I) -> bool {
     if ch == CSI.0 && chars.next() == Some(CSI.1) {
         // We have found the start of an ANSI escape code, typically
         // used for colored terminal text. We skip until we find a
@@ -175,7 +172,6 @@
 /// [Unicode equivalence]: https://en.wikipedia.org/wiki/Unicode_equivalence
 /// [CJK characters]: https://en.wikipedia.org/wiki/CJK_characters
 /// [emoji modifier sequences]: https://unicode.org/emoji/charts/full-emoji-modifiers.html
-#[inline]
 pub fn display_width(text: &str) -> usize {
     let mut chars = text.chars();
     let mut width = 0;
@@ -200,15 +196,15 @@
 /// the displayed width of each part, which this trait provides.
 pub trait Fragment: std::fmt::Debug {
     /// Displayed width of word represented by this fragment.
-    fn width(&self) -> usize;
+    fn width(&self) -> f64;
 
     /// Displayed width of the whitespace that must follow the word
     /// when the word is not at the end of a line.
-    fn whitespace_width(&self) -> usize;
+    fn whitespace_width(&self) -> f64;
 
     /// Displayed width of the penalty that must be inserted if the
     /// word falls at the end of a line.
-    fn penalty_width(&self) -> usize;
+    fn penalty_width(&self) -> f64;
 }
 
 /// A piece of wrappable text, including any trailing whitespace.
@@ -217,10 +213,14 @@
 /// trailing whitespace, and potentially a penalty item.
 #[derive(Debug, Copy, Clone, PartialEq, Eq)]
 pub struct Word<'a> {
-    word: &'a str,
-    width: usize,
-    pub(crate) whitespace: &'a str,
-    pub(crate) penalty: &'a str,
+    /// Word content.
+    pub word: &'a str,
+    /// Whitespace to insert if the word does not fall at the end of a line.
+    pub whitespace: &'a str,
+    /// Penalty string to insert if the word falls at the end of a line.
+    pub penalty: &'a str,
+    // Cached width in columns.
+    pub(crate) width: usize,
 }
 
 impl std::ops::Deref for Word<'_> {
@@ -232,7 +232,7 @@
 }
 
 impl<'a> Word<'a> {
-    /// Construct a new `Word`.
+    /// Construct a `Word` from a string.
     ///
     /// A trailing stretch of `' '` is automatically taken to be the
     /// whitespace part of the word.
@@ -240,7 +240,7 @@
         let trimmed = word.trim_end_matches(' ');
         Word {
             word: trimmed,
-            width: display_width(&trimmed),
+            width: display_width(trimmed),
             whitespace: &word[trimmed.len()..],
             penalty: "",
         }
@@ -303,137 +303,25 @@
 
 impl Fragment for Word<'_> {
     #[inline]
-    fn width(&self) -> usize {
-        self.width
+    fn width(&self) -> f64 {
+        self.width as f64
     }
 
     // We assume the whitespace consist of ' ' only. This allows us to
     // compute the display width in constant time.
     #[inline]
-    fn whitespace_width(&self) -> usize {
-        self.whitespace.len()
+    fn whitespace_width(&self) -> f64 {
+        self.whitespace.len() as f64
     }
 
     // We assume the penalty is `""` or `"-"`. This allows us to
     // compute the display width in constant time.
     #[inline]
-    fn penalty_width(&self) -> usize {
-        self.penalty.len()
+    fn penalty_width(&self) -> f64 {
+        self.penalty.len() as f64
     }
 }
 
-/// Split line into words separated by regions of `' '` characters.
-///
-/// # Examples
-///
-/// ```
-/// use textwrap::core::{find_words, Fragment, Word};
-/// let words = find_words("Hello World!").collect::<Vec<_>>();
-/// assert_eq!(words, vec![Word::from("Hello "), Word::from("World!")]);
-/// assert_eq!(words[0].width(), 5);
-/// assert_eq!(words[0].whitespace_width(), 1);
-/// assert_eq!(words[0].penalty_width(), 0);
-/// ```
-pub fn find_words(line: &str) -> impl Iterator<Item = Word> {
-    let mut start = 0;
-    let mut in_whitespace = false;
-    let mut char_indices = line.char_indices();
-
-    std::iter::from_fn(move || {
-        // for (idx, ch) in char_indices does not work, gives this
-        // error:
-        //
-        // > cannot move out of `char_indices`, a captured variable in
-        // > an `FnMut` closure
-        #[allow(clippy::while_let_on_iterator)]
-        while let Some((idx, ch)) = char_indices.next() {
-            if in_whitespace && ch != ' ' {
-                let word = Word::from(&line[start..idx]);
-                start = idx;
-                in_whitespace = ch == ' ';
-                return Some(word);
-            }
-
-            in_whitespace = ch == ' ';
-        }
-
-        if start < line.len() {
-            let word = Word::from(&line[start..]);
-            start = line.len();
-            return Some(word);
-        }
-
-        None
-    })
-}
-
-/// Split words into smaller words according to the split points given
-/// by `options`.
-///
-/// Note that we split all words, regardless of their length. This is
-/// to more cleanly separate the business of splitting (including
-/// automatic hyphenation) from the business of word wrapping.
-///
-/// # Examples
-///
-/// ```
-/// use textwrap::core::{split_words, Word};
-/// use textwrap::{NoHyphenation, Options};
-///
-/// // The default splitter is HyphenSplitter:
-/// let options = Options::new(80);
-/// assert_eq!(
-///     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<_>>(),
-///     vec![Word::from("foo-bar")]
-/// );
-/// ```
-pub fn split_words<'a, I, S, Opt>(words: I, options: Opt) -> impl Iterator<Item = Word<'a>>
-where
-    I: IntoIterator<Item = Word<'a>>,
-    S: WordSplitter,
-    Opt: Into<Options<'a, S>>,
-{
-    let options = options.into();
-
-    words.into_iter().flat_map(move |word| {
-        let mut prev = 0;
-        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('-');
-                let w = Word {
-                    word: &word.word[prev..idx],
-                    width: display_width(&word[prev..idx]),
-                    whitespace: "",
-                    penalty: if need_hyphen { "-" } else { "" },
-                };
-                prev = idx;
-                return Some(w);
-            }
-
-            if prev < word.word.len() || prev == 0 {
-                let w = Word {
-                    word: &word.word[prev..],
-                    width: display_width(&word[prev..]),
-                    whitespace: word.whitespace,
-                    penalty: word.penalty,
-                };
-                prev = word.word.len() + 1;
-                return Some(w);
-            }
-
-            None
-        })
-    })
-}
-
 /// Forcibly break words wider than `line_width` into smaller words.
 ///
 /// This simply calls [`Word::break_apart`] on words that are too
@@ -445,7 +333,7 @@
 {
     let mut shortened_words = Vec::new();
     for word in words {
-        if word.width() > line_width {
+        if word.width() > line_width as f64 {
             shortened_words.extend(word.break_apart(line_width));
         } else {
             shortened_words.push(word);
@@ -454,200 +342,6 @@
     shortened_words
 }
 
-/// Wrapping algorithms.
-///
-/// After a text has been broken into [`Fragment`]s, the one now has
-/// to decide how to break the fragments into lines. The simplest
-/// algorithm for this is implemented by [`wrap_first_fit`]: it uses
-/// no look-ahead and simply adds fragments to the line as long as
-/// they fit. However, this can lead to poor line breaks if a large
-/// fragment almost-but-not-quite fits on a line. When that happens,
-/// the fragment is moved to the next line and it will leave behind a
-/// large gap. A more advanced algorithm, implemented by
-/// [`wrap_optimal_fit`], will take this into account. The optimal-fit
-/// algorithm considers all possible line breaks and will attempt to
-/// minimize the gaps left behind by overly short lines.
-///
-/// While both algorithms run in linear time, the first-fit algorithm
-/// is about 4 times faster than the optimal-fit algorithm.
-#[derive(Debug, Copy, Clone, Eq, PartialEq)]
-pub enum WrapAlgorithm {
-    /// Use an advanced algorithm which considers the entire paragraph
-    /// to find optimal line breaks. Implemented by
-    /// [`wrap_optimal_fit`].
-    ///
-    /// **Note:** Only available when the `smawk` Cargo feature is
-    /// enabled.
-    #[cfg(feature = "smawk")]
-    OptimalFit,
-    /// Use a fast and simple algorithm with no look-ahead to find
-    /// line breaks. Implemented by [`wrap_first_fit`].
-    FirstFit,
-}
-
-/// Wrap abstract fragments into lines with a first-fit algorithm.
-///
-/// The `line_widths` map line numbers (starting from 0) to a target
-/// line width. This can be used to implement hanging indentation.
-///
-/// The fragments must already have been split into the desired
-/// widths, this function will not (and cannot) attempt to split them
-/// further when arranging them into lines.
-///
-/// # First-Fit Algorithm
-///
-/// This implements a simple “greedy” algorithm: accumulate fragments
-/// one by one and when a fragment no longer fits, start a new line.
-/// There is no look-ahead, we simply take first fit of the fragments
-/// we find.
-///
-/// While fast and predictable, this algorithm can produce poor line
-/// breaks when a long fragment is moved to a new line, leaving behind
-/// a large gap:
-///
-/// ```
-/// use textwrap::core::{find_words, wrap_first_fit, Word};
-///
-/// // Helper to convert wrapped lines to a Vec<String>.
-/// fn lines_to_strings(lines: Vec<&[Word<'_>]>) -> Vec<String> {
-///     lines.iter().map(|line| {
-///         line.iter().map(|word| &**word).collect::<Vec<_>>().join(" ")
-///     }).collect::<Vec<_>>()
-/// }
-///
-/// let text = "These few words will unfortunately not wrap nicely.";
-/// let words = find_words(text).collect::<Vec<_>>();
-/// assert_eq!(lines_to_strings(wrap_first_fit(&words, |_| 15)),
-///            vec!["These few words",
-///                 "will",  // <-- short line
-///                 "unfortunately",
-///                 "not wrap",
-///                 "nicely."]);
-///
-/// // We can avoid the short line if we look ahead:
-/// #[cfg(feature = "smawk")]
-/// assert_eq!(lines_to_strings(textwrap::core::wrap_optimal_fit(&words, |_| 15)),
-///            vec!["These few",
-///                 "words will",
-///                 "unfortunately",
-///                 "not wrap",
-///                 "nicely."]);
-/// ```
-///
-/// The [`wrap_optimal_fit`] function was used above to get better
-/// line breaks. It uses an advanced algorithm which tries to avoid
-/// short lines. This function is about 4 times faster than
-/// [`wrap_optimal_fit`].
-///
-/// # Examples
-///
-/// Imagine you're building a house site and you have a number of
-/// tasks you need to execute. Things like pour foundation, complete
-/// framing, install plumbing, electric cabling, install insulation.
-///
-/// The construction workers can only work during daytime, so they
-/// need to pack up everything at night. Because they need to secure
-/// their tools and move machines back to the garage, this process
-/// takes much more time than the time it would take them to simply
-/// switch to another task.
-///
-/// You would like to make a list of tasks to execute every day based
-/// on your estimates. You can model this with a program like this:
-///
-/// ```
-/// use textwrap::core::{wrap_first_fit, Fragment};
-///
-/// #[derive(Debug)]
-/// struct Task<'a> {
-///     name: &'a str,
-///     hours: usize,   // Time needed to complete task.
-///     sweep: usize,   // Time needed for a quick sweep after task during the day.
-///     cleanup: usize, // Time needed for full cleanup if day ends with this task.
-/// }
-///
-/// impl Fragment for Task<'_> {
-///     fn width(&self) -> usize { self.hours }
-///     fn whitespace_width(&self) -> usize { self.sweep }
-///     fn penalty_width(&self) -> usize { self.cleanup }
-/// }
-///
-/// // The morning tasks
-/// let tasks = vec![
-///     Task { name: "Foundation",  hours: 4, sweep: 2, cleanup: 3 },
-///     Task { name: "Framing",     hours: 3, sweep: 1, cleanup: 2 },
-///     Task { name: "Plumbing",    hours: 2, sweep: 2, cleanup: 2 },
-///     Task { name: "Electrical",  hours: 2, sweep: 1, cleanup: 2 },
-///     Task { name: "Insulation",  hours: 2, sweep: 1, cleanup: 2 },
-///     Task { name: "Drywall",     hours: 3, sweep: 1, cleanup: 2 },
-///     Task { name: "Floors",      hours: 3, sweep: 1, cleanup: 2 },
-///     Task { name: "Countertops", hours: 1, sweep: 1, cleanup: 2 },
-///     Task { name: "Bathrooms",   hours: 2, sweep: 1, cleanup: 2 },
-/// ];
-///
-/// // Fill tasks into days, taking `day_length` into account. The
-/// // output shows the hours worked per day along with the names of
-/// // the tasks for that day.
-/// fn assign_days<'a>(tasks: &[Task<'a>], day_length: usize) -> Vec<(usize, Vec<&'a str>)> {
-///     let mut days = Vec::new();
-///     // Assign tasks to days. The assignment is a vector of slices,
-///     // with a slice per day.
-///     let assigned_days: Vec<&[Task<'a>]> = wrap_first_fit(&tasks, |i| day_length);
-///     for day in assigned_days.iter() {
-///         let last = day.last().unwrap();
-///         let work_hours: usize = day.iter().map(|t| t.hours + t.sweep).sum();
-///         let names = day.iter().map(|t| t.name).collect::<Vec<_>>();
-///         days.push((work_hours - last.sweep + last.cleanup, names));
-///     }
-///     days
-/// }
-///
-/// // With a single crew working 8 hours a day:
-/// assert_eq!(
-///     assign_days(&tasks, 8),
-///     [
-///         (7, vec!["Foundation"]),
-///         (8, vec!["Framing", "Plumbing"]),
-///         (7, vec!["Electrical", "Insulation"]),
-///         (5, vec!["Drywall"]),
-///         (7, vec!["Floors", "Countertops"]),
-///         (4, vec!["Bathrooms"]),
-///     ]
-/// );
-///
-/// // With two crews working in shifts, 16 hours a day:
-/// assert_eq!(
-///     assign_days(&tasks, 16),
-///     [
-///         (14, vec!["Foundation", "Framing", "Plumbing"]),
-///         (15, vec!["Electrical", "Insulation", "Drywall", "Floors"]),
-///         (6, vec!["Countertops", "Bathrooms"]),
-///     ]
-/// );
-/// ```
-///
-/// Apologies to anyone who actually knows how to build a house and
-/// knows how long each step takes :-)
-pub fn wrap_first_fit<T: Fragment, F: Fn(usize) -> usize>(
-    fragments: &[T],
-    line_widths: F,
-) -> Vec<&[T]> {
-    let mut lines = Vec::new();
-    let mut start = 0;
-    let mut width = 0;
-
-    for (idx, fragment) in fragments.iter().enumerate() {
-        let line_width = line_widths(lines.len());
-        if width + fragment.width() + fragment.penalty_width() > line_width && idx > start {
-            lines.push(&fragments[start..idx]);
-            start = idx;
-            width = 0;
-        }
-        width += fragment.width() + fragment.whitespace_width();
-    }
-    lines.push(&fragments[start..]);
-    lines
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -655,13 +349,6 @@
     #[cfg(feature = "unicode-width")]
     use unicode_width::UnicodeWidthChar;
 
-    // Like assert_eq!, but the left expression is an iterator.
-    macro_rules! assert_iter_eq {
-        ($left:expr, $right:expr) => {
-            assert_eq!($left.collect::<Vec<_>>(), $right);
-        };
-    }
-
     #[test]
     fn skip_ansi_escape_sequence_works() {
         let blue_text = "\u{1b}[34mHello\u{1b}[0m";
@@ -743,160 +430,4 @@
     fn display_width_emojis() {
         assert_eq!(display_width("😂😭🥺🤣✨😍🙏🥰😊🔥"), 20);
     }
-
-    #[test]
-    fn find_words_empty() {
-        assert_iter_eq!(find_words(""), vec![]);
-    }
-
-    #[test]
-    fn find_words_single_word() {
-        assert_iter_eq!(find_words("foo"), vec![Word::from("foo")]);
-    }
-
-    #[test]
-    fn find_words_two_words() {
-        assert_iter_eq!(
-            find_words("foo bar"),
-            vec![Word::from("foo "), Word::from("bar")]
-        );
-    }
-
-    #[test]
-    fn find_words_multiple_words() {
-        assert_iter_eq!(
-            find_words("foo bar baz"),
-            vec![Word::from("foo "), Word::from("bar "), Word::from("baz")]
-        );
-    }
-
-    #[test]
-    fn find_words_whitespace() {
-        assert_iter_eq!(find_words("    "), vec![Word::from("    ")]);
-    }
-
-    #[test]
-    fn find_words_inter_word_whitespace() {
-        assert_iter_eq!(
-            find_words("foo   bar"),
-            vec![Word::from("foo   "), Word::from("bar")]
-        )
-    }
-
-    #[test]
-    fn find_words_trailing_whitespace() {
-        assert_iter_eq!(find_words("foo   "), vec![Word::from("foo   ")]);
-    }
-
-    #[test]
-    fn find_words_leading_whitespace() {
-        assert_iter_eq!(
-            find_words("   foo"),
-            vec![Word::from("   "), Word::from("foo")]
-        );
-    }
-
-    #[test]
-    fn find_words_multi_column_char() {
-        assert_iter_eq!(
-            find_words("\u{1f920}"), // cowboy emoji 🤠
-            vec![Word::from("\u{1f920}")]
-        );
-    }
-
-    #[test]
-    fn find_words_hyphens() {
-        assert_iter_eq!(find_words("foo-bar"), vec![Word::from("foo-bar")]);
-        assert_iter_eq!(
-            find_words("foo- bar"),
-            vec![Word::from("foo- "), Word::from("bar")]
-        );
-        assert_iter_eq!(
-            find_words("foo - bar"),
-            vec![Word::from("foo "), Word::from("- "), Word::from("bar")]
-        );
-        assert_iter_eq!(
-            find_words("foo -bar"),
-            vec![Word::from("foo "), Word::from("-bar")]
-        );
-    }
-
-    #[test]
-    fn split_words_no_words() {
-        assert_iter_eq!(split_words(vec![], 80), vec![]);
-    }
-
-    #[test]
-    fn split_words_empty_word() {
-        assert_iter_eq!(
-            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),
-            vec![Word::from("foo-"), Word::from("bar")]
-        );
-    }
-
-    #[test]
-    fn split_words_short_line() {
-        // 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),
-            vec![Word::from("foobar")]
-        );
-    }
-
-    #[test]
-    fn split_words_adds_penalty() {
-        #[derive(Debug)]
-        struct FixedSplitPoint;
-        impl WordSplitter for FixedSplitPoint {
-            fn split_points(&self, _: &str) -> Vec<usize> {
-                vec![3]
-            }
-        }
-
-        let options = Options::new(80).splitter(FixedSplitPoint);
-        assert_iter_eq!(
-            split_words(vec![Word::from("foobar")].into_iter(), &options),
-            vec![
-                Word {
-                    word: "foo",
-                    width: 3,
-                    whitespace: "",
-                    penalty: "-"
-                },
-                Word {
-                    word: "bar",
-                    width: 3,
-                    whitespace: "",
-                    penalty: ""
-                }
-            ]
-        );
-
-        assert_iter_eq!(
-            split_words(vec![Word::from("fo-bar")].into_iter(), &options),
-            vec![
-                Word {
-                    word: "fo-",
-                    width: 3,
-                    whitespace: "",
-                    penalty: ""
-                },
-                Word {
-                    word: "bar",
-                    width: 3,
-                    whitespace: "",
-                    penalty: ""
-                }
-            ]
-        );
-    }
 }
diff --git a/src/core/optimal_fit.rs b/src/core/optimal_fit.rs
deleted file mode 100644
index c18b974..0000000
--- a/src/core/optimal_fit.rs
+++ /dev/null
@@ -1,228 +0,0 @@
-use crate::core::Fragment;
-use std::cell::RefCell;
-
-/// Cache for line numbers. This is necessary to avoid a O(n**2)
-/// behavior when computing line numbers in [`wrap_optimal_fit`].
-struct LineNumbers {
-    line_numbers: RefCell<Vec<usize>>,
-}
-
-impl LineNumbers {
-    fn new(size: usize) -> Self {
-        let mut line_numbers = Vec::with_capacity(size);
-        line_numbers.push(0);
-        LineNumbers {
-            line_numbers: RefCell::new(line_numbers),
-        }
-    }
-
-    fn get<T>(&self, i: usize, minima: &[(usize, T)]) -> usize {
-        while self.line_numbers.borrow_mut().len() < i + 1 {
-            let pos = self.line_numbers.borrow().len();
-            let line_number = 1 + self.get(minima[pos].0, &minima);
-            self.line_numbers.borrow_mut().push(line_number);
-        }
-
-        self.line_numbers.borrow()[i]
-    }
-}
-
-/// Per-line penalty. This is added for every line, which makes it
-/// expensive to output more lines than the minimum required.
-const NLINE_PENALTY: i32 = 1000;
-
-/// Per-character cost for lines that overflow the target line width.
-///
-/// With a value of 50², every single character costs as much as
-/// leaving a gap of 50 characters behind. This is becuase we assign
-/// as cost of `gap * gap` to a short line. This means that we can
-/// overflow the line by 1 character in extreme cases:
-///
-/// ```
-/// use textwrap::core::{wrap_optimal_fit, Word};
-///
-/// let short = "foo ";
-/// let long = "x".repeat(50);
-/// let fragments = vec![Word::from(short), Word::from(&long)];
-///
-/// // Perfect fit, both words are on a single line with no overflow.
-/// let wrapped = wrap_optimal_fit(&fragments, |_| short.len() + long.len());
-/// assert_eq!(wrapped, vec![&[Word::from(short), Word::from(&long)]]);
-///
-/// // The words no longer fit, yet we get a single line back. While
-/// // the cost of overflow (`1 * 2500`) is the same as the cost of the
-/// // gap (`50 * 50 = 2500`), the tie is broken by `NLINE_PENALTY`
-/// // which makes it cheaper to overflow than to use two lines.
-/// let wrapped = wrap_optimal_fit(&fragments, |_| short.len() + long.len() - 1);
-/// assert_eq!(wrapped, vec![&[Word::from(short), Word::from(&long)]]);
-///
-/// // The cost of overflow would be 2 * 2500, whereas the cost of the
-/// // gap is only `49 * 49 + NLINE_PENALTY = 2401 + 1000 = 3401`. We
-/// // therefore get two lines.
-/// let wrapped = wrap_optimal_fit(&fragments, |_| short.len() + long.len() - 2);
-/// assert_eq!(wrapped, vec![&[Word::from(short)],
-///                          &[Word::from(&long)]]);
-/// ```
-///
-/// This only happens if the overflowing word is 50 characters long
-/// _and_ if it happens to overflow the line by exactly one character.
-/// If it overflows by more than one character, the overflow penalty
-/// will quickly outgrow the cost of the gap, as seen above.
-const OVERFLOW_PENALTY: i32 = 50 * 50;
-
-/// The last line is short if it is less than 1/4 of the target width.
-const SHORT_LINE_FRACTION: usize = 4;
-
-/// Penalize a short last line.
-const SHORT_LAST_LINE_PENALTY: i32 = 25;
-
-/// Penalty for lines ending with a hyphen.
-const HYPHEN_PENALTY: i32 = 25;
-
-/// Wrap abstract fragments into lines with an optimal-fit algorithm.
-///
-/// The `line_widths` map line numbers (starting from 0) to a target
-/// line width. This can be used to implement hanging indentation.
-///
-/// The fragments must already have been split into the desired
-/// widths, this function will not (and cannot) attempt to split them
-/// further when arranging them into lines.
-///
-/// # Optimal-Fit Algorithm
-///
-/// The algorithm considers all possible break points and picks the
-/// breaks which minimizes the gaps at the end of each line. More
-/// precisely, the algorithm assigns a cost or penalty to each break
-/// point, determined by `cost = gap * gap` where `gap = target_width -
-/// line_width`. Shorter lines are thus penalized more heavily since
-/// they leave behind a larger gap.
-///
-/// We can illustrate this with the text “To be, or not to be: that is
-/// the question”. We will be wrapping it in a narrow column with room
-/// for only 10 characters. The [greedy
-/// algorithm](super::wrap_first_fit) will produce these lines, each
-/// annotated with the corresponding penalty:
-///
-/// ```text
-/// "To be, or"   1² =  1
-/// "not to be:"  0² =  0
-/// "that is"     3² =  9
-/// "the"         7² = 49
-/// "question"    2² =  4
-/// ```
-///
-/// We see that line four with “the” leaves a gap of 7 columns, which
-/// gives it a penalty of 49. The sum of the penalties is 63.
-///
-/// There are 10 words, which means that there are `2_u32.pow(9)` or
-/// 512 different ways to typeset it. We can compute
-/// the sum of the penalties for each possible line break and search
-/// for the one with the lowest sum:
-///
-/// ```text
-/// "To be,"     4² = 16
-/// "or not to"  1² =  1
-/// "be: that"   2² =  4
-/// "is the"     4² = 16
-/// "question"   2² =  4
-/// ```
-///
-/// The sum of the penalties is 41, which is better than what the
-/// greedy algorithm produced.
-///
-/// Searching through all possible combinations would normally be
-/// prohibitively slow. However, it turns out that the problem can be
-/// formulated as the task of finding column minima in a cost matrix.
-/// This matrix has a special form (totally monotone) which lets us
-/// use a [linear-time algorithm called
-/// SMAWK](https://lib.rs/crates/smawk) to find the optimal break
-/// points.
-///
-/// This means that the time complexity remains O(_n_) where _n_ is
-/// the number of words. Compared to
-/// [`wrap_first_fit`](super::wrap_first_fit), this function is about
-/// 4 times slower.
-///
-/// The optimization of per-line costs over the entire paragraph is
-/// inspired by the line breaking algorithm used in TeX, as described
-/// in the 1981 article [_Breaking Paragraphs into
-/// Lines_](http://www.eprg.org/G53DOC/pdfs/knuth-plass-breaking.pdf)
-/// by Knuth and Plass. The implementation here is based on [Python
-/// code by David
-/// Eppstein](https://github.com/jfinkels/PADS/blob/master/pads/wrap.py).
-///
-/// **Note:** Only available when the `smawk` Cargo feature is
-/// enabled.
-pub fn wrap_optimal_fit<'a, T: Fragment, F: Fn(usize) -> usize>(
-    fragments: &'a [T],
-    line_widths: F,
-) -> Vec<&'a [T]> {
-    let mut widths = Vec::with_capacity(fragments.len() + 1);
-    let mut width = 0;
-    widths.push(width);
-    for fragment in fragments {
-        width += fragment.width() + fragment.whitespace_width();
-        widths.push(width);
-    }
-
-    let line_numbers = LineNumbers::new(fragments.len());
-
-    let minima = smawk::online_column_minima(0, widths.len(), |minima, i, j| {
-        // Line number for fragment `i`.
-        let line_number = line_numbers.get(i, &minima);
-        let target_width = std::cmp::max(1, line_widths(line_number));
-
-        // Compute the width of a line spanning fragments[i..j] in
-        // constant time. We need to adjust widths[j] by subtracting
-        // the whitespace of fragment[j-i] and then add the penalty.
-        let line_width = widths[j] - widths[i] - fragments[j - 1].whitespace_width()
-            + fragments[j - 1].penalty_width();
-
-        // We compute cost of the line containing fragments[i..j]. We
-        // start with values[i].1, which is the optimal cost for
-        // breaking before fragments[i].
-        //
-        // First, every extra line cost NLINE_PENALTY.
-        let mut cost = minima[i].1 + NLINE_PENALTY;
-
-        // Next, we add a penalty depending on the line length.
-        if line_width > target_width {
-            // Lines that overflow get a hefty penalty.
-            let overflow = (line_width - target_width) as i32;
-            cost += overflow * OVERFLOW_PENALTY;
-        } else if j < fragments.len() {
-            // Other lines (except for the last line) get a milder
-            // penalty which depend on the size of the gap.
-            let gap = (target_width - line_width) as i32;
-            cost += gap * gap;
-        } else if i + 1 == j && line_width < target_width / SHORT_LINE_FRACTION {
-            // The last line can have any size gap, but we do add a
-            // penalty if the line is very short (typically because it
-            // contains just a single word).
-            cost += SHORT_LAST_LINE_PENALTY;
-        }
-
-        // Finally, we discourage hyphens.
-        if fragments[j - 1].penalty_width() > 0 {
-            // TODO: this should use a penalty value from the fragment
-            // instead.
-            cost += HYPHEN_PENALTY;
-        }
-
-        cost
-    });
-
-    let mut lines = Vec::with_capacity(line_numbers.get(fragments.len(), &minima));
-    let mut pos = fragments.len();
-    loop {
-        let prev = minima[pos].0;
-        lines.push(&fragments[prev..pos]);
-        pos = prev;
-        if pos == 0 {
-            break;
-        }
-    }
-
-    lines.reverse();
-    lines
-}
diff --git a/src/indentation.rs b/src/indentation.rs
index cc2351f..5d90c06 100644
--- a/src/indentation.rs
+++ b/src/indentation.rs
@@ -4,42 +4,45 @@
 //! The functions here can be used to uniformly indent or dedent
 //! (unindent) word wrapped lines of text.
 
-/// Add prefix to each non-empty line.
+/// Indent each line by the given prefix.
+///
+/// # Examples
 ///
 /// ```
 /// use textwrap::indent;
 ///
-/// assert_eq!(indent("
-/// Foo
-/// Bar
-/// ", "  "), "
-///   Foo
-///   Bar
-/// ");
+/// assert_eq!(indent("First line.\nSecond line.\n", "  "),
+///            "  First line.\n  Second line.\n");
 /// ```
 ///
-/// Lines consisting only of whitespace are kept unchanged:
+/// When indenting, trailing whitespace is stripped from the prefix.
+/// This means that empty lines remain empty afterwards:
 ///
 /// ```
 /// use textwrap::indent;
 ///
-/// assert_eq!(indent("
-/// Foo
-///
-/// Bar
-///   \t
-/// Baz
-/// ", "->"), "
-/// ->Foo
-///
-/// ->Bar
-///   \t
-/// ->Baz
-/// ");
+/// assert_eq!(indent("First line.\n\n\nSecond line.\n", "  "),
+///            "  First line.\n\n\n  Second line.\n");
 /// ```
 ///
-/// Leading and trailing whitespace on non-empty lines is kept
-/// unchanged:
+/// Notice how `"\n\n\n"` remained as `"\n\n\n"`.
+///
+/// This feature is useful when you want to indent text and have a
+/// space between your prefix and the text. In this case, you _don't_
+/// want a trailing space on empty lines:
+///
+/// ```
+/// use textwrap::indent;
+///
+/// assert_eq!(indent("foo = 123\n\nprint(foo)\n", "# "),
+///            "# foo = 123\n#\n# print(foo)\n");
+/// ```
+///
+/// Notice how `"\n\n"` became `"\n#\n"` instead of `"\n# \n"` which
+/// would have trailing whitespace.
+///
+/// Leading and trailing whitespace coming from the text itself is
+/// kept unchanged:
 ///
 /// ```
 /// use textwrap::indent;
@@ -47,18 +50,27 @@
 /// assert_eq!(indent(" \t  Foo   ", "->"), "-> \t  Foo   ");
 /// ```
 pub fn indent(s: &str, prefix: &str) -> String {
-    let mut result = String::new();
-
-    for (idx, line) in s.split('\n').enumerate() {
+    // We know we'll need more than s.len() bytes for the output, but
+    // without counting '\n' characters (which is somewhat slow), we
+    // don't know exactly how much. However, we can preemptively do
+    // the first doubling of the output size.
+    let mut result = String::with_capacity(2 * s.len());
+    let trimmed_prefix = prefix.trim_end();
+    for (idx, line) in s.split_terminator('\n').enumerate() {
         if idx > 0 {
             result.push('\n');
         }
-        if !line.trim().is_empty() {
+        if line.trim().is_empty() {
+            result.push_str(trimmed_prefix);
+        } else {
             result.push_str(prefix);
         }
         result.push_str(line);
     }
-
+    if s.ends_with('\n') {
+        // split_terminator will have eaten the final '\n'.
+        result.push('\n');
+    }
     result
 }
 
@@ -155,11 +167,11 @@
             "  baz\n",
         ].join("");
         let expected = [
-            "//  foo\n",
-            "//bar\n",
-            "//  baz\n",
+            "//   foo\n",
+            "// bar\n",
+            "//   baz\n",
         ].join("");
-        assert_eq!(indent(&text, "//"), expected);
+        assert_eq!(indent(&text, "// "), expected);
     }
 
     #[test]
@@ -172,12 +184,12 @@
             "  baz",
         ].join("\n");
         let expected = [
-            "//  foo",
-            "//bar",
-            "",
-            "//  baz",
+            "//   foo",
+            "// bar",
+            "//",
+            "//   baz",
         ].join("\n");
-        assert_eq!(indent(&text, "//"), expected);
+        assert_eq!(indent(&text, "// "), expected);
     }
 
     #[test]
diff --git a/src/lib.rs b/src/lib.rs
index ee6d5d8..6d68309 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -7,47 +7,35 @@
 //! you want to format dynamic output nicely so it looks good in a
 //! terminal. A quick example:
 //!
-//! ```no_run
-//! fn main() {
-//!     let text = "textwrap: a small library for wrapping text.";
-//!     println!("{}", textwrap::fill(text, 18));
-//! }
+//! ```
+//! # #[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."]);
+//! # }
 //! ```
 //!
-//! When you run this program, it will display the following output:
-//!
-//! ```text
-//! textwrap: a small
-//! library for
-//! wrapping text.
-//! ```
+//! The [`wrap`] function returns the individual lines, use [`fill`]
+//! is you want the lines joined with `'\n'` to form a `String`.
 //!
 //! If you enable the `hyphenation` Cargo feature, you can get
 //! automatic hyphenation for a number of languages:
 //!
-//! ```no_run
-//! # #[cfg(feature = "hyphenation")]
-//! use hyphenation::{Language, Load, Standard};
-//! use textwrap::{fill, Options};
-//!
-//! # #[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() { }
 //! ```
+//! #[cfg(feature = "hyphenation")] {
+//! use hyphenation::{Language, Load, Standard};
+//! use textwrap::{wrap, Options, WordSplitter};
 //!
-//! The program will now output:
-//!
-//! ```text
-//! textwrap: a small
-//! library for wrap-
-//! ping text.
+//! 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."]);
+//! }
 //! ```
 //!
 //! See also the [`unfill`] and [`refill`] functions which allow you to
@@ -86,12 +74,12 @@
 //! into a bullet list:
 //!
 //! ```
-//! let before = "
+//! let before = "\
 //! foo
 //! bar
 //! baz
 //! ";
-//! let after = "
+//! let after = "\
 //! * foo
 //! * bar
 //! * baz
@@ -124,12 +112,22 @@
 //! 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.13.4.svg">
+//! <img src="https://raw.githubusercontent.com/mgeisler/textwrap/master/images/textwrap-0.15.0.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
@@ -142,11 +140,34 @@
 //!   other ways.
 //!
 //! * `smawk`: enables linear-time wrapping of the whole paragraph via
-//!   the [smawk] crate. See the [`core::wrap_optimal_fit`] function
-//!   for details on the optimal-fit algorithm.
+//!   the [smawk] crate. See the [`wrap_algorithms::wrap_optimal_fit`]
+//!   function for details on the optimal-fit algorithm.
 //!
 //!   This feature can be disabled if you only ever intend to use
-//!   [`core::wrap_first_fit`].
+//!   [`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.
 //!
 //! ## Optional Features
 //!
@@ -157,34 +178,61 @@
 //!   [`Options::with_termwidth`] constructor for details.
 //!
 //! * `hyphenation`: enables language-sensitive hyphenation via the
-//!   [hyphenation] crate. See the [`WordSplitter`] trait for details.
+//!   [hyphenation] crate. See the [`word_splitters::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.13.4")]
+#![doc(html_root_url = "https://docs.rs/textwrap/0.15.0")]
 #![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;
-pub use crate::indentation::indent;
+pub use crate::indentation::{dedent, indent};
 
-mod splitting;
-pub use crate::splitting::{HyphenSplitter, NoHyphenation, WordSplitter};
+mod word_separators;
+pub use word_separators::WordSeparator;
+
+pub mod word_splitters;
+pub use word_splitters::WordSplitter;
+
+pub mod wrap_algorithms;
+pub use wrap_algorithms::WrapAlgorithm;
 
 pub mod core;
 
-/// Holds settings for wrapping and filling text.
+#[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.
 #[derive(Debug, Clone)]
-pub struct Options<'a, S: ?Sized = Box<dyn WordSplitter>> {
+pub struct Options<'a> {
     /// The width in columns at which the text will be wrapped.
     pub width: usize,
     /// Indentation used for the first line of output. See the
@@ -197,42 +245,44 @@
     /// When set to `false`, some lines may be longer than
     /// `self.width`. See the [`Options::break_words`] method.
     pub break_words: bool,
-    /// Wraping algorithm to use, see [`core::WrapAlgorithm`] for
-    /// details.
-    pub wrap_algorithm: core::WrapAlgorithm,
+    /// 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,
     /// 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. Please see the
-    /// [`WordSplitter`] trait for details.
-    pub splitter: S,
+    /// language-aware machine hyphenation.
+    pub word_splitter: WordSplitter,
 }
 
-impl<'a, S: ?Sized> From<&'a Options<'a, S>> for Options<'a, &'a S> {
-    fn from(options: &'a Options<'a, S>) -> Self {
+impl<'a> From<&'a Options<'a>> for Options<'a> {
+    fn from(options: &'a Options<'a>) -> 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,
-            splitter: &options.splitter,
+            word_splitter: options.word_splitter.clone(),
         }
     }
 }
 
-impl<'a> From<usize> for Options<'a, HyphenSplitter> {
+impl<'a> From<usize> for Options<'a> {
     fn from(width: usize) -> Self {
         Options::new(width)
     }
 }
 
-/// 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
+impl<'a> Options<'a> {
+    /// Creates a new [`Options`] with the specified width. Equivalent to
     ///
     /// ```
-    /// # use textwrap::{Options, HyphenSplitter, WordSplitter};
+    /// # use textwrap::{Options, WordSplitter, WordSeparator, WrapAlgorithm};
     /// # let width = 80;
     /// # let actual = Options::new(width);
     /// # let expected =
@@ -241,75 +291,37 @@
     ///     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: textwrap::core::WrapAlgorithm::OptimalFit,
+    ///     wrap_algorithm: WrapAlgorithm::new_optimal_fit(),
     ///     #[cfg(not(feature = "smawk"))]
-    ///     wrap_algorithm: textwrap::core::WrapAlgorithm::FirstFit,
-    ///     splitter: HyphenSplitter,
+    ///     wrap_algorithm: WrapAlgorithm::FirstFit,
+    ///     word_splitter: WordSplitter::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.wrap_algorithm, expected.wrap_algorithm);
-    /// # let expected_coerced: Options<'static, HyphenSplitter> = expected;
+    /// # assert_eq!(actual.word_splitter, expected.word_splitter);
     /// ```
     ///
-    /// 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;
-    /// ```
-    ///
-    /// 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.
+    /// Note that the default word separator and wrap algorithms
+    /// changes based on the available Cargo features. The best
+    /// available algorithms are used by default.
     pub const fn new(width: usize) -> Self {
-        Options::with_splitter(width, HyphenSplitter)
+        Options {
+            width,
+            initial_indent: "",
+            subsequent_indent: "",
+            break_words: true,
+            word_separator: DefaultWordSeparator!(),
+            wrap_algorithm: WrapAlgorithm::new(),
+            word_splitter: WordSplitter::HyphenSplitter,
+        }
     }
 
     /// Creates a new [`Options`] with `width` set to the current
@@ -335,90 +347,7 @@
     }
 }
 
-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> {
+impl<'a> Options<'a> {
     /// Change [`self.initial_indent`]. The initial indentation is
     /// used on the very first line of output.
     ///
@@ -428,7 +357,7 @@
     /// initial indentation and wrapping each paragraph by itself:
     ///
     /// ```
-    /// use textwrap::{Options, wrap};
+    /// use textwrap::{wrap, Options};
     ///
     /// let options = Options::new(16).initial_indent("    ");
     /// assert_eq!(wrap("This is a little example.", options),
@@ -453,7 +382,7 @@
     /// single paragraph as a bullet list:
     ///
     /// ```
-    /// use textwrap::{Options, wrap};
+    /// use textwrap::{wrap, Options};
     ///
     /// let options = Options::new(12)
     ///     .initial_indent("* ")
@@ -507,50 +436,74 @@
         }
     }
 
-    /// Change [`self.wrap_algorithm`].
+    /// Change [`self.word_separator`].
     ///
-    /// See [`core::WrapAlgorithm`] for details on the choices.
+    /// See [`word_separators::WordSeparator`] for details on the choices.
     ///
-    /// [`self.wrap_algorithm`]: #structfield.wrap_algorithm
-    pub fn wrap_algorithm(self, wrap_algorithm: core::WrapAlgorithm) -> Self {
-        Options {
-            wrap_algorithm,
-            ..self
-        }
-    }
-
-    /// Change [`self.splitter`]. The [`WordSplitter`] is used to fit
-    /// part of a word into the current line when wrapping text.
-    ///
-    /// 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::{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.splitter`]: #structfield.splitter
-    pub fn splitter<T>(self, splitter: T) -> Options<'a, T> {
+    /// [`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,
-            splitter: splitter,
+            word_splitter: self.word_splitter,
+        }
+    }
+
+    /// Change [`self.wrap_algorithm`].
+    ///
+    /// See the [`wrap_algorithms::WrapAlgorithm`] trait for details on
+    /// the choices.
+    ///
+    /// [`self.wrap_algorithm`]: #structfield.wrap_algorithm
+    pub fn wrap_algorithm(self, wrap_algorithm: WrapAlgorithm) -> Options<'a> {
+        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,
+        }
+    }
+
+    /// Change [`self.word_splitter`]. The
+    /// [`word_splitters::WordSplitter`] is used to fit part of a word
+    /// into the current line when wrapping text.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// 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);
+    /// ```
+    ///
+    /// [`self.word_splitter`]: #structfield.word_splitter
+    pub fn word_splitter(self, word_splitter: WordSplitter) -> Options<'a> {
+        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,
         }
     }
 }
 
-/// 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
 ///
@@ -558,11 +511,10 @@
 /// with a two column margin to the left and the right:
 ///
 /// ```no_run
-/// use textwrap::{termwidth, NoHyphenation, Options};
+/// use textwrap::{termwidth, Options};
 ///
 /// let width = termwidth() - 4; // Two columns on each side.
 /// let options = Options::new(width)
-///     .splitter(NoHyphenation)
 ///     .initial_indent("  ")
 ///     .subsequent_indent("  ");
 /// ```
@@ -606,10 +558,9 @@
 ///     "- Memory safety\n  without\n  garbage\n  collection."
 /// );
 /// ```
-pub fn fill<'a, S, Opt>(text: &str, width_or_options: Opt) -> String
+pub fn fill<'a, Opt>(text: &str, width_or_options: Opt) -> String
 where
-    S: WordSplitter,
-    Opt: Into<Options<'a, S>>,
+    Opt: Into<Options<'a>>,
 {
     // This will avoid reallocation in simple cases (no
     // indentation, no hyphenation).
@@ -619,7 +570,7 @@
         if i > 0 {
             result.push('\n');
         }
-        result.push_str(&line);
+        result.push_str(line);
     }
 
     result
@@ -671,7 +622,7 @@
 /// assert_eq!(options.initial_indent, "* ");
 /// assert_eq!(options.subsequent_indent, "  ");
 /// ```
-pub fn unfill<'a>(text: &'a str) -> (String, Options<'a, HyphenSplitter>) {
+pub fn unfill(text: &str) -> (String, Options<'_>) {
     let trimmed = text.trim_end_matches('\n');
     let prefix_chars: &[_] = &[' ', '-', '+', '*', '>', '#', '/'];
 
@@ -728,20 +679,47 @@
 /// ```
 /// 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, 15), "\
+///
+/// assert_eq!(refill(text, 20), "\
 /// > Memory safety
-/// > without
-/// > garbage
+/// > without garbage
 /// > collection.
 /// ");
-pub fn refill<'a, S, Opt>(filled_text: &str, new_width_or_options: Opt) -> String
+///
+/// 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
 where
-    S: WordSplitter,
-    Opt: Into<Options<'a, S>>,
+    Opt: Into<Options<'a>>,
 {
     let trimmed = filled_text.trim_end_matches('\n');
     let (text, options) = unfill(trimmed);
@@ -757,8 +735,9 @@
 ///
 /// 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 a trailing `'\n'`. Use
-/// the [`fill`] function if you need a [`String`] instead.
+/// `&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.
 ///
 /// The easiest way to use this function is to pass an integer for
 /// `width_or_options`:
@@ -806,8 +785,7 @@
 /// narrow column with room for only 10 characters looks like this:
 ///
 /// ```
-/// # use textwrap::{Options, wrap};
-/// # use textwrap::core::WrapAlgorithm::FirstFit;
+/// # use textwrap::{WrapAlgorithm::FirstFit, Options, wrap};
 /// #
 /// # let lines = wrap("To be, or not to be: that is the question",
 /// #                  Options::new(10).wrap_algorithm(FirstFit));
@@ -831,11 +809,12 @@
 ///
 /// ```
 /// # #[cfg(feature = "smawk")] {
-/// # use textwrap::{Options, wrap};
-/// # use textwrap::core::WrapAlgorithm::OptimalFit;
+/// # use textwrap::{Options, WrapAlgorithm, wrap};
 /// #
-/// # let lines = wrap("To be, or not to be: that is the question",
-/// #                  Options::new(10).wrap_algorithm(OptimalFit));
+/// # let lines = wrap(
+/// #     "To be, or not to be: that is the question",
+/// #     Options::new(10).wrap_algorithm(WrapAlgorithm::new_optimal_fit())
+/// # );
 /// # assert_eq!(lines.join("\n") + "\n", "\
 /// To be,
 /// or not to
@@ -845,7 +824,7 @@
 /// # "); }
 /// ```
 ///
-/// Please see [`core::WrapAlgorithm`] for details.
+/// Please see [`WrapAlgorithm`] for details on the choices.
 ///
 /// # Examples
 ///
@@ -876,10 +855,55 @@
 ///     ]
 /// );
 /// ```
-pub fn wrap<'a, S, Opt>(text: &str, width_or_options: Opt) -> Vec<Cow<'_, str>>
+///
+/// ## 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>>
 where
-    S: WordSplitter,
-    Opt: Into<Options<'a, S>>,
+    Opt: Into<Options<'a>>,
 {
     let options = width_or_options.into();
 
@@ -892,8 +916,8 @@
 
     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 words = options.word_separator.find_words(line);
+        let split_words = word_splitters::split_words(words, &options.word_splitter);
         let broken_words = if options.break_words {
             let mut broken_words = core::break_words(split_words, subsequent_width);
             if !options.initial_indent.is_empty() {
@@ -909,13 +933,8 @@
             split_words.collect::<Vec<_>>()
         };
 
-        #[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 line_widths = [initial_width, subsequent_width];
+        let wrapped_words = options.wrap_algorithm.wrap(&broken_words, &line_widths);
 
         let mut idx = 0;
         for words in wrapped_words {
@@ -952,7 +971,7 @@
             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);
@@ -969,7 +988,7 @@
 
 /// Wrap text into columns with a given total width.
 ///
-/// The `left_gap`, `mid_gap` and `right_gap` arguments specify the
+/// The `left_gap`, `middle_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
@@ -1024,17 +1043,16 @@
 ///                 "| example text, | columns.      | shorter than   |",
 ///                 "| which is      | Notice how    | the others.    |",
 ///                 "| wrapped into  | the final     |                |"]);
-pub fn wrap_columns<'a, S, Opt>(
+pub fn wrap_columns<'a, Opt>(
     text: &str,
     columns: usize,
     total_width_or_options: Opt,
     left_gap: &str,
-    mid_gap: &str,
+    middle_gap: &str,
     right_gap: &str,
 ) -> Vec<String>
 where
-    S: WordSplitter,
-    Opt: Into<Options<'a, S>>,
+    Opt: Into<Options<'a>>,
 {
     assert!(columns > 0);
 
@@ -1044,7 +1062,7 @@
         .width
         .saturating_sub(core::display_width(left_gap))
         .saturating_sub(core::display_width(right_gap))
-        .saturating_sub(core::display_width(mid_gap) * (columns - 1));
+        .saturating_sub(core::display_width(middle_gap) * (columns - 1));
 
     let column_width = std::cmp::max(inner_width / columns, 1);
     options.width = column_width;
@@ -1058,8 +1076,8 @@
         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));
@@ -1068,7 +1086,7 @@
             if column_no == columns - 1 {
                 line.push_str(&last_column_padding);
             } else {
-                line.push_str(mid_gap);
+                line.push_str(middle_gap);
             }
         }
         line.push_str(right_gap);
@@ -1086,24 +1104,27 @@
 ///
 /// 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. 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. 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:
 ///
 /// ```
-/// # use textwrap::{Options, NoHyphenation};
+/// # use textwrap::{core, Options, WordSplitter, WordSeparator, WrapAlgorithm};
 /// # let width = 80;
 /// Options {
 ///     width: width,
 ///     initial_indent: "",
 ///     subsequent_indent: "",
 ///     break_words: false,
-///     wrap_algorithm: textwrap::core::WrapAlgorithm::FirstFit,
-///     splitter: NoHyphenation,
+///     word_separator: WordSeparator::AsciiSpace,
+///     wrap_algorithm: WrapAlgorithm::FirstFit,
+///     word_splitter: WordSplitter::NoHyphenation,
 /// };
 /// ```
 ///
-/// The wrap algorithm is [`core::WrapAlgorithm::FirstFit`] since this
+/// The wrap algorithm is [`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.
@@ -1133,8 +1154,10 @@
 
     let mut offset = 0;
     for line in text.split('\n') {
-        let words = core::find_words(line).collect::<Vec<_>>();
-        let wrapped_words = core::wrap_first_fit(&words, |_| width);
+        let words = WordSeparator::AsciiSpace
+            .find_words(line)
+            .collect::<Vec<_>>();
+        let wrapped_words = wrap_algorithms::wrap_first_fit(&words, &[width as f64]);
 
         let mut line_offset = offset;
         for words in &wrapped_words[..wrapped_words.len() - 1] {
@@ -1164,6 +1187,7 @@
 #[cfg(test)]
 mod tests {
     use super::*;
+
     #[cfg(feature = "hyphenation")]
     use hyphenation::{Language, Load, Standard};
 
@@ -1177,8 +1201,8 @@
         assert_eq!(opt_usize.subsequent_indent, opt_options.subsequent_indent);
         assert_eq!(opt_usize.break_words, opt_options.break_words);
         assert_eq!(
-            opt_usize.splitter.split_points("hello-world"),
-            opt_options.splitter.split_points("hello-world")
+            opt_usize.word_splitter.split_points("hello-world"),
+            opt_options.word_splitter.split_points("hello-world")
         );
     }
 
@@ -1197,7 +1221,7 @@
         assert_eq!(
             wrap(
                 "To be, or not to be, that is the question.",
-                Options::new(10).wrap_algorithm(core::WrapAlgorithm::FirstFit)
+                Options::new(10).wrap_algorithm(WrapAlgorithm::FirstFit)
             ),
             vec!["To be, or", "not to be,", "that is", "the", "question."]
         );
@@ -1220,7 +1244,11 @@
 
     #[test]
     fn max_width() {
-        assert_eq!(wrap("foo bar", usize::max_value()), vec!["foo bar"]);
+        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]);
     }
 
     #[test]
@@ -1229,6 +1257,15 @@
     }
 
     #[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
@@ -1250,17 +1287,31 @@
     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.
-        assert_eq!(wrap("x – x", 1), vec!["x", "–", "x"]);
+        let options = Options::new(1).word_separator(WordSeparator::AsciiSpace);
+        assert_eq!(wrap("x – x", options), 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!", 15),
+            wrap(
+                "Hello, World!",
+                Options::new(15).word_separator(WordSeparator::AsciiSpace)
+            ),
             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]
@@ -1280,7 +1331,6 @@
     }
 
     #[test]
-    #[cfg(feature = "unicode-width")]
     fn indent_first_emoji() {
         let options = Options::new(10).initial_indent("👉👉");
         assert_eq!(
@@ -1388,32 +1438,20 @@
     }
 
     #[test]
-    fn simple_hyphens_static() {
-        let options = Options::new(8).splitter(HyphenSplitter);
+    fn simple_hyphens() {
+        let options = Options::new(8).word_splitter(WordSplitter::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_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));
+    fn no_hyphenation() {
+        let options = Options::new(8).word_splitter(WordSplitter::NoHyphenation);
         assert_eq!(wrap("foo bar-baz", &options), vec!["foo", "bar-baz"]);
     }
 
     #[test]
     #[cfg(feature = "hyphenation")]
-    fn auto_hyphenation_double_hyphenation_static() {
+    fn auto_hyphenation_double_hyphenation() {
         let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap();
         let options = Options::new(10);
         assert_eq!(
@@ -1421,24 +1459,7 @@
             vec!["Internatio", "nalization"]
         );
 
-        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));
+        let options = Options::new(10).word_splitter(WordSplitter::Hyphenation(dictionary));
         assert_eq!(
             wrap("Internationalization", &options),
             vec!["Interna-", "tionaliza-", "tion"]
@@ -1455,7 +1476,7 @@
             vec!["participat", "ion is", "the key to", "success"]
         );
 
-        let options = Options::new(10).splitter(dictionary);
+        let options = Options::new(10).word_splitter(WordSplitter::Hyphenation(dictionary));
         assert_eq!(
             wrap("participation is the key to success", &options),
             vec!["partici-", "pation is", "the key to", "success"]
@@ -1465,10 +1486,10 @@
     #[test]
     #[cfg(feature = "hyphenation")]
     fn split_len_hyphenation() {
-        // Test that hyphenation takes the width of the wihtespace
+        // Test that hyphenation takes the width of the whitespace
         // into account.
         let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap();
-        let options = Options::new(15).splitter(dictionary);
+        let options = Options::new(15).word_splitter(WordSplitter::Hyphenation(dictionary));
         assert_eq!(
             wrap("garbage   collection", &options),
             vec!["garbage   col-", "lection"]
@@ -1482,8 +1503,9 @@
         // line is borrowed.
         use std::borrow::Cow::{Borrowed, Owned};
         let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap();
-        let options = Options::new(10).splitter(dictionary);
+        let options = Options::new(10).word_splitter(WordSplitter::Hyphenation(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);
         }
@@ -1505,7 +1527,7 @@
             vec!["over-", "caffinated"]
         );
 
-        let options = options.splitter(dictionary);
+        let options = options.word_splitter(WordSplitter::Hyphenation(dictionary));
         assert_eq!(
             wrap("over-caffinated", &options),
             vec!["over-", "caffi-", "nated"]
@@ -1521,7 +1543,8 @@
     fn break_words_wide_characters() {
         // Even the poor man's version of `ch_width` counts these
         // characters as wide.
-        assert_eq!(wrap("Hello", 5), vec!["He", "ll", "o"]);
+        let options = Options::new(5).word_separator(WordSeparator::AsciiSpace);
+        assert_eq!(wrap("Hello", options), vec!["He", "ll", "o"]);
     }
 
     #[test]
@@ -1558,14 +1581,14 @@
         assert_eq!(
             fill(
                 "1 3 5 7\n1 3 5 7",
-                Options::new(7).wrap_algorithm(core::WrapAlgorithm::FirstFit)
+                Options::new(7).wrap_algorithm(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(core::WrapAlgorithm::FirstFit)
+                Options::new(5).wrap_algorithm(WrapAlgorithm::FirstFit)
             ),
             "1 3 5\n7\n1 3 5\n7"
         );
@@ -1608,11 +1631,9 @@
     }
 
     #[test]
-    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);
+    fn fill_unicode_boundary() {
+        // https://github.com/mgeisler/textwrap/issues/390
+        fill("\u{1b}!Ͽ", 10);
     }
 
     #[test]
@@ -1751,74 +1772,6 @@
     }
 
     #[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!["|        |"]);
     }
diff --git a/src/splitting.rs b/src/splitting.rs
deleted file mode 100644
index e92b188..0000000
--- a/src/splitting.rs
+++ /dev/null
@@ -1,140 +0,0 @@
-//! Word splitting functionality.
-//!
-//! To wrap text into lines, long words sometimes need to be split
-//! across lines. The [`WordSplitter`] trait defines this
-//! functionality. [`HyphenSplitter`] is the default implementation of
-//! this treat: it will simply split words on existing hyphens.
-
-/// The `WordSplitter` trait describes where words can be split.
-///
-/// If the textwrap crate has been compiled with the `hyphenation`
-/// Cargo feature enabled, you will find an implementation of
-/// `WordSplitter` by the `hyphenation::Standard` struct. Use this
-/// struct for language-aware hyphenation:
-///
-/// ```
-/// #[cfg(feature = "hyphenation")]
-/// {
-///     use hyphenation::{Language, Load, Standard};
-///     use textwrap::{wrap, Options};
-///
-///     let text = "Oxidation is the loss of electrons.";
-///     let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap();
-///     let options = Options::new(8).splitter(dictionary);
-///     assert_eq!(wrap(text, &options), vec!["Oxida-",
-///                                           "tion is",
-///                                           "the loss",
-///                                           "of elec-",
-///                                           "trons."]);
-/// }
-/// ```
-///
-/// Please see the documentation for the [hyphenation] crate for more
-/// details.
-///
-/// [hyphenation]: https://docs.rs/hyphenation/
-pub trait WordSplitter: std::fmt::Debug {
-    /// Return all possible indices where `word` can be split.
-    ///
-    /// The indices returned must be in range `0..word.len()`. They
-    /// should point to the index _after_ the split point, i.e., after
-    /// `-` if splitting on hyphens. This way, `word.split_at(idx)`
-    /// will break the word into two well-formed pieces.
-    ///
-    /// # Examples
-    ///
-    /// ```
-    /// use textwrap::{HyphenSplitter, NoHyphenation, WordSplitter};
-    /// assert_eq!(NoHyphenation.split_points("cannot-be-split"), vec![]);
-    /// assert_eq!(HyphenSplitter.split_points("can-be-split"), vec![4, 7]);
-    /// ```
-    fn split_points(&self, word: &str) -> Vec<usize>;
-}
-
-impl<S: WordSplitter + ?Sized> WordSplitter for Box<S> {
-    fn split_points(&self, word: &str) -> Vec<usize> {
-        use std::ops::Deref;
-        self.deref().split_points(word)
-    }
-}
-
-impl<T: ?Sized + WordSplitter> WordSplitter for &T {
-    fn split_points(&self, word: &str) -> Vec<usize> {
-        (*self).split_points(word)
-    }
-}
-
-/// Use this as a [`Options.splitter`] to avoid any kind of
-/// hyphenation:
-///
-/// ```
-/// use textwrap::{wrap, NoHyphenation, Options};
-///
-/// let options = Options::new(8).splitter(NoHyphenation);
-/// assert_eq!(wrap("foo bar-baz", &options),
-///            vec!["foo", "bar-baz"]);
-/// ```
-///
-/// [`Options.splitter`]: super::Options::splitter
-#[derive(Clone, Copy, Debug)]
-pub struct NoHyphenation;
-
-/// `NoHyphenation` implements `WordSplitter` by not splitting the
-/// word at all.
-impl WordSplitter for NoHyphenation {
-    fn split_points(&self, _: &str) -> Vec<usize> {
-        Vec::new()
-    }
-}
-
-/// Simple and default way to split words: splitting on existing
-/// hyphens only.
-///
-/// You probably don't need to use this type since it's already used
-/// by default by [`Options::new`](super::Options::new).
-#[derive(Clone, Copy, Debug)]
-pub struct HyphenSplitter;
-
-/// `HyphenSplitter` is the default `WordSplitter` used by
-/// [`Options::new`](super::Options::new). It will split words on any
-/// existing hyphens in the word.
-///
-/// It will only use hyphens that are surrounded by alphanumeric
-/// characters, which prevents a word like `"--foo-bar"` from being
-/// split into `"--"` and `"foo-bar"`.
-impl WordSplitter for HyphenSplitter {
-    fn split_points(&self, word: &str) -> Vec<usize> {
-        let mut splits = Vec::new();
-
-        for (idx, _) in word.match_indices('-') {
-            // We only use hyphens that are surrounded by alphanumeric
-            // characters. This is to avoid splitting on repeated hyphens,
-            // such as those found in --foo-bar.
-            let prev = word[..idx].chars().next_back();
-            let next = word[idx + 1..].chars().next();
-
-            if prev.filter(|ch| ch.is_alphanumeric()).is_some()
-                && next.filter(|ch| ch.is_alphanumeric()).is_some()
-            {
-                splits.push(idx + 1); // +1 due to width of '-'.
-            }
-        }
-
-        splits
-    }
-}
-
-/// A hyphenation dictionary can be used to do language-specific
-/// hyphenation using patterns from the [hyphenation] crate.
-///
-/// **Note:** Only available when the `hyphenation` Cargo feature is
-/// enabled.
-///
-/// [hyphenation]: https://docs.rs/hyphenation/
-#[cfg(feature = "hyphenation")]
-impl WordSplitter for hyphenation::Standard {
-    fn split_points(&self, word: &str) -> Vec<usize> {
-        use hyphenation::Hyphenator;
-        self.hyphenate(word).breaks
-    }
-}
diff --git a/src/word_separators.rs b/src/word_separators.rs
new file mode 100644
index 0000000..25adf31
--- /dev/null
+++ b/src/word_separators.rs
@@ -0,0 +1,428 @@
+//! Functionality for finding words.
+//!
+//! In order to wrap text, we need to know where the legal break
+//! points are, i.e., where the words of the text are. This means that
+//! we need to define what a "word" is.
+//!
+//! A simple approach is to simply split the text on whitespace, but
+//! this does not work for East-Asian languages such as Chinese or
+//! Japanese where there are no spaces between words. Breaking a long
+//! sequence of emojis is another example where line breaks might be
+//! wanted even if there are no whitespace to be found.
+//!
+//! The [`WordSeparator`] trait is responsible for determining where
+//! there words are in a line of text. Please refer to the trait and
+//! the structs which implement it for more information.
+
+#[cfg(feature = "unicode-linebreak")]
+use crate::core::skip_ansi_escape_sequence;
+use crate::core::Word;
+
+/// Describes where words occur in a line of text.
+///
+/// The simplest approach is say that words are separated by one or
+/// more ASCII spaces (`' '`). This works for Western languages
+/// without emojis. A more complex approach is to use the Unicode line
+/// breaking algorithm, which finds break points in non-ASCII text.
+///
+/// The line breaks occur between words, please see
+/// [`WordSplitter`](crate::WordSplitter) for options of how to handle
+/// hyphenation of individual words.
+///
+/// # Examples
+///
+/// ```
+/// use textwrap::core::Word;
+/// use textwrap::WordSeparator::AsciiSpace;
+///
+/// let words = AsciiSpace.find_words("Hello World!").collect::<Vec<_>>();
+/// assert_eq!(words, vec![Word::from("Hello "), Word::from("World!")]);
+/// ```
+#[derive(Clone, Copy)]
+pub enum WordSeparator {
+    /// Find words by splitting on runs of `' '` characters.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use textwrap::core::Word;
+    /// use textwrap::WordSeparator::AsciiSpace;
+    ///
+    /// let words = AsciiSpace.find_words("Hello   World!").collect::<Vec<_>>();
+    /// assert_eq!(words, vec![Word::from("Hello   "),
+    ///                        Word::from("World!")]);
+    /// ```
+    AsciiSpace,
+
+    /// Split `line` into words using Unicode break properties.
+    ///
+    /// This word separator uses the Unicode line breaking algorithm
+    /// described in [Unicode Standard Annex
+    /// #14](https://www.unicode.org/reports/tr14/) to find legal places
+    /// to break lines. There is a small difference in that the U+002D
+    /// (Hyphen-Minus) and U+00AD (Soft Hyphen) don’t create a line break:
+    /// to allow a line break at a hyphen, use
+    /// [`WordSplitter::HyphenSplitter`](crate::WordSplitter::HyphenSplitter).
+    /// Soft hyphens are not currently supported.
+    ///
+    /// # Examples
+    ///
+    /// Unlike [`WordSeparator::AsciiSpace`], the Unicode line
+    /// breaking algorithm will find line break opportunities between
+    /// some characters with no intervening whitespace:
+    ///
+    /// ```
+    /// #[cfg(feature = "unicode-linebreak")] {
+    /// use textwrap::core::Word;
+    /// use textwrap::WordSeparator::UnicodeBreakProperties;
+    ///
+    /// assert_eq!(UnicodeBreakProperties.find_words("Emojis: 😂😍").collect::<Vec<_>>(),
+    ///            vec![Word::from("Emojis: "),
+    ///                 Word::from("😂"),
+    ///                 Word::from("😍")]);
+    ///
+    /// assert_eq!(UnicodeBreakProperties.find_words("CJK: 你好").collect::<Vec<_>>(),
+    ///            vec![Word::from("CJK: "),
+    ///                 Word::from("你"),
+    ///                 Word::from("好")]);
+    /// }
+    /// ```
+    ///
+    /// A U+2060 (Word Joiner) character can be inserted if you want to
+    /// manually override the defaults and keep the characters together:
+    ///
+    /// ```
+    /// #[cfg(feature = "unicode-linebreak")] {
+    /// use textwrap::core::Word;
+    /// use textwrap::WordSeparator::UnicodeBreakProperties;
+    ///
+    /// assert_eq!(UnicodeBreakProperties.find_words("Emojis: 😂\u{2060}😍").collect::<Vec<_>>(),
+    ///            vec![Word::from("Emojis: "),
+    ///                 Word::from("😂\u{2060}😍")]);
+    /// }
+    /// ```
+    ///
+    /// The Unicode line breaking algorithm will also automatically
+    /// suppress break breaks around certain punctuation characters::
+    ///
+    /// ```
+    /// #[cfg(feature = "unicode-linebreak")] {
+    /// use textwrap::core::Word;
+    /// use textwrap::WordSeparator::UnicodeBreakProperties;
+    ///
+    /// assert_eq!(UnicodeBreakProperties.find_words("[ foo ] bar !").collect::<Vec<_>>(),
+    ///            vec![Word::from("[ foo ] "),
+    ///                 Word::from("bar !")]);
+    /// }
+    /// ```
+    #[cfg(feature = "unicode-linebreak")]
+    UnicodeBreakProperties,
+
+    /// Find words using a custom word separator
+    Custom(fn(line: &str) -> Box<dyn Iterator<Item = Word<'_>> + '_>),
+}
+
+impl std::fmt::Debug for WordSeparator {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            WordSeparator::AsciiSpace => f.write_str("AsciiSpace"),
+            #[cfg(feature = "unicode-linebreak")]
+            WordSeparator::UnicodeBreakProperties => f.write_str("UnicodeBreakProperties"),
+            WordSeparator::Custom(_) => f.write_str("Custom(...)"),
+        }
+    }
+}
+
+impl WordSeparator {
+    // This function should really return impl Iterator<Item = Word>, but
+    // this isn't possible until Rust supports higher-kinded types:
+    // https://github.com/rust-lang/rfcs/blob/master/text/1522-conservative-impl-trait.md
+    /// Find all words in `line`.
+    pub fn find_words<'a>(&self, line: &'a str) -> Box<dyn Iterator<Item = Word<'a>> + 'a> {
+        match self {
+            WordSeparator::AsciiSpace => find_words_ascii_space(line),
+            #[cfg(feature = "unicode-linebreak")]
+            WordSeparator::UnicodeBreakProperties => find_words_unicode_break_properties(line),
+            WordSeparator::Custom(func) => func(line),
+        }
+    }
+}
+
+fn find_words_ascii_space<'a>(line: &'a str) -> Box<dyn Iterator<Item = Word<'a>> + 'a> {
+    let mut start = 0;
+    let mut in_whitespace = false;
+    let mut char_indices = line.char_indices();
+
+    Box::new(std::iter::from_fn(move || {
+        // for (idx, ch) in char_indices does not work, gives this
+        // error:
+        //
+        // > cannot move out of `char_indices`, a captured variable in
+        // > an `FnMut` closure
+        #[allow(clippy::while_let_on_iterator)]
+        while let Some((idx, ch)) = char_indices.next() {
+            if in_whitespace && ch != ' ' {
+                let word = Word::from(&line[start..idx]);
+                start = idx;
+                in_whitespace = ch == ' ';
+                return Some(word);
+            }
+
+            in_whitespace = ch == ' ';
+        }
+
+        if start < line.len() {
+            let word = Word::from(&line[start..]);
+            start = line.len();
+            return Some(word);
+        }
+
+        None
+    }))
+}
+
+// Strip all ANSI escape sequences from `text`.
+#[cfg(feature = "unicode-linebreak")]
+fn strip_ansi_escape_sequences(text: &str) -> String {
+    let mut result = String::with_capacity(text.len());
+
+    let mut chars = text.chars();
+    while let Some(ch) = chars.next() {
+        if skip_ansi_escape_sequence(ch, &mut chars) {
+            continue;
+        }
+        result.push(ch);
+    }
+
+    result
+}
+
+/// Soft hyphen, also knows as a “shy hyphen”. Should show up as ‘-’
+/// if a line is broken at this point, and otherwise be invisible.
+/// Textwrap does not currently support breaking words at soft
+/// hyphens.
+#[cfg(feature = "unicode-linebreak")]
+const SHY: char = '\u{00ad}';
+
+/// Find words in line. ANSI escape sequences are ignored in `line`.
+#[cfg(feature = "unicode-linebreak")]
+fn find_words_unicode_break_properties<'a>(
+    line: &'a str,
+) -> Box<dyn Iterator<Item = Word<'a>> + 'a> {
+    // Construct an iterator over (original index, stripped index)
+    // tuples. We find the Unicode linebreaks on a stripped string,
+    // but we need the original indices so we can form words based on
+    // the original string.
+    let mut last_stripped_idx = 0;
+    let mut char_indices = line.char_indices();
+    let mut idx_map = std::iter::from_fn(move || match char_indices.next() {
+        Some((orig_idx, ch)) => {
+            let stripped_idx = last_stripped_idx;
+            if !skip_ansi_escape_sequence(ch, &mut char_indices.by_ref().map(|(_, ch)| ch)) {
+                last_stripped_idx += ch.len_utf8();
+            }
+            Some((orig_idx, stripped_idx))
+        }
+        None => None,
+    });
+
+    let stripped = strip_ansi_escape_sequences(line);
+    let mut opportunities = unicode_linebreak::linebreaks(&stripped)
+        .filter(|(idx, _)| {
+            #[allow(clippy::match_like_matches_macro)]
+            match &stripped[..*idx].chars().next_back() {
+                // We suppress breaks at ‘-’ since we want to control
+                // this via the WordSplitter.
+                Some('-') => false,
+                // Soft hyphens are currently not supported since we
+                // require all `Word` fragments to be continuous in
+                // the input string.
+                Some(SHY) => false,
+                // Other breaks should be fine!
+                _ => true,
+            }
+        })
+        .collect::<Vec<_>>()
+        .into_iter();
+
+    // Remove final break opportunity, we will add it below using
+    // &line[start..]; This ensures that we correctly include a
+    // trailing ANSI escape sequence.
+    opportunities.next_back();
+
+    let mut start = 0;
+    Box::new(std::iter::from_fn(move || {
+        #[allow(clippy::while_let_on_iterator)]
+        while let Some((idx, _)) = opportunities.next() {
+            if let Some((orig_idx, _)) = idx_map.find(|&(_, stripped_idx)| stripped_idx == idx) {
+                let word = Word::from(&line[start..orig_idx]);
+                start = orig_idx;
+                return Some(word);
+            }
+        }
+
+        if start < line.len() {
+            let word = Word::from(&line[start..]);
+            start = line.len();
+            return Some(word);
+        }
+
+        None
+    }))
+}
+
+#[cfg(test)]
+mod tests {
+    use super::WordSeparator::*;
+    use super::*;
+
+    // Like assert_eq!, but the left expression is an iterator.
+    macro_rules! assert_iter_eq {
+        ($left:expr, $right:expr) => {
+            assert_eq!($left.collect::<Vec<_>>(), $right);
+        };
+    }
+
+    fn to_words<'a>(words: Vec<&'a str>) -> Vec<Word<'a>> {
+        words.into_iter().map(|w: &str| Word::from(&w)).collect()
+    }
+
+    macro_rules! test_find_words {
+        ($ascii_name:ident,
+         $unicode_name:ident,
+         $([ $line:expr, $ascii_words:expr, $unicode_words:expr ]),+) => {
+            #[test]
+            fn $ascii_name() {
+                $(
+                    let expected_words = to_words($ascii_words.to_vec());
+                    let actual_words = WordSeparator::AsciiSpace
+                        .find_words($line)
+                        .collect::<Vec<_>>();
+                    assert_eq!(actual_words, expected_words, "Line: {:?}", $line);
+                )+
+            }
+
+            #[test]
+            #[cfg(feature = "unicode-linebreak")]
+            fn $unicode_name() {
+                $(
+                    let expected_words = to_words($unicode_words.to_vec());
+                    let actual_words = WordSeparator::UnicodeBreakProperties
+                        .find_words($line)
+                        .collect::<Vec<_>>();
+                    assert_eq!(actual_words, expected_words, "Line: {:?}", $line);
+                )+
+            }
+        };
+    }
+
+    test_find_words!(ascii_space_empty, unicode_empty, ["", [], []]);
+
+    test_find_words!(
+        ascii_single_word,
+        unicode_single_word,
+        ["foo", ["foo"], ["foo"]]
+    );
+
+    test_find_words!(
+        ascii_two_words,
+        unicode_two_words,
+        ["foo bar", ["foo ", "bar"], ["foo ", "bar"]]
+    );
+
+    test_find_words!(
+        ascii_multiple_words,
+        unicode_multiple_words,
+        ["foo bar", ["foo ", "bar"], ["foo ", "bar"]],
+        ["x y z", ["x ", "y ", "z"], ["x ", "y ", "z"]]
+    );
+
+    test_find_words!(
+        ascii_only_whitespace,
+        unicode_only_whitespace,
+        [" ", [" "], [" "]],
+        ["    ", ["    "], ["    "]]
+    );
+
+    test_find_words!(
+        ascii_inter_word_whitespace,
+        unicode_inter_word_whitespace,
+        ["foo   bar", ["foo   ", "bar"], ["foo   ", "bar"]]
+    );
+
+    test_find_words!(
+        ascii_trailing_whitespace,
+        unicode_trailing_whitespace,
+        ["foo   ", ["foo   "], ["foo   "]]
+    );
+
+    test_find_words!(
+        ascii_leading_whitespace,
+        unicode_leading_whitespace,
+        ["   foo", ["   ", "foo"], ["   ", "foo"]]
+    );
+
+    test_find_words!(
+        ascii_multi_column_char,
+        unicode_multi_column_char,
+        ["\u{1f920}", ["\u{1f920}"], ["\u{1f920}"]] // cowboy emoji 🤠
+    );
+
+    test_find_words!(
+        ascii_hyphens,
+        unicode_hyphens,
+        ["foo-bar", ["foo-bar"], ["foo-bar"]],
+        ["foo- bar", ["foo- ", "bar"], ["foo- ", "bar"]],
+        ["foo - bar", ["foo ", "- ", "bar"], ["foo ", "- ", "bar"]],
+        ["foo -bar", ["foo ", "-bar"], ["foo ", "-bar"]]
+    );
+
+    test_find_words!(
+        ascii_newline,
+        unicode_newline,
+        ["foo\nbar", ["foo\nbar"], ["foo\n", "bar"]]
+    );
+
+    test_find_words!(
+        ascii_tab,
+        unicode_tab,
+        ["foo\tbar", ["foo\tbar"], ["foo\t", "bar"]]
+    );
+
+    test_find_words!(
+        ascii_non_breaking_space,
+        unicode_non_breaking_space,
+        ["foo\u{00A0}bar", ["foo\u{00A0}bar"], ["foo\u{00A0}bar"]]
+    );
+
+    #[test]
+    #[cfg(unix)]
+    fn find_words_colored_text() {
+        use termion::color::{Blue, Fg, Green, Reset};
+
+        let green_hello = format!("{}Hello{} ", Fg(Green), Fg(Reset));
+        let blue_world = format!("{}World!{}", Fg(Blue), Fg(Reset));
+        assert_iter_eq!(
+            AsciiSpace.find_words(&format!("{}{}", green_hello, blue_world)),
+            vec![Word::from(&green_hello), Word::from(&blue_world)]
+        );
+
+        #[cfg(feature = "unicode-linebreak")]
+        assert_iter_eq!(
+            UnicodeBreakProperties.find_words(&format!("{}{}", green_hello, blue_world)),
+            vec![Word::from(&green_hello), Word::from(&blue_world)]
+        );
+    }
+
+    #[test]
+    fn find_words_color_inside_word() {
+        let text = "foo\u{1b}[0m\u{1b}[32mbar\u{1b}[0mbaz";
+        assert_iter_eq!(AsciiSpace.find_words(&text), vec![Word::from(text)]);
+
+        #[cfg(feature = "unicode-linebreak")]
+        assert_iter_eq!(
+            UnicodeBreakProperties.find_words(&text),
+            vec![Word::from(text)]
+        );
+    }
+}
diff --git a/src/word_splitters.rs b/src/word_splitters.rs
new file mode 100644
index 0000000..69e246f
--- /dev/null
+++ b/src/word_splitters.rs
@@ -0,0 +1,314 @@
+//! Word splitting functionality.
+//!
+//! To wrap text into lines, long words sometimes need to be split
+//! across lines. The [`WordSplitter`] enum defines this
+//! functionality.
+
+use crate::core::{display_width, Word};
+
+/// The `WordSplitter` enum describes where words can be split.
+///
+/// If the textwrap crate has been compiled with the `hyphenation`
+/// Cargo feature enabled, you will find a
+/// [`WordSplitter::Hyphenation`] variant. Use this struct for
+/// language-aware hyphenation:
+///
+/// ```
+/// #[cfg(feature = "hyphenation")] {
+///     use hyphenation::{Language, Load, Standard};
+///     use textwrap::{wrap, Options, WordSplitter};
+///
+///     let text = "Oxidation is the loss of electrons.";
+///     let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap();
+///     let options = Options::new(8).word_splitter(WordSplitter::Hyphenation(dictionary));
+///     assert_eq!(wrap(text, &options), vec!["Oxida-",
+///                                           "tion is",
+///                                           "the loss",
+///                                           "of elec-",
+///                                           "trons."]);
+/// }
+/// ```
+///
+/// Please see the documentation for the [hyphenation] crate for more
+/// details.
+///
+/// [hyphenation]: https://docs.rs/hyphenation/
+#[derive(Clone)]
+pub enum WordSplitter {
+    /// Use this as a [`Options.word_splitter`] to avoid any kind of
+    /// hyphenation:
+    ///
+    /// ```
+    /// use textwrap::{wrap, Options, WordSplitter};
+    ///
+    /// let options = Options::new(8).word_splitter(WordSplitter::NoHyphenation);
+    /// assert_eq!(wrap("foo bar-baz", &options),
+    ///            vec!["foo", "bar-baz"]);
+    /// ```
+    ///
+    /// [`Options.word_splitter`]: super::Options::word_splitter
+    NoHyphenation,
+
+    /// `HyphenSplitter` is the default `WordSplitter` used by
+    /// [`Options::new`](super::Options::new). It will split words on
+    /// existing hyphens in the word.
+    ///
+    /// It will only use hyphens that are surrounded by alphanumeric
+    /// characters, which prevents a word like `"--foo-bar"` from
+    /// being split into `"--"` and `"foo-bar"`.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use textwrap::WordSplitter;
+    ///
+    /// assert_eq!(WordSplitter::HyphenSplitter.split_points("--foo-bar"),
+    ///            vec![6]);
+    /// ```
+    HyphenSplitter,
+
+    /// Use a custom function as the word splitter.
+    ///
+    /// This varian lets you implement a custom word splitter using
+    /// your own function.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use textwrap::WordSplitter;
+    ///
+    /// fn split_at_underscore(word: &str) -> Vec<usize> {
+    ///     word.match_indices('_').map(|(idx, _)| idx + 1).collect()
+    /// }
+    ///
+    /// let word_splitter = WordSplitter::Custom(split_at_underscore);
+    /// assert_eq!(word_splitter.split_points("a_long_identifier"),
+    ///            vec![2, 7]);
+    /// ```
+    Custom(fn(word: &str) -> Vec<usize>),
+
+    /// A hyphenation dictionary can be used to do language-specific
+    /// hyphenation using patterns from the [hyphenation] crate.
+    ///
+    /// **Note:** Only available when the `hyphenation` Cargo feature is
+    /// enabled.
+    ///
+    /// [hyphenation]: https://docs.rs/hyphenation/
+    #[cfg(feature = "hyphenation")]
+    Hyphenation(hyphenation::Standard),
+}
+
+impl std::fmt::Debug for WordSplitter {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            WordSplitter::NoHyphenation => f.write_str("NoHyphenation"),
+            WordSplitter::HyphenSplitter => f.write_str("HyphenSplitter"),
+            WordSplitter::Custom(_) => f.write_str("Custom(...)"),
+            #[cfg(feature = "hyphenation")]
+            WordSplitter::Hyphenation(dict) => write!(f, "Hyphenation({})", dict.language()),
+        }
+    }
+}
+
+impl PartialEq<WordSplitter> for WordSplitter {
+    fn eq(&self, other: &WordSplitter) -> bool {
+        match (self, other) {
+            (WordSplitter::NoHyphenation, WordSplitter::NoHyphenation) => true,
+            (WordSplitter::HyphenSplitter, WordSplitter::HyphenSplitter) => true,
+            #[cfg(feature = "hyphenation")]
+            (WordSplitter::Hyphenation(this_dict), WordSplitter::Hyphenation(other_dict)) => {
+                this_dict.language() == other_dict.language()
+            }
+            (_, _) => false,
+        }
+    }
+}
+
+impl WordSplitter {
+    /// Return all possible indices where `word` can be split.
+    ///
+    /// The indices are in the range `0..word.len()`. They point to
+    /// the index _after_ the split point, i.e., after `-` if
+    /// splitting on hyphens. This way, `word.split_at(idx)` will
+    /// break the word into two well-formed pieces.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use textwrap::WordSplitter;
+    /// assert_eq!(WordSplitter::NoHyphenation.split_points("cannot-be-split"), vec![]);
+    /// assert_eq!(WordSplitter::HyphenSplitter.split_points("can-be-split"), vec![4, 7]);
+    /// assert_eq!(WordSplitter::Custom(|word| vec![word.len()/2]).split_points("middle"), vec![3]);
+    /// ```
+    pub fn split_points(&self, word: &str) -> Vec<usize> {
+        match self {
+            WordSplitter::NoHyphenation => Vec::new(),
+            WordSplitter::HyphenSplitter => {
+                let mut splits = Vec::new();
+
+                for (idx, _) in word.match_indices('-') {
+                    // We only use hyphens that are surrounded by alphanumeric
+                    // characters. This is to avoid splitting on repeated hyphens,
+                    // such as those found in --foo-bar.
+                    let prev = word[..idx].chars().next_back();
+                    let next = word[idx + 1..].chars().next();
+
+                    if prev.filter(|ch| ch.is_alphanumeric()).is_some()
+                        && next.filter(|ch| ch.is_alphanumeric()).is_some()
+                    {
+                        splits.push(idx + 1); // +1 due to width of '-'.
+                    }
+                }
+
+                splits
+            }
+            WordSplitter::Custom(splitter_func) => splitter_func(word),
+            #[cfg(feature = "hyphenation")]
+            WordSplitter::Hyphenation(dictionary) => {
+                use hyphenation::Hyphenator;
+                dictionary.hyphenate(word).breaks
+            }
+        }
+    }
+}
+
+/// Split words into smaller words according to the split points given
+/// by `word_splitter`.
+///
+/// Note that we split all words, regardless of their length. This is
+/// to more cleanly separate the business of splitting (including
+/// automatic hyphenation) from the business of word wrapping.
+pub fn split_words<'a, I>(
+    words: I,
+    word_splitter: &'a WordSplitter,
+) -> impl Iterator<Item = Word<'a>>
+where
+    I: IntoIterator<Item = Word<'a>>,
+{
+    words.into_iter().flat_map(move |word| {
+        let mut prev = 0;
+        let mut split_points = word_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('-');
+                let w = Word {
+                    word: &word.word[prev..idx],
+                    width: display_width(&word[prev..idx]),
+                    whitespace: "",
+                    penalty: if need_hyphen { "-" } else { "" },
+                };
+                prev = idx;
+                return Some(w);
+            }
+
+            if prev < word.word.len() || prev == 0 {
+                let w = Word {
+                    word: &word.word[prev..],
+                    width: display_width(&word[prev..]),
+                    whitespace: word.whitespace,
+                    penalty: word.penalty,
+                };
+                prev = word.word.len() + 1;
+                return Some(w);
+            }
+
+            None
+        })
+    })
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    // Like assert_eq!, but the left expression is an iterator.
+    macro_rules! assert_iter_eq {
+        ($left:expr, $right:expr) => {
+            assert_eq!($left.collect::<Vec<_>>(), $right);
+        };
+    }
+
+    #[test]
+    fn split_words_no_words() {
+        assert_iter_eq!(split_words(vec![], &WordSplitter::HyphenSplitter), vec![]);
+    }
+
+    #[test]
+    fn split_words_empty_word() {
+        assert_iter_eq!(
+            split_words(vec![Word::from("   ")], &WordSplitter::HyphenSplitter),
+            vec![Word::from("   ")]
+        );
+    }
+
+    #[test]
+    fn split_words_single_word() {
+        assert_iter_eq!(
+            split_words(vec![Word::from("foobar")], &WordSplitter::HyphenSplitter),
+            vec![Word::from("foobar")]
+        );
+    }
+
+    #[test]
+    fn split_words_hyphen_splitter() {
+        assert_iter_eq!(
+            split_words(vec![Word::from("foo-bar")], &WordSplitter::HyphenSplitter),
+            vec![Word::from("foo-"), Word::from("bar")]
+        );
+    }
+
+    #[test]
+    fn split_words_no_hyphenation() {
+        assert_iter_eq!(
+            split_words(vec![Word::from("foo-bar")], &WordSplitter::NoHyphenation),
+            vec![Word::from("foo-bar")]
+        );
+    }
+
+    #[test]
+    fn split_words_adds_penalty() {
+        let fixed_split_point = |_: &str| vec![3];
+
+        assert_iter_eq!(
+            split_words(
+                vec![Word::from("foobar")].into_iter(),
+                &WordSplitter::Custom(fixed_split_point)
+            ),
+            vec![
+                Word {
+                    word: "foo",
+                    width: 3,
+                    whitespace: "",
+                    penalty: "-"
+                },
+                Word {
+                    word: "bar",
+                    width: 3,
+                    whitespace: "",
+                    penalty: ""
+                }
+            ]
+        );
+
+        assert_iter_eq!(
+            split_words(
+                vec![Word::from("fo-bar")].into_iter(),
+                &WordSplitter::Custom(fixed_split_point)
+            ),
+            vec![
+                Word {
+                    word: "fo-",
+                    width: 3,
+                    whitespace: "",
+                    penalty: ""
+                },
+                Word {
+                    word: "bar",
+                    width: 3,
+                    whitespace: "",
+                    penalty: ""
+                }
+            ]
+        );
+    }
+}
diff --git a/src/wrap_algorithms.rs b/src/wrap_algorithms.rs
new file mode 100644
index 0000000..5ca49c3
--- /dev/null
+++ b/src/wrap_algorithms.rs
@@ -0,0 +1,381 @@
+//! Word wrapping algorithms.
+//!
+//! After a text has been broken into words (or [`Fragment`]s), one
+//! now has to decide how to break the fragments into lines. The
+//! simplest algorithm for this is implemented by [`wrap_first_fit`]:
+//! it uses no look-ahead and simply adds fragments to the line as
+//! long as they fit. However, this can lead to poor line breaks if a
+//! large fragment almost-but-not-quite fits on a line. When that
+//! happens, the fragment is moved to the next line and it will leave
+//! behind a large gap. A more advanced algorithm, implemented by
+//! [`wrap_optimal_fit`], will take this into account. The optimal-fit
+//! algorithm considers all possible line breaks and will attempt to
+//! minimize the gaps left behind by overly short lines.
+//!
+//! While both algorithms run in linear time, the first-fit algorithm
+//! is about 4 times faster than the optimal-fit algorithm.
+
+#[cfg(feature = "smawk")]
+mod optimal_fit;
+#[cfg(feature = "smawk")]
+pub use optimal_fit::{wrap_optimal_fit, OverflowError, Penalties};
+
+use crate::core::{Fragment, Word};
+
+/// Describes how to wrap words into lines.
+///
+/// The simplest approach is to wrap words one word at a time and
+/// accept the first way of wrapping which fit
+/// ([`WrapAlgorithm::FirstFit`]). If the `smawk` Cargo feature is
+/// enabled, a more complex algorithm is available which will look at
+/// an entire paragraph at a time in order to find optimal line breaks
+/// ([`WrapAlgorithm::OptimalFit`]).
+#[derive(Clone, Copy)]
+pub enum WrapAlgorithm {
+    /// Wrap words using a fast and simple algorithm.
+    ///
+    /// This algorithm uses no look-ahead when finding line breaks.
+    /// Implemented by [`wrap_first_fit`], please see that function for
+    /// details and examples.
+    FirstFit,
+
+    /// Wrap words using an advanced algorithm with look-ahead.
+    ///
+    /// This wrapping algorithm considers the entire paragraph to find
+    /// optimal line breaks. When wrapping text, "penalties" are
+    /// assigned to line breaks based on the gaps left at the end of
+    /// lines. See [`Penalties`] for details.
+    ///
+    /// The underlying wrapping algorithm is implemented by
+    /// [`wrap_optimal_fit`], please see that function for examples.
+    ///
+    /// **Note:** Only available when the `smawk` Cargo feature is
+    /// enabled.
+    #[cfg(feature = "smawk")]
+    OptimalFit(Penalties),
+
+    /// Custom wrapping function.
+    ///
+    /// Use this if you want to implement your own wrapping algorithm.
+    /// The function can freely decide how to turn a slice of
+    /// [`Word`]s into lines.
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use textwrap::core::Word;
+    /// use textwrap::{wrap, Options, WrapAlgorithm};
+    ///
+    /// fn stair<'a, 'b>(words: &'b [Word<'a>], _: &'b [usize]) -> Vec<&'b [Word<'a>]> {
+    ///     let mut lines = Vec::new();
+    ///     let mut step = 1;
+    ///     let mut start_idx = 0;
+    ///     while start_idx + step <= words.len() {
+    ///       lines.push(&words[start_idx .. start_idx+step]);
+    ///       start_idx += step;
+    ///       step += 1;
+    ///     }
+    ///     lines
+    /// }
+    ///
+    /// let options = Options::new(10).wrap_algorithm(WrapAlgorithm::Custom(stair));
+    /// assert_eq!(wrap("First, second, third, fourth, fifth, sixth", options),
+    ///            vec!["First,",
+    ///                 "second, third,",
+    ///                 "fourth, fifth, sixth"]);
+    /// ```
+    Custom(for<'a, 'b> fn(words: &'b [Word<'a>], line_widths: &'b [usize]) -> Vec<&'b [Word<'a>]>),
+}
+
+impl std::fmt::Debug for WrapAlgorithm {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            WrapAlgorithm::FirstFit => f.write_str("FirstFit"),
+            #[cfg(feature = "smawk")]
+            WrapAlgorithm::OptimalFit(penalties) => write!(f, "OptimalFit({:?})", penalties),
+            WrapAlgorithm::Custom(_) => f.write_str("Custom(...)"),
+        }
+    }
+}
+
+impl WrapAlgorithm {
+    /// Create new wrap algorithm.
+    ///
+    /// The best wrapping algorithm is used by default, i.e.,
+    /// [`WrapAlgorithm::OptimalFit`] if available, otherwise
+    /// [`WrapAlgorithm::FirstFit`].
+    pub const fn new() -> Self {
+        #[cfg(not(feature = "smawk"))]
+        {
+            WrapAlgorithm::FirstFit
+        }
+
+        #[cfg(feature = "smawk")]
+        {
+            WrapAlgorithm::new_optimal_fit()
+        }
+    }
+
+    /// New [`WrapAlgorithm::OptimalFit`] with default penalties. This
+    /// works well for monospace text.
+    ///
+    /// **Note:** Only available when the `smawk` Cargo feature is
+    /// enabled.
+    #[cfg(feature = "smawk")]
+    pub const fn new_optimal_fit() -> Self {
+        WrapAlgorithm::OptimalFit(Penalties::new())
+    }
+
+    /// Wrap words according to line widths.
+    ///
+    /// The `line_widths` slice gives the target line width for each
+    /// line (the last slice element is repeated as necessary). This
+    /// can be used to implement hanging indentation.
+    #[inline]
+    pub fn wrap<'a, 'b>(
+        &self,
+        words: &'b [Word<'a>],
+        line_widths: &'b [usize],
+    ) -> Vec<&'b [Word<'a>]> {
+        // Every integer up to 2u64.pow(f64::MANTISSA_DIGITS) = 2**53
+        // = 9_007_199_254_740_992 can be represented without loss by
+        // a f64. Larger line widths will be rounded to the nearest
+        // representable number.
+        let f64_line_widths = line_widths.iter().map(|w| *w as f64).collect::<Vec<_>>();
+
+        match self {
+            WrapAlgorithm::FirstFit => wrap_first_fit(words, &f64_line_widths),
+
+            #[cfg(feature = "smawk")]
+            WrapAlgorithm::OptimalFit(penalties) => {
+                // The computation cannnot overflow when the line
+                // widths are restricted to usize.
+                wrap_optimal_fit(words, &f64_line_widths, penalties).unwrap()
+            }
+
+            WrapAlgorithm::Custom(func) => func(words, line_widths),
+        }
+    }
+}
+
+impl Default for WrapAlgorithm {
+    fn default() -> Self {
+        WrapAlgorithm::new()
+    }
+}
+
+/// Wrap abstract fragments into lines with a first-fit algorithm.
+///
+/// The `line_widths` slice gives the target line width for each line
+/// (the last slice element is repeated as necessary). This can be
+/// used to implement hanging indentation.
+///
+/// The fragments must already have been split into the desired
+/// widths, this function will not (and cannot) attempt to split them
+/// further when arranging them into lines.
+///
+/// # First-Fit Algorithm
+///
+/// This implements a simple “greedy” algorithm: accumulate fragments
+/// one by one and when a fragment no longer fits, start a new line.
+/// There is no look-ahead, we simply take first fit of the fragments
+/// we find.
+///
+/// While fast and predictable, this algorithm can produce poor line
+/// breaks when a long fragment is moved to a new line, leaving behind
+/// a large gap:
+///
+/// ```
+/// use textwrap::core::Word;
+/// use textwrap::wrap_algorithms::wrap_first_fit;
+/// use textwrap::WordSeparator;
+///
+/// // Helper to convert wrapped lines to a Vec<String>.
+/// fn lines_to_strings(lines: Vec<&[Word<'_>]>) -> Vec<String> {
+///     lines.iter().map(|line| {
+///         line.iter().map(|word| &**word).collect::<Vec<_>>().join(" ")
+///     }).collect::<Vec<_>>()
+/// }
+///
+/// let text = "These few words will unfortunately not wrap nicely.";
+/// let words = WordSeparator::AsciiSpace.find_words(text).collect::<Vec<_>>();
+/// assert_eq!(lines_to_strings(wrap_first_fit(&words, &[15.0])),
+///            vec!["These few words",
+///                 "will",  // <-- short line
+///                 "unfortunately",
+///                 "not wrap",
+///                 "nicely."]);
+///
+/// // We can avoid the short line if we look ahead:
+/// #[cfg(feature = "smawk")]
+/// use textwrap::wrap_algorithms::{wrap_optimal_fit, Penalties};
+/// #[cfg(feature = "smawk")]
+/// assert_eq!(lines_to_strings(wrap_optimal_fit(&words, &[15.0], &Penalties::new()).unwrap()),
+///            vec!["These few",
+///                 "words will",
+///                 "unfortunately",
+///                 "not wrap",
+///                 "nicely."]);
+/// ```
+///
+/// The [`wrap_optimal_fit`] function was used above to get better
+/// line breaks. It uses an advanced algorithm which tries to avoid
+/// short lines. This function is about 4 times faster than
+/// [`wrap_optimal_fit`].
+///
+/// # Examples
+///
+/// Imagine you're building a house site and you have a number of
+/// tasks you need to execute. Things like pour foundation, complete
+/// framing, install plumbing, electric cabling, install insulation.
+///
+/// The construction workers can only work during daytime, so they
+/// need to pack up everything at night. Because they need to secure
+/// their tools and move machines back to the garage, this process
+/// takes much more time than the time it would take them to simply
+/// switch to another task.
+///
+/// You would like to make a list of tasks to execute every day based
+/// on your estimates. You can model this with a program like this:
+///
+/// ```
+/// use textwrap::core::{Fragment, Word};
+/// use textwrap::wrap_algorithms::wrap_first_fit;
+///
+/// #[derive(Debug)]
+/// struct Task<'a> {
+///     name: &'a str,
+///     hours: f64,   // Time needed to complete task.
+///     sweep: f64,   // Time needed for a quick sweep after task during the day.
+///     cleanup: f64, // Time needed for full cleanup if day ends with this task.
+/// }
+///
+/// impl Fragment for Task<'_> {
+///     fn width(&self) -> f64 { self.hours }
+///     fn whitespace_width(&self) -> f64 { self.sweep }
+///     fn penalty_width(&self) -> f64 { self.cleanup }
+/// }
+///
+/// // The morning tasks
+/// let tasks = vec![
+///     Task { name: "Foundation",  hours: 4.0, sweep: 2.0, cleanup: 3.0 },
+///     Task { name: "Framing",     hours: 3.0, sweep: 1.0, cleanup: 2.0 },
+///     Task { name: "Plumbing",    hours: 2.0, sweep: 2.0, cleanup: 2.0 },
+///     Task { name: "Electrical",  hours: 2.0, sweep: 1.0, cleanup: 2.0 },
+///     Task { name: "Insulation",  hours: 2.0, sweep: 1.0, cleanup: 2.0 },
+///     Task { name: "Drywall",     hours: 3.0, sweep: 1.0, cleanup: 2.0 },
+///     Task { name: "Floors",      hours: 3.0, sweep: 1.0, cleanup: 2.0 },
+///     Task { name: "Countertops", hours: 1.0, sweep: 1.0, cleanup: 2.0 },
+///     Task { name: "Bathrooms",   hours: 2.0, sweep: 1.0, cleanup: 2.0 },
+/// ];
+///
+/// // Fill tasks into days, taking `day_length` into account. The
+/// // output shows the hours worked per day along with the names of
+/// // the tasks for that day.
+/// fn assign_days<'a>(tasks: &[Task<'a>], day_length: f64) -> Vec<(f64, Vec<&'a str>)> {
+///     let mut days = Vec::new();
+///     // Assign tasks to days. The assignment is a vector of slices,
+///     // with a slice per day.
+///     let assigned_days: Vec<&[Task<'a>]> = wrap_first_fit(&tasks, &[day_length]);
+///     for day in assigned_days.iter() {
+///         let last = day.last().unwrap();
+///         let work_hours: f64 = day.iter().map(|t| t.hours + t.sweep).sum();
+///         let names = day.iter().map(|t| t.name).collect::<Vec<_>>();
+///         days.push((work_hours - last.sweep + last.cleanup, names));
+///     }
+///     days
+/// }
+///
+/// // With a single crew working 8 hours a day:
+/// assert_eq!(
+///     assign_days(&tasks, 8.0),
+///     [
+///         (7.0, vec!["Foundation"]),
+///         (8.0, vec!["Framing", "Plumbing"]),
+///         (7.0, vec!["Electrical", "Insulation"]),
+///         (5.0, vec!["Drywall"]),
+///         (7.0, vec!["Floors", "Countertops"]),
+///         (4.0, vec!["Bathrooms"]),
+///     ]
+/// );
+///
+/// // With two crews working in shifts, 16 hours a day:
+/// assert_eq!(
+///     assign_days(&tasks, 16.0),
+///     [
+///         (14.0, vec!["Foundation", "Framing", "Plumbing"]),
+///         (15.0, vec!["Electrical", "Insulation", "Drywall", "Floors"]),
+///         (6.0, vec!["Countertops", "Bathrooms"]),
+///     ]
+/// );
+/// ```
+///
+/// Apologies to anyone who actually knows how to build a house and
+/// knows how long each step takes :-)
+pub fn wrap_first_fit<'a, 'b, T: Fragment>(
+    fragments: &'a [T],
+    line_widths: &'b [f64],
+) -> Vec<&'a [T]> {
+    // The final line width is used for all remaining lines.
+    let default_line_width = line_widths.last().copied().unwrap_or(0.0);
+    let mut lines = Vec::new();
+    let mut start = 0;
+    let mut width = 0.0;
+
+    for (idx, fragment) in fragments.iter().enumerate() {
+        let line_width = line_widths
+            .get(lines.len())
+            .copied()
+            .unwrap_or(default_line_width);
+        if width + fragment.width() + fragment.penalty_width() > line_width && idx > start {
+            lines.push(&fragments[start..idx]);
+            start = idx;
+            width = 0.0;
+        }
+        width += fragment.width() + fragment.whitespace_width();
+    }
+    lines.push(&fragments[start..]);
+    lines
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[derive(Debug, PartialEq)]
+    struct Word(f64);
+
+    #[rustfmt::skip]
+    impl Fragment for Word {
+        fn width(&self) -> f64 { self.0 }
+        fn whitespace_width(&self) -> f64 { 1.0 }
+        fn penalty_width(&self) -> f64 { 0.0 }
+    }
+
+    #[test]
+    fn wrap_string_longer_than_f64() {
+        let words = vec![
+            Word(1e307),
+            Word(2e307),
+            Word(3e307),
+            Word(4e307),
+            Word(5e307),
+            Word(6e307),
+        ];
+        // Wrap at just under f64::MAX (~19e307). The tiny
+        // whitespace_widths disappear because of loss of precision.
+        assert_eq!(
+            wrap_first_fit(&words, &[15e307]),
+            &[
+                vec![
+                    Word(1e307),
+                    Word(2e307),
+                    Word(3e307),
+                    Word(4e307),
+                    Word(5e307)
+                ],
+                vec![Word(6e307)]
+            ]
+        );
+    }
+}
diff --git a/src/wrap_algorithms/optimal_fit.rs b/src/wrap_algorithms/optimal_fit.rs
new file mode 100644
index 0000000..0625e28
--- /dev/null
+++ b/src/wrap_algorithms/optimal_fit.rs
@@ -0,0 +1,433 @@
+use std::cell::RefCell;
+
+use crate::core::Fragment;
+
+/// Penalties for
+/// [`WrapAlgorithm::OptimalFit`](crate::WrapAlgorithm::OptimalFit)
+/// and [`wrap_optimal_fit`].
+///
+/// This wrapping algorithm in [`wrap_optimal_fit`] considers the
+/// entire paragraph to find optimal line breaks. When wrapping text,
+/// "penalties" are assigned to line breaks based on the gaps left at
+/// the end of lines. The penalties are given by this struct, with
+/// [`Penalties::default`] assigning penalties that work well for
+/// monospace text.
+///
+/// If you are wrapping proportional text, you are advised to assign
+/// your own penalties according to your font size. See the individual
+/// penalties below for details.
+///
+/// **Note:** Only available when the `smawk` Cargo feature is
+/// enabled.
+#[derive(Clone, Copy, Debug)]
+pub struct Penalties {
+    /// Per-line penalty. This is added for every line, which makes it
+    /// expensive to output more lines than the minimum required.
+    pub nline_penalty: usize,
+
+    /// Per-character cost for lines that overflow the target line width.
+    ///
+    /// With a default value of 50², every single character costs as
+    /// much as leaving a gap of 50 characters behind. This is because
+    /// we assign as cost of `gap * gap` to a short line. When
+    /// wrapping monospace text, we can overflow the line by 1
+    /// character in extreme cases:
+    ///
+    /// ```
+    /// use textwrap::core::Word;
+    /// use textwrap::wrap_algorithms::{wrap_optimal_fit, Penalties};
+    ///
+    /// let short = "foo ";
+    /// let long = "x".repeat(50);
+    /// let length = (short.len() + long.len()) as f64;
+    /// let fragments = vec![Word::from(short), Word::from(&long)];
+    /// let penalties = Penalties::new();
+    ///
+    /// // Perfect fit, both words are on a single line with no overflow.
+    /// let wrapped = wrap_optimal_fit(&fragments, &[length], &penalties).unwrap();
+    /// assert_eq!(wrapped, vec![&[Word::from(short), Word::from(&long)]]);
+    ///
+    /// // The words no longer fit, yet we get a single line back. While
+    /// // the cost of overflow (`1 * 2500`) is the same as the cost of the
+    /// // gap (`50 * 50 = 2500`), the tie is broken by `nline_penalty`
+    /// // which makes it cheaper to overflow than to use two lines.
+    /// let wrapped = wrap_optimal_fit(&fragments, &[length - 1.0], &penalties).unwrap();
+    /// assert_eq!(wrapped, vec![&[Word::from(short), Word::from(&long)]]);
+    ///
+    /// // The cost of overflow would be 2 * 2500, whereas the cost of
+    /// // the gap is only `49 * 49 + nline_penalty = 2401 + 1000 =
+    /// // 3401`. We therefore get two lines.
+    /// let wrapped = wrap_optimal_fit(&fragments, &[length - 2.0], &penalties).unwrap();
+    /// assert_eq!(wrapped, vec![&[Word::from(short)],
+    ///                          &[Word::from(&long)]]);
+    /// ```
+    ///
+    /// This only happens if the overflowing word is 50 characters
+    /// long _and_ if the word overflows the line by exactly one
+    /// character. If it overflows by more than one character, the
+    /// overflow penalty will quickly outgrow the cost of the gap, as
+    /// seen above.
+    pub overflow_penalty: usize,
+
+    /// When should the a single word on the last line be considered
+    /// "too short"?
+    ///
+    /// If the last line of the text consist of a single word and if
+    /// this word is shorter than `1 / short_last_line_fraction` of
+    /// the line width, then the final line will be considered "short"
+    /// and `short_last_line_penalty` is added as an extra penalty.
+    ///
+    /// The effect of this is to avoid a final line consisting of a
+    /// single small word. For example, with a
+    /// `short_last_line_penalty` of 25 (the default), a gap of up to
+    /// 5 columns will be seen as more desirable than having a final
+    /// short line.
+    ///
+    /// ## Examples
+    ///
+    /// ```
+    /// use textwrap::{wrap, wrap_algorithms, Options, WrapAlgorithm};
+    ///
+    /// let text = "This is a demo of the short last line penalty.";
+    ///
+    /// // The first-fit algorithm leaves a single short word on the last line:
+    /// assert_eq!(wrap(text, Options::new(37).wrap_algorithm(WrapAlgorithm::FirstFit)),
+    ///            vec!["This is a demo of the short last line",
+    ///                 "penalty."]);
+    ///
+    /// #[cfg(feature = "smawk")] {
+    /// let mut penalties = wrap_algorithms::Penalties::new();
+    ///
+    /// // Since "penalty." is shorter than 25% of the line width, the
+    /// // optimal-fit algorithm adds a penalty of 25. This is enough
+    /// // to move "line " down:
+    /// assert_eq!(wrap(text, Options::new(37).wrap_algorithm(WrapAlgorithm::OptimalFit(penalties))),
+    ///            vec!["This is a demo of the short last",
+    ///                 "line penalty."]);
+    ///
+    /// // We can change the meaning of "short" lines. Here, only words
+    /// // shorter than 1/10th of the line width will be considered short:
+    /// penalties.short_last_line_fraction = 10;
+    /// assert_eq!(wrap(text, Options::new(37).wrap_algorithm(WrapAlgorithm::OptimalFit(penalties))),
+    ///            vec!["This is a demo of the short last line",
+    ///                 "penalty."]);
+    ///
+    /// // If desired, the penalty can also be disabled:
+    /// penalties.short_last_line_fraction = 4;
+    /// penalties.short_last_line_penalty = 0;
+    /// assert_eq!(wrap(text, Options::new(37).wrap_algorithm(WrapAlgorithm::OptimalFit(penalties))),
+    ///            vec!["This is a demo of the short last line",
+    ///                 "penalty."]);
+    /// }
+    /// ```
+    pub short_last_line_fraction: usize,
+
+    /// Penalty for a last line with a single short word.
+    ///
+    /// Set this to zero if you do not want to penalize short last lines.
+    pub short_last_line_penalty: usize,
+
+    /// Penalty for lines ending with a hyphen.
+    pub hyphen_penalty: usize,
+}
+
+impl Penalties {
+    /// Default penalties for monospace text.
+    ///
+    /// The penalties here work well for monospace text. This is
+    /// because they expect the gaps at the end of lines to be roughly
+    /// in the range `0..100`. If the gaps are larger, the
+    /// `overflow_penalty` and `hyphen_penalty` become insignificant.
+    pub const fn new() -> Self {
+        Penalties {
+            nline_penalty: 1000,
+            overflow_penalty: 50 * 50,
+            short_last_line_fraction: 4,
+            short_last_line_penalty: 25,
+            hyphen_penalty: 25,
+        }
+    }
+}
+
+impl Default for Penalties {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+/// Cache for line numbers. This is necessary to avoid a O(n**2)
+/// behavior when computing line numbers in [`wrap_optimal_fit`].
+struct LineNumbers {
+    line_numbers: RefCell<Vec<usize>>,
+}
+
+impl LineNumbers {
+    fn new(size: usize) -> Self {
+        let mut line_numbers = Vec::with_capacity(size);
+        line_numbers.push(0);
+        LineNumbers {
+            line_numbers: RefCell::new(line_numbers),
+        }
+    }
+
+    fn get<T>(&self, i: usize, minima: &[(usize, T)]) -> usize {
+        while self.line_numbers.borrow_mut().len() < i + 1 {
+            let pos = self.line_numbers.borrow().len();
+            let line_number = 1 + self.get(minima[pos].0, minima);
+            self.line_numbers.borrow_mut().push(line_number);
+        }
+
+        self.line_numbers.borrow()[i]
+    }
+}
+
+/// Overflow error during the [`wrap_optimal_fit`] computation.
+#[derive(Debug, PartialEq, Eq)]
+pub struct OverflowError;
+
+impl std::fmt::Display for OverflowError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "wrap_optimal_fit cost computation overflowed")
+    }
+}
+
+impl std::error::Error for OverflowError {}
+
+/// Wrap abstract fragments into lines with an optimal-fit algorithm.
+///
+/// The `line_widths` slice gives the target line width for each line
+/// (the last slice element is repeated as necessary). This can be
+/// used to implement hanging indentation.
+///
+/// The fragments must already have been split into the desired
+/// widths, this function will not (and cannot) attempt to split them
+/// further when arranging them into lines.
+///
+/// # Optimal-Fit Algorithm
+///
+/// The algorithm considers all possible break points and picks the
+/// breaks which minimizes the gaps at the end of each line. More
+/// precisely, the algorithm assigns a cost or penalty to each break
+/// point, determined by `cost = gap * gap` where `gap = target_width -
+/// line_width`. Shorter lines are thus penalized more heavily since
+/// they leave behind a larger gap.
+///
+/// We can illustrate this with the text “To be, or not to be: that is
+/// the question”. We will be wrapping it in a narrow column with room
+/// for only 10 characters. The [greedy
+/// algorithm](super::wrap_first_fit) will produce these lines, each
+/// annotated with the corresponding penalty:
+///
+/// ```text
+/// "To be, or"   1² =  1
+/// "not to be:"  0² =  0
+/// "that is"     3² =  9
+/// "the"         7² = 49
+/// "question"    2² =  4
+/// ```
+///
+/// We see that line four with “the” leaves a gap of 7 columns, which
+/// gives it a penalty of 49. The sum of the penalties is 63.
+///
+/// There are 10 words, which means that there are `2_u32.pow(9)` or
+/// 512 different ways to typeset it. We can compute
+/// the sum of the penalties for each possible line break and search
+/// for the one with the lowest sum:
+///
+/// ```text
+/// "To be,"     4² = 16
+/// "or not to"  1² =  1
+/// "be: that"   2² =  4
+/// "is the"     4² = 16
+/// "question"   2² =  4
+/// ```
+///
+/// The sum of the penalties is 41, which is better than what the
+/// greedy algorithm produced.
+///
+/// Searching through all possible combinations would normally be
+/// prohibitively slow. However, it turns out that the problem can be
+/// formulated as the task of finding column minima in a cost matrix.
+/// This matrix has a special form (totally monotone) which lets us
+/// use a [linear-time algorithm called
+/// SMAWK](https://lib.rs/crates/smawk) to find the optimal break
+/// points.
+///
+/// This means that the time complexity remains O(_n_) where _n_ is
+/// the number of words. Compared to
+/// [`wrap_first_fit`](super::wrap_first_fit), this function is about
+/// 4 times slower.
+///
+/// The optimization of per-line costs over the entire paragraph is
+/// inspired by the line breaking algorithm used in TeX, as described
+/// in the 1981 article [_Breaking Paragraphs into
+/// Lines_](http://www.eprg.org/G53DOC/pdfs/knuth-plass-breaking.pdf)
+/// by Knuth and Plass. The implementation here is based on [Python
+/// code by David
+/// Eppstein](https://github.com/jfinkels/PADS/blob/master/pads/wrap.py).
+///
+/// # Errors
+///
+/// In case of an overflow during the cost computation, an `Err` is
+/// returned. Overflows happens when fragments or lines have infinite
+/// widths (`f64::INFINITY`) or if the widths are so large that the
+/// gaps at the end of lines have sizes larger than `f64::MAX.sqrt()`
+/// (approximately 1e154):
+///
+/// ```
+/// use textwrap::core::Fragment;
+/// use textwrap::wrap_algorithms::{wrap_optimal_fit, OverflowError, Penalties};
+///
+/// #[derive(Debug, PartialEq)]
+/// struct Word(f64);
+///
+/// impl Fragment for Word {
+///     fn width(&self) -> f64 { self.0 }
+///     fn whitespace_width(&self) -> f64 { 1.0 }
+///     fn penalty_width(&self) -> f64 { 0.0 }
+/// }
+///
+/// // Wrapping overflows because 1e155 * 1e155 = 1e310, which is
+/// // larger than f64::MAX:
+/// assert_eq!(wrap_optimal_fit(&[Word(0.0), Word(0.0)], &[1e155], &Penalties::default()),
+///            Err(OverflowError));
+/// ```
+///
+/// When using fragment widths and line widths which fit inside an
+/// `u64`, overflows cannot happen. This means that fragments derived
+/// from a `&str` cannot cause overflows.
+///
+/// **Note:** Only available when the `smawk` Cargo feature is
+/// enabled.
+pub fn wrap_optimal_fit<'a, 'b, T: Fragment>(
+    fragments: &'a [T],
+    line_widths: &'b [f64],
+    penalties: &'b Penalties,
+) -> Result<Vec<&'a [T]>, OverflowError> {
+    // The final line width is used for all remaining lines.
+    let default_line_width = line_widths.last().copied().unwrap_or(0.0);
+    let mut widths = Vec::with_capacity(fragments.len() + 1);
+    let mut width = 0.0;
+    widths.push(width);
+    for fragment in fragments {
+        width += fragment.width() + fragment.whitespace_width();
+        widths.push(width);
+    }
+
+    let line_numbers = LineNumbers::new(fragments.len());
+
+    let minima = smawk::online_column_minima(0.0, widths.len(), |minima, i, j| {
+        // Line number for fragment `i`.
+        let line_number = line_numbers.get(i, minima);
+        let line_width = line_widths
+            .get(line_number)
+            .copied()
+            .unwrap_or(default_line_width);
+        let target_width = line_width.max(1.0);
+
+        // Compute the width of a line spanning fragments[i..j] in
+        // constant time. We need to adjust widths[j] by subtracting
+        // the whitespace of fragment[j-1] and then add the penalty.
+        let line_width = widths[j] - widths[i] - fragments[j - 1].whitespace_width()
+            + fragments[j - 1].penalty_width();
+
+        // We compute cost of the line containing fragments[i..j]. We
+        // start with values[i].1, which is the optimal cost for
+        // breaking before fragments[i].
+        //
+        // First, every extra line cost NLINE_PENALTY.
+        let mut cost = minima[i].1 + penalties.nline_penalty as f64;
+
+        // Next, we add a penalty depending on the line length.
+        if line_width > target_width {
+            // Lines that overflow get a hefty penalty.
+            let overflow = line_width - target_width;
+            cost += overflow * penalties.overflow_penalty as f64;
+        } else if j < fragments.len() {
+            // Other lines (except for the last line) get a milder
+            // penalty which depend on the size of the gap.
+            let gap = target_width - line_width;
+            cost += gap * gap;
+        } else if i + 1 == j
+            && line_width < target_width / penalties.short_last_line_fraction as f64
+        {
+            // The last line can have any size gap, but we do add a
+            // penalty if the line is very short (typically because it
+            // contains just a single word).
+            cost += penalties.short_last_line_penalty as f64;
+        }
+
+        // Finally, we discourage hyphens.
+        if fragments[j - 1].penalty_width() > 0.0 {
+            // TODO: this should use a penalty value from the fragment
+            // instead.
+            cost += penalties.hyphen_penalty as f64;
+        }
+
+        cost
+    });
+
+    for (_, cost) in &minima {
+        if cost.is_infinite() {
+            return Err(OverflowError);
+        }
+    }
+
+    let mut lines = Vec::with_capacity(line_numbers.get(fragments.len(), &minima));
+    let mut pos = fragments.len();
+    loop {
+        let prev = minima[pos].0;
+        lines.push(&fragments[prev..pos]);
+        pos = prev;
+        if pos == 0 {
+            break;
+        }
+    }
+
+    lines.reverse();
+    Ok(lines)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[derive(Debug, PartialEq)]
+    struct Word(f64);
+
+    #[rustfmt::skip]
+    impl Fragment for Word {
+        fn width(&self) -> f64 { self.0 }
+        fn whitespace_width(&self) -> f64 { 1.0 }
+        fn penalty_width(&self) -> f64 { 0.0 }
+    }
+
+    #[test]
+    fn wrap_fragments_with_infinite_widths() {
+        let words = vec![Word(f64::INFINITY)];
+        assert_eq!(
+            wrap_optimal_fit(&words, &[0.0], &Penalties::default()),
+            Err(OverflowError)
+        );
+    }
+
+    #[test]
+    fn wrap_fragments_with_huge_widths() {
+        let words = vec![Word(1e200), Word(1e250), Word(1e300)];
+        assert_eq!(
+            wrap_optimal_fit(&words, &[1e300], &Penalties::default()),
+            Err(OverflowError)
+        );
+    }
+
+    #[test]
+    fn wrap_fragments_with_large_widths() {
+        // The gaps will be of the sizes between 1e25 and 1e75. This
+        // makes the `gap * gap` cost fit comfortably in a f64.
+        let words = vec![Word(1e25), Word(1e50), Word(1e75)];
+        assert_eq!(
+            wrap_optimal_fit(&words, &[1e100], &Penalties::default()),
+            Ok(vec![&vec![Word(1e25), Word(1e50), Word(1e75)][..]])
+        );
+    }
+}