Repository: mgeisler/textwrap (language: rust)

Issue:
Support building without hyphenation support
As discussed in https://github.com/kbknapp/clap-rs/pull/845, the `hyphenation` crate pulls in a number of dependencies which might not be needed or wanted. It would be nice to have a Cargo feature flag to control this.
Support building without hyphenation support
As discussed in https://github.com/kbknapp/clap-rs/pull/845, the `hyphenation` crate pulls in a number of dependencies which might not be needed or wanted. It would be nice to have a Cargo feature flag to control this.


Discussion / hints:

Fix patch:
diff --git a/.travis.yml b/.travis.yml
index 9123db62..5c4b3a49 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -8,6 +8,14 @@ rust:
 
 cache: cargo
 
+script:
+  - cargo build --verbose --features "$FEATURES"
+  - cargo test --verbose --features "$FEATURES"
+
+env:
+  - FEATURES="hyphenation"
+  - FEATURES=""
+
 matrix:
   allow_failures:
     - rust: nightly
diff --git a/Cargo.toml b/Cargo.toml
index 3b3e034d..a8d76ea4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -16,4 +16,4 @@ appveyor = { repository = "mgeisler/textwrap" }
 
 [dependencies]
 unicode-width = "0.1.3"
-hyphenation = "0.6.1"
+hyphenation = { version = "0.6.1", optional = true }
diff --git a/README.md b/README.md
index 1681214c..76f56ad9 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,13 @@ and this to your crate root:
 extern crate textwrap;
 ```
 
+If you would like to have automatic hyphenation, specify the
+dependency as:
+```toml
+[dependencies]
+textwrap = { version: "0.4", features: ["hyphenation"] }
+```
+
 ## Documentation
 
 **[API documentation][api-docs]**
@@ -46,8 +53,9 @@ library for
 wrapping text.
 ```
 
-You can use automatic hyphenation using TeX hyphenation patterns (with
-support for [about 70 languages][patterns]) and get:
+With the `hyphenation` feature, you can get automatic hyphenation
+for [about 70 languages][patterns]. Your program must load and
+configure the hyphenation patterns to use:
 ```rust
 extern crate hyphenation;
 extern crate textwrap;
@@ -58,7 +66,7 @@ use textwrap::Wrapper;
 fn main() {
     let corpus = hyphenation::load(Language::English_US).unwrap();
     let mut wrapper = Wrapper::new(18);
-    wrapper.corpus = Some(&corpus);
+    wrapper.splitter = Box::new(corpus);
     let text = "textwrap: a small library for wrapping text.";
     println!("{}", wrapper.fill(text))
 }
@@ -71,13 +79,15 @@ library for wrap-
 ping text.
 ```
 
+The hyphenation uses high-quality TeX hyphenation patterns.
+
 ## Examples
 
 The library comes with a small example program that shows how a fixed
 example string is wrapped at different widths. Run the example with:
 
 ```shell
-$ cargo run --example layout
+$ cargo run --features hyphenation --example layout
 ```
 
 The program will use the following string:
diff --git a/benches/linear.rs b/benches/linear.rs
index b7786ef3..1077816e 100644
--- a/benches/linear.rs
+++ b/benches/linear.rs
@@ -4,11 +4,14 @@
 // where *n* is the size of the text to be wrapped.
 
 extern crate test;
+#[cfg(feature = "hyphenation")]
 extern crate hyphenation;
 extern crate textwrap;
 
 use test::Bencher;
+#[cfg(feature = "hyphenation")]
 use hyphenation::Language;
+#[cfg(feature = "hyphenation")]
 use textwrap::Wrapper;
 
 const LINE_LENGTH: usize = 60;
@@ -53,41 +56,45 @@ fn lorem_800(b: &mut Bencher) {
 }
 
 #[bench]
+#[cfg(feature = "hyphenation")]
 fn hyphenation_lorem_100(b: &mut Bencher) {
     let text = lorem_ipsum(100);
     let corpus = hyphenation::load(Language::Latin).unwrap();
     let mut wrapper = Wrapper::new(LINE_LENGTH);
-    wrapper.corpus = Some(&corpus);
+    wrapper.splitter = Box::new(corpus);
 
     b.iter(|| wrapper.fill(text))
 }
 
 #[bench]
+#[cfg(feature = "hyphenation")]
 fn hyphenation_lorem_200(b: &mut Bencher) {
     let text = lorem_ipsum(200);
     let corpus = hyphenation::load(Language::Latin).unwrap();
     let mut wrapper = Wrapper::new(LINE_LENGTH);
-    wrapper.corpus = Some(&corpus);
+    wrapper.splitter = Box::new(corpus);
 
     b.iter(|| wrapper.fill(text))
 }
 
 #[bench]
+#[cfg(feature = "hyphenation")]
 fn hyphenation_lorem_400(b: &mut Bencher) {
     let text = lorem_ipsum(400);
     let corpus = hyphenation::load(Language::Latin).unwrap();
     let mut wrapper = Wrapper::new(LINE_LENGTH);
-    wrapper.corpus = Some(&corpus);
+    wrapper.splitter = Box::new(corpus);
 
     b.iter(|| wrapper.fill(text))
 }
 
 #[bench]
+#[cfg(feature = "hyphenation")]
 fn hyphenation_lorem_800(b: &mut Bencher) {
     let text = lorem_ipsum(800);
     let corpus = hyphenation::load(Language::Latin).unwrap();
     let mut wrapper = Wrapper::new(LINE_LENGTH);
-    wrapper.corpus = Some(&corpus);
+    wrapper.splitter = Box::new(corpus);
 
     b.iter(|| wrapper.fill(text))
 }
diff --git a/examples/layout.rs b/examples/layout.rs
index 5ddcb88e..21c627b5 100644
--- a/examples/layout.rs
+++ b/examples/layout.rs
@@ -1,19 +1,32 @@
+#[cfg(feature = "hyphenation")]
 extern crate hyphenation;
 extern crate textwrap;
 
+#[cfg(feature = "hyphenation")]
 use hyphenation::Language;
 use textwrap::Wrapper;
 
+
+#[cfg(not(feature = "hyphenation"))]
+fn new_wrapper() -> Wrapper {
+    Wrapper::new(0)
+}
+
+#[cfg(feature = "hyphenation")]
+fn new_wrapper() -> Wrapper {
+    let mut wrapper = Wrapper::new(0);
+    wrapper.splitter = Box::new(hyphenation::load(Language::English_US).unwrap());
+    wrapper
+}
+
 fn main() {
-    let corpus = hyphenation::load(Language::English_US).unwrap();
     let example = "
 Memory safety without garbage collection.
 Concurrency without data races.
 Zero-cost abstractions.
 ";
     let mut prev_lines = vec![];
-    let mut wrapper = Wrapper::new(0);
-    wrapper.corpus = Some(&corpus);
+    let mut wrapper = new_wrapper();
     for width in 15..60 {
         wrapper.width = width;
         let lines = wrapper.wrap(example);
diff --git a/src/lib.rs b/src/lib.rs
index dc2ae4ef..5f3f7fd5 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -22,12 +22,82 @@
 
 
 extern crate unicode_width;
+#[cfg(feature = "hyphenation")]
 extern crate hyphenation;
 
 use unicode_width::UnicodeWidthStr;
 use unicode_width::UnicodeWidthChar;
-use hyphenation::Hyphenation;
-use hyphenation::Corpus;
+#[cfg(feature = "hyphenation")]
+use hyphenation::{Hyphenation, Corpus};
+
+pub trait WordSplitter {
+    /// Return all possible splits of word. Each split is a triple
+    /// with a head, a hyphen, and a tail where `head + &hyphen +
+    /// &tail == word`. The hyphen can be empty if there is already a
+    /// hyphen in the head.
+    ///
+    /// The splits should go from smallest to longest and should
+    /// include no split at all. So the word "technology" could be
+    /// split into
+    ///
+    /// ```
+    /// vec![("tech", "-", "nology"),
+    ///      ("technol", "-", "ogy"),
+    ///      ("technolo", "-", "gy"),
+    ///      ("technology", "", "")];
+    /// ```
+    fn split<'w>(&self, word: &'w str) -> Vec<(&'w str, &'w str, &'w str)>;
+}
+
+pub struct HyphenSplitter;
+
+/// HyphenSplitter is the default WordSplitter. As the name says, 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 on the first or second hyphen.
+impl WordSplitter for HyphenSplitter {
+    fn split<'w>(&self, word: &'w str) -> Vec<(&'w str, &'w str, &'w str)> {
+        let mut triples = Vec::new();
+        // Split on hyphens, smallest split first. 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 char_indices = word.char_indices().collect::<Vec<_>>();
+        for w in char_indices.windows(3) {
+            let ((_, prev), (n, c), (_, next)) = (w[0], w[1], w[2]);
+            if prev.is_alphanumeric() && c == '-' && next.is_alphanumeric() {
+                let (head, tail) = word.split_at(n + 1);
+                triples.push((head, "", tail));
+            }
+        }
+
+        // Finally option is no split at all.
+        triples.push((word, "", ""));
+
+        triples
+    }
+}
+
+/// A hyphenation Corpus can be used to do language-specific
+/// hyphenation using patterns from the hyphenation crate.
+#[cfg(feature = "hyphenation")]
+impl WordSplitter for Corpus {
+    fn split<'w>(&self, word: &'w str) -> Vec<(&'w str, &'w str, &'w str)> {
+        // Find splits based on language corpus.
+        let mut triples = Vec::new();
+        for n in word.opportunities(&self) {
+            let (head, tail) = word.split_at(n);
+            let hyphen = if head.ends_with('-') { "" } else { "-" };
+            triples.push((head, hyphen, tail));
+        }
+        // Finally option is no split at all.
+        triples.push((word, "", ""));
+
+        triples
+    }
+}
 
 /// A Wrapper holds settings for wrapping and filling text.
 ///
@@ -35,26 +105,25 @@ use hyphenation::Corpus;
 /// scan over words in the input string and splitting them into one or
 /// more lines. The time and memory complexity is O(*n*) where *n* is
 /// the length of the input string.
-pub struct Wrapper<'a> {
+pub struct Wrapper {
     /// The width in columns at which the text will be wrapped.
     pub width: usize,
     /// Allow long words to be broken if they cannot fit on a line.
     /// When set to false, some lines be being longer than self.width.
     pub break_words: bool,
-    /// The hyphenation corpus (if any) used for automatic
-    /// hyphenation.
-    pub corpus: Option<&'a Corpus>,
+    /// The method for splitting words, if any.
+    pub splitter: Box<WordSplitter>,
 }
 
-impl<'a> Wrapper<'a> {
+impl Wrapper {
     /// Create a new Wrapper for wrapping at the specified width. By
     /// default, we allow words longer than `width` to be broken. No
     /// hyphenation corpus is loaded by default.
-    pub fn new(width: usize) -> Wrapper<'a> {
-        Wrapper::<'a> {
+    pub fn new(width: usize) -> Wrapper {
+        Wrapper {
             width: width,
             break_words: true,
-            corpus: None,
+            splitter: Box::new(HyphenSplitter {}),
         }
     }
 
@@ -115,7 +184,7 @@ impl<'a> Wrapper<'a> {
 
             // If that failed, loop until nothing remains to be added.
             while !word.is_empty() {
-                let splits = self.split_word(word);
+                let splits = self.splitter.split(word);
                 let (smallest, hyphen, longest) = splits[0];
                 let min_width = smallest.width() + hyphen.len();
 
@@ -170,45 +239,6 @@ impl<'a> Wrapper<'a> {
         lines
     }
 
-    /// Split word into all possible (head, hyphen, tail) triples.
-    /// Word must be non-empty. The returned vector will always be
-    /// non-empty.
-    fn split_word<'b>(&self, word: &'b str) -> Vec<(&'b str, &'b str, &'b str)> {
-        let mut triples = Vec::new();
-
-        // Split on hyphens or use the language corpus.
-        match self.corpus {
-            None => {
-                // Split on hyphens, smallest split first. 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 char_indices = word.char_indices().collect::<Vec<_>>();
-                for w in char_indices.windows(3) {
-                    let ((_, prev), (n, c), (_, next)) = (w[0], w[1], w[2]);
-                    if prev.is_alphanumeric() && c == '-' && next.is_alphanumeric() {
-                        let (head, tail) = word.split_at(n + 1);
-                        triples.push((head, "", tail));
-                    }
-                }
-            }
-            Some(corpus) => {
-                // Find splits based on language corpus. This includes
-                // the splits that would have been found above.
-                for n in word.opportunities(corpus) {
-                    let (head, tail) = word.split_at(n);
-                    let hyphen = if head.ends_with('-') { "" } else { "-" };
-                    triples.push((head, hyphen, tail));
-                }
-            }
-        }
-
-        // Finally option is no split at all.
-        triples.push((word, "", ""));
-
-        triples
-    }
-
     /// Try to fit a word (or part of a word) onto a line. The line
     /// and the remaining width is updated as appropriate if the word
     /// or part fits.
@@ -371,8 +401,10 @@ pub fn dedent(s: &str) -> String {
 
 #[cfg(test)]
 mod tests {
+    #[cfg(feature = "hyphenation")]
     extern crate hyphenation;
 
+    #[cfg(feature = "hyphenation")]
     use hyphenation::Language;
     use super::*;
 
@@ -476,25 +508,27 @@ mod tests {
     }
 
     #[test]
+    #[cfg(feature = "hyphenation")]
     fn auto_hyphenation() {
         let corpus = hyphenation::load(Language::English_US).unwrap();
         let mut wrapper = Wrapper::new(10);
         assert_eq!(wrapper.wrap("Internationalization"),
                    vec!["Internatio", "nalization"]);
 
-        wrapper.corpus = Some(&corpus);
+        wrapper.splitter = Box::new(corpus);
         assert_eq!(wrapper.wrap("Internationalization"),
                    vec!["Interna-", "tionaliza-", "tion"]);
     }
 
     #[test]
+    #[cfg(feature = "hyphenation")]
     fn auto_hyphenation_with_hyphen() {
         let corpus = hyphenation::load(Language::English_US).unwrap();
         let mut wrapper = Wrapper::new(8);
         wrapper.break_words = false;
         assert_eq!(wrapper.wrap("over-caffinated"), vec!["over-", "caffinated"]);
 
-        wrapper.corpus = Some(&corpus);
+        wrapper.splitter = Box::new(corpus);
         assert_eq!(wrapper.wrap("over-caffinated"),
                    vec!["over-", "caffi-", "nated"]);
     }


Test patch: