diff options
author | Robin Krahl <robin.krahl@ireas.org> | 2020-01-07 11:18:04 +0000 |
---|---|---|
committer | Daniel Mueller <deso@posteo.net> | 2020-01-08 09:20:25 -0800 |
commit | 5e20a29b4fdc8a2d442d1093681b396dcb4b816b (patch) | |
tree | 55ab083fa8999d2ccbb5e921c1ffe52560dca152 /textwrap | |
parent | 203e691f46d591a2cc8acdfd850fa9f5b0fb8a98 (diff) | |
download | nitrocli-5e20a29b4fdc8a2d442d1093681b396dcb4b816b.tar.gz nitrocli-5e20a29b4fdc8a2d442d1093681b396dcb4b816b.tar.bz2 |
Add structopt dependency in version 0.3.7
This patch series replaces argparse with structopt in the argument
handling code. As a first step, we need structopt as a dependency.
Import subrepo structopt/:structopt at efbdda4753592e27bc430fb01f7b9650b2f3174d
Import subrepo bitflags/:bitflags at 30668016aca6bd3b02c766e8347e0b4080d4c296
Import subrepo clap/:clap at 784524f7eb193e35f81082cc69454c8c21b948f7
Import subrepo heck/:heck at 093d56fbf001e1506e56dbfa38631d99b1066df1
Import subrepo proc-macro-error/:proc-macro-error at 6c4cfe79a622c5de8ae68557993542be46eacae2
Import subrepo proc-macro2/:proc-macro2 at d5d48eddca4566e5438e8a2cbed4a74e049544de
Import subrepo quote/:quote at 727436c6c137b20f0f34dde5d8fda2679b9747ad
Import subrepo rustversion/:rustversion at 0c5663313516263059ce9059ef81fc7a1cf655ca
Import subrepo syn-mid/:syn-mid at 5d3d85414a9e6674e1857ec22a87b96e04a6851a
Import subrepo syn/:syn at e87c27e87f6f4ef8919d0372bdb056d53ef0d8f3
Import subrepo textwrap/:textwrap at abcd618beae3f74841032aa5b53c1086b0a57ca2
Import subrepo unicode-segmentation/:unicode-segmentation at 637c9874c4fe0c205ff27787faf150a40295c6c3
Import subrepo unicode-width/:unicode-width at 3033826f8bf05e82724140a981d5941e48fce393
Import subrepo unicode-xid/:unicode-xid at 4baae9fffb156ba229665b972a9cd5991787ceb7
Diffstat (limited to 'textwrap')
-rw-r--r-- | textwrap/.appveyor.yml | 23 | ||||
-rw-r--r-- | textwrap/.circleci/config.yml | 13 | ||||
-rw-r--r-- | textwrap/.codecov.yml | 13 | ||||
-rw-r--r-- | textwrap/.dir-locals.el | 3 | ||||
-rw-r--r-- | textwrap/.gitignore | 5 | ||||
-rw-r--r-- | textwrap/.travis.yml | 17 | ||||
-rw-r--r-- | textwrap/Cargo.toml | 38 | ||||
-rw-r--r-- | textwrap/LICENSE | 21 | ||||
-rw-r--r-- | textwrap/README.md | 337 | ||||
-rw-r--r-- | textwrap/benches/linear.rs | 122 | ||||
-rw-r--r-- | textwrap/examples/layout.rs | 38 | ||||
-rw-r--r-- | textwrap/examples/termwidth.rs | 41 | ||||
-rw-r--r-- | textwrap/src/indentation.rs | 294 | ||||
-rw-r--r-- | textwrap/src/lib.rs | 987 | ||||
-rw-r--r-- | textwrap/src/splitting.rs | 139 | ||||
-rw-r--r-- | textwrap/tests/version-numbers.rs | 17 |
16 files changed, 2108 insertions, 0 deletions
diff --git a/textwrap/.appveyor.yml b/textwrap/.appveyor.yml new file mode 100644 index 0000000..3555d73 --- /dev/null +++ b/textwrap/.appveyor.yml @@ -0,0 +1,23 @@ +environment: + matrix: + - TOOLCHAIN: stable + - TOOLCHAIN: nightly + +matrix: + allow_failures: + - TOOLCHAIN: nightly + +install: + - ps: Start-FileDownload 'https://static.rust-lang.org/rustup/dist/i686-pc-windows-gnu/rustup-init.exe' + - rustup-init.exe -y --default-toolchain %TOOLCHAIN% + - set PATH=%PATH%;%USERPROFILE%\.cargo\bin + +build_script: + - cargo build --verbose --all-features + +test_script: + - cargo test --verbose --all-features + +cache: + - '%USERPROFILE%\.cargo' + - target diff --git a/textwrap/.circleci/config.yml b/textwrap/.circleci/config.yml new file mode 100644 index 0000000..4f928a6 --- /dev/null +++ b/textwrap/.circleci/config.yml @@ -0,0 +1,13 @@ +version: 2 +jobs: + build: + docker: + - image: xd009642/tarpaulin + steps: + - checkout + - run: + name: Generate coverage report + command: cargo tarpaulin --out Xml --all-features + - run: + name: Upload to codecov.io + command: bash <(curl -s https://codecov.io/bash) -Z -f cobertura.xml diff --git a/textwrap/.codecov.yml b/textwrap/.codecov.yml new file mode 100644 index 0000000..a9b0bd2 --- /dev/null +++ b/textwrap/.codecov.yml @@ -0,0 +1,13 @@ +codecov: + # Do not wait for these CI providers since they will not upload any + # coverage reports. + ci: + - !appveyor + - !travis + +coverage: + status: + project: + default: + # Allow a 5% drop in overall project coverage on a PR. + threshold: 5% diff --git a/textwrap/.dir-locals.el b/textwrap/.dir-locals.el new file mode 100644 index 0000000..0faa664 --- /dev/null +++ b/textwrap/.dir-locals.el @@ -0,0 +1,3 @@ +((nil + (bug-reference-bug-regexp . "#\\(?2:[0-9]+\\)") + (bug-reference-url-format . "https://github.com/mgeisler/textwrap/issues/%s"))) diff --git a/textwrap/.gitignore b/textwrap/.gitignore new file mode 100644 index 0000000..6513848 --- /dev/null +++ b/textwrap/.gitignore @@ -0,0 +1,5 @@ +Cargo.lock +target/ + +*~ +*.orig diff --git a/textwrap/.travis.yml b/textwrap/.travis.yml new file mode 100644 index 0000000..156418b --- /dev/null +++ b/textwrap/.travis.yml @@ -0,0 +1,17 @@ +language: rust + +rust: + - stable + - beta + - nightly + - 1.22.0 + +cache: cargo + +script: + - cargo build --verbose --all-features + - cargo test --verbose --all-features + +matrix: + allow_failures: + - rust: nightly diff --git a/textwrap/Cargo.toml b/textwrap/Cargo.toml new file mode 100644 index 0000000..9d82ca5 --- /dev/null +++ b/textwrap/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "textwrap" +version = "0.11.0" +authors = ["Martin Geisler <martin@geisler.net>"] +description = """ +Textwrap is a small library for word wrapping, indenting, and +dedenting strings. + +You can use it to format strings (such as help and error messages) for +display in commandline applications. It is designed to be efficient +and handle Unicode characters correctly. +""" +documentation = "https://docs.rs/textwrap/" +repository = "https://github.com/mgeisler/textwrap" +readme = "README.md" +keywords = ["text", "formatting", "wrap", "typesetting", "hyphenation"] +categories = ["text-processing", "command-line-interface"] +license = "MIT" +exclude = [".dir-locals.el"] + +[package.metadata.docs.rs] +all-features = true + +[badges] +travis-ci = { repository = "mgeisler/textwrap" } +appveyor = { repository = "mgeisler/textwrap" } +codecov = { repository = "mgeisler/textwrap" } + +[dependencies] +unicode-width = "0.1.3" +term_size = { version = "0.3.0", optional = true } +hyphenation = { version = "0.7.1", optional = true, features = ["embed_all"] } + +[dev-dependencies] +lipsum = "0.6" +rand = "0.6" +rand_xorshift = "0.1" +version-sync = "0.6" diff --git a/textwrap/LICENSE b/textwrap/LICENSE new file mode 100644 index 0000000..0d37ec3 --- /dev/null +++ b/textwrap/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Martin Geisler + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/textwrap/README.md b/textwrap/README.md new file mode 100644 index 0000000..23a5439 --- /dev/null +++ b/textwrap/README.md @@ -0,0 +1,337 @@ +# Textwrap + +[![](https://img.shields.io/crates/v/textwrap.svg)][crates-io] +[![](https://docs.rs/textwrap/badge.svg)][api-docs] +[![](https://travis-ci.org/mgeisler/textwrap.svg?branch=master)][travis-ci] +[![](https://ci.appveyor.com/api/projects/status/github/mgeisler/textwrap?branch=master&svg=true)][appveyor] +[![](https://codecov.io/gh/mgeisler/textwrap/branch/master/graph/badge.svg)][codecov] + +Textwrap is a small Rust crate for word wrapping text. You can use it +to format strings for display in commandline applications. The crate +name and interface is inspired by +the [Python textwrap module][py-textwrap]. + +## Usage + +Add this to your `Cargo.toml`: +```toml +[dependencies] +textwrap = "0.11" +``` + +and this to your crate root: +```rust +extern crate textwrap; +``` + +If you would like to have automatic hyphenation, specify the +dependency as: +```toml +[dependencies] +textwrap = { version = "0.11", features = ["hyphenation"] } +``` + +To conveniently wrap text at the current terminal width, enable the +`term_size` feature: + +```toml +[dependencies] +textwrap = { version = "0.11", features = ["term_size"] } +``` + +## Documentation + +**[API documentation][api-docs]** + +## Getting Started + +Word wrapping single strings is easy using the `fill` function: +```rust +extern crate textwrap; +use textwrap::fill; + +fn main() { + let text = "textwrap: a small library for wrapping text."; + println!("{}", fill(text, 18)); +} +``` +The output is +``` +textwrap: a small +library for +wrapping text. +``` + +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; + +use hyphenation::{Language, Load, Standard}; +use textwrap::Wrapper; + +fn main() { + let hyphenator = Standard::from_embedded(Language::EnglishUS).unwrap(); + let wrapper = Wrapper::with_splitter(18, hyphenator); + let text = "textwrap: a small library for wrapping text."; + println!("{}", wrapper.fill(text)) +} +``` + +The output now looks like this: +``` +textwrap: a small +library for wrap- +ping text. +``` + +The hyphenation uses high-quality TeX hyphenation patterns. + +## Examples + +The library comes with some small example programs that shows various +features. + +### Layout Example + +The `layout` example shows how a fixed example string is wrapped at +different widths. Run the example with: + +```shell +$ cargo run --features hyphenation --example layout +``` + +The program will use the following string: + +> Memory safety without garbage collection. Concurrency without data +> races. Zero-cost abstractions. + +The string is wrapped at all widths between 15 and 60 columns. With +narrow columns the output looks like this: + +``` +.--- Width: 15 ---. +| Memory safety | +| without garbage | +| collection. | +| Concurrency | +| without data | +| races. Zero- | +| cost abstrac- | +| tions. | +.--- Width: 16 ----. +| Memory safety | +| without garbage | +| collection. Con- | +| currency without | +| data races. Ze- | +| ro-cost abstrac- | +| tions. | +``` + +Later, longer lines are used and the output now looks like this: + +``` +.-------------------- Width: 49 --------------------. +| Memory safety without garbage collection. Concur- | +| rency without data races. Zero-cost abstractions. | +.---------------------- Width: 53 ----------------------. +| Memory safety without garbage collection. Concurrency | +| without data races. Zero-cost abstractions. | +.------------------------- Width: 59 -------------------------. +| Memory safety without garbage collection. Concurrency with- | +| out data races. Zero-cost abstractions. | +``` + +Notice how words are split at hyphens (such as "zero-cost") but also +how words are hyphenated using automatic/machine hyphenation. + +### Terminal Width Example + +The `termwidth` example simply shows how the width can be set +automatically to the current terminal width. Run it with this command: + +``` +$ cargo run --example termwidth +``` + +If you run it in a narrow terminal, you'll see output like this: +``` +Formatted in within 60 columns: +---- +Memory safety without garbage collection. Concurrency +without data races. Zero-cost abstractions. +---- +``` + +If `stdout` is not connected to the terminal, the program will use a +default of 80 columns for the width: + +``` +$ cargo run --example termwidth | cat +Formatted in within 80 columns: +---- +Memory safety without garbage collection. Concurrency without data races. Zero- +cost abstractions. +---- +``` + +## Release History + +This section lists the largest changes per release. + +### Version 0.11.0 — December 9th, 2018 + +Due to our dependencies bumping their minimum supported version of +Rust, the minimum version of Rust we test against is now 1.22.0. + +* Merged [#141][issue-141]: Fix `dedent` handling of empty lines and + trailing newlines. Thanks @bbqsrc! +* Fixed [#151][issue-151]: Release of version with hyphenation 0.7. + +### Version 0.10.0 — April 28th, 2018 + +Due to our dependencies bumping their minimum supported version of +Rust, the minimum version of Rust we test against is now 1.17.0. + +* Fixed [#99][issue-99]: Word broken even though it would fit on line. +* Fixed [#107][issue-107]: Automatic hyphenation is off by one. +* Fixed [#122][issue-122]: Take newlines into account when wrapping. +* Fixed [#129][issue-129]: Panic on string with em-dash. + +### Version 0.9.0 — October 5th, 2017 + +The dependency on `term_size` is now optional, and by default this +feature is not enabled. This is a *breaking change* for users of +`Wrapper::with_termwidth`. Enable the `term_size` feature to restore +the old functionality. + +Added a regression test for the case where `width` is set to +`usize::MAX`, thanks @Fraser999! All public structs now implement +`Debug`, thanks @hcpl! + +* Fixed [#101][issue-101]: Make `term_size` an optional dependency. + +### Version 0.8.0 — September 4th, 2017 + +The `Wrapper` stuct is now generic over the type of word splitter +being used. This means less boxing and a nicer API. The +`Wrapper::word_splitter` method has been removed. This is a *breaking +API change* if you used the method to change the word splitter. + +The `Wrapper` struct has two new methods that will wrap the input text +lazily: `Wrapper::wrap_iter` and `Wrapper::into_wrap_iter`. Use those +if you will be iterating over the wrapped lines one by one. + +* Fixed [#59][issue-59]: `wrap` could return an iterator. Thanks + @hcpl! +* Fixed [#81][issue-81]: Set `html_root_url`. + +### Version 0.7.0 — July 20th, 2017 + +Version 0.7.0 changes the return type of `Wrapper::wrap` from +`Vec<String>` to `Vec<Cow<'a, str>>`. This means that the output lines +borrow data from the input string. This is a *breaking API change* if +you relied on the exact return type of `Wrapper::wrap`. Callers of the +`textwrap::fill` convenience function will see no breakage. + +The above change and other optimizations makes version 0.7.0 roughly +15-30% faster than version 0.6.0. + +The `squeeze_whitespace` option has been removed since it was +complicating the above optimization. Let us know if this option is +important for you so we can provide a work around. + +* Fixed [#58][issue-58]: Add a "fast_wrap" function. +* Fixed [#61][issue-61]: Documentation errors. + +### Version 0.6.0 — May 22nd, 2017 + +Version 0.6.0 adds builder methods to `Wrapper` for easy one-line +initialization and configuration: + +```rust +let wrapper = Wrapper::new(60).break_words(false); +``` + +It also add a new `NoHyphenation` word splitter that will never split +words, not even at existing hyphens. + +* Fixed [#28][issue-28]: Support not squeezing whitespace. + +### Version 0.5.0 — May 15th, 2017 + +Version 0.5.0 has *breaking API changes*. However, this only affects +code using the hyphenation feature. The feature is now optional, so +you will first need to enable the `hyphenation` feature as described +above. Afterwards, please change your code from +```rust +wrapper.corpus = Some(&corpus); +``` +to +```rust +wrapper.splitter = Box::new(corpus); +``` + +Other changes include optimizations, so version 0.5.0 is roughly +10-15% faster than version 0.4.0. + +* Fixed [#19][issue-19]: Add support for finding terminal size. +* Fixed [#25][issue-25]: Handle words longer than `self.width`. +* Fixed [#26][issue-26]: Support custom indentation. +* Fixed [#36][issue-36]: Support building without `hyphenation`. +* Fixed [#39][issue-39]: Respect non-breaking spaces. + +### Version 0.4.0 — January 24th, 2017 + +Documented complexities and tested these via `cargo bench`. + +* Fixed [#13][issue-13]: Immediatedly add word if it fits. +* Fixed [#14][issue-14]: Avoid splitting on initial hyphens. + +### Version 0.3.0 — January 7th, 2017 + +Added support for automatic hyphenation. + +### Version 0.2.0 — December 28th, 2016 + +Introduced `Wrapper` struct. Added support for wrapping on hyphens. + +### Version 0.1.0 — December 17th, 2016 + +First public release with support for wrapping strings on whitespace. + +## License + +Textwrap can be distributed according to the [MIT license][mit]. +Contributions will be accepted under the same license. + +[crates-io]: https://crates.io/crates/textwrap +[travis-ci]: https://travis-ci.org/mgeisler/textwrap +[appveyor]: https://ci.appveyor.com/project/mgeisler/textwrap +[codecov]: https://codecov.io/gh/mgeisler/textwrap +[py-textwrap]: https://docs.python.org/library/textwrap +[patterns]: https://github.com/tapeinosyne/hyphenation/tree/master/patterns-tex +[api-docs]: https://docs.rs/textwrap/ +[issue-13]: https://github.com/mgeisler/textwrap/issues/13 +[issue-14]: https://github.com/mgeisler/textwrap/issues/14 +[issue-19]: https://github.com/mgeisler/textwrap/issues/19 +[issue-25]: https://github.com/mgeisler/textwrap/issues/25 +[issue-26]: https://github.com/mgeisler/textwrap/issues/26 +[issue-28]: https://github.com/mgeisler/textwrap/issues/28 +[issue-36]: https://github.com/mgeisler/textwrap/issues/36 +[issue-39]: https://github.com/mgeisler/textwrap/issues/39 +[issue-58]: https://github.com/mgeisler/textwrap/issues/58 +[issue-59]: https://github.com/mgeisler/textwrap/issues/59 +[issue-61]: https://github.com/mgeisler/textwrap/issues/61 +[issue-81]: https://github.com/mgeisler/textwrap/issues/81 +[issue-99]: https://github.com/mgeisler/textwrap/issues/99 +[issue-101]: https://github.com/mgeisler/textwrap/issues/101 +[issue-107]: https://github.com/mgeisler/textwrap/issues/107 +[issue-122]: https://github.com/mgeisler/textwrap/issues/122 +[issue-129]: https://github.com/mgeisler/textwrap/issues/129 +[issue-141]: https://github.com/mgeisler/textwrap/issues/141 +[issue-151]: https://github.com/mgeisler/textwrap/issues/151 +[mit]: LICENSE diff --git a/textwrap/benches/linear.rs b/textwrap/benches/linear.rs new file mode 100644 index 0000000..104398a --- /dev/null +++ b/textwrap/benches/linear.rs @@ -0,0 +1,122 @@ +#![feature(test)] + +// The benchmarks here verify that the complexity grows as O(*n*) +// where *n* is the number of characters in the text to be wrapped. + +#[cfg(feature = "hyphenation")] +extern crate hyphenation; +extern crate lipsum; +extern crate rand; +extern crate rand_xorshift; +extern crate test; +extern crate textwrap; + +#[cfg(feature = "hyphenation")] +use hyphenation::{Language, Load, Standard}; +use lipsum::MarkovChain; +use rand::SeedableRng; +use rand_xorshift::XorShiftRng; +use test::Bencher; +#[cfg(feature = "hyphenation")] +use textwrap::Wrapper; + +const LINE_LENGTH: usize = 60; + +/// Generate a lorem ipsum text with the given number of characters. +fn lorem_ipsum(length: usize) -> String { + // The average word length in the lorem ipsum text is somewhere + // between 6 and 7. So we conservatively divide by 5 to have a + // long enough text that we can truncate below. + let rng = XorShiftRng::seed_from_u64(0); + let mut chain = MarkovChain::new_with_rng(rng); + chain.learn(lipsum::LOREM_IPSUM); + chain.learn(lipsum::LIBER_PRIMUS); + + let mut text = chain.generate_from(length / 5, ("Lorem", "ipsum")); + text.truncate(length); + text +} + +#[bench] +fn fill_100(b: &mut Bencher) { + let text = &lorem_ipsum(100); + b.iter(|| textwrap::fill(text, LINE_LENGTH)) +} + +#[bench] +fn fill_200(b: &mut Bencher) { + let text = &lorem_ipsum(200); + b.iter(|| textwrap::fill(text, LINE_LENGTH)) +} + +#[bench] +fn fill_400(b: &mut Bencher) { + let text = &lorem_ipsum(400); + b.iter(|| textwrap::fill(text, LINE_LENGTH)) +} + +#[bench] +fn fill_800(b: &mut Bencher) { + let text = &lorem_ipsum(800); + b.iter(|| textwrap::fill(text, LINE_LENGTH)) +} + +#[bench] +fn wrap_100(b: &mut Bencher) { + let text = &lorem_ipsum(100); + b.iter(|| textwrap::wrap(text, LINE_LENGTH)) +} + +#[bench] +fn wrap_200(b: &mut Bencher) { + let text = &lorem_ipsum(200); + b.iter(|| textwrap::wrap(text, LINE_LENGTH)) +} + +#[bench] +fn wrap_400(b: &mut Bencher) { + let text = &lorem_ipsum(400); + b.iter(|| textwrap::wrap(text, LINE_LENGTH)) +} + +#[bench] +fn wrap_800(b: &mut Bencher) { + let text = &lorem_ipsum(800); + b.iter(|| textwrap::wrap(text, LINE_LENGTH)) +} + +#[bench] +#[cfg(feature = "hyphenation")] +fn hyphenation_fill_100(b: &mut Bencher) { + let text = &lorem_ipsum(100); + let dictionary = Standard::from_embedded(Language::Latin).unwrap(); + let wrapper = Wrapper::with_splitter(LINE_LENGTH, dictionary); + b.iter(|| wrapper.fill(text)) +} + +#[bench] +#[cfg(feature = "hyphenation")] +fn hyphenation_fill_200(b: &mut Bencher) { + let text = &lorem_ipsum(200); + let dictionary = Standard::from_embedded(Language::Latin).unwrap(); + let wrapper = Wrapper::with_splitter(LINE_LENGTH, dictionary); + b.iter(|| wrapper.fill(text)) +} + +#[bench] +#[cfg(feature = "hyphenation")] +fn hyphenation_fill_400(b: &mut Bencher) { + let text = &lorem_ipsum(400); + let dictionary = Standard::from_embedded(Language::Latin).unwrap(); + let wrapper = Wrapper::with_splitter(LINE_LENGTH, dictionary); + b.iter(|| wrapper.fill(text)) +} + +#[bench] +#[cfg(feature = "hyphenation")] +fn hyphenation_fill_800(b: &mut Bencher) { + let text = &lorem_ipsum(800); + let dictionary = Standard::from_embedded(Language::Latin).unwrap(); + let wrapper = Wrapper::with_splitter(LINE_LENGTH, dictionary); + b.iter(|| wrapper.fill(text)) +} diff --git a/textwrap/examples/layout.rs b/textwrap/examples/layout.rs new file mode 100644 index 0000000..d36cb3a --- /dev/null +++ b/textwrap/examples/layout.rs @@ -0,0 +1,38 @@ +#[cfg(feature = "hyphenation")] +extern crate hyphenation; +extern crate textwrap; + +#[cfg(feature = "hyphenation")] +use hyphenation::{Language, Load}; +use textwrap::Wrapper; + +#[cfg(not(feature = "hyphenation"))] +fn new_wrapper<'a>() -> Wrapper<'a, textwrap::HyphenSplitter> { + Wrapper::new(0) +} + +#[cfg(feature = "hyphenation")] +fn new_wrapper<'a>() -> Wrapper<'a, hyphenation::Standard> { + let dictionary = hyphenation::Standard::from_embedded(Language::EnglishUS).unwrap(); + Wrapper::with_splitter(0, dictionary) +} + +fn main() { + let example = "Memory safety without garbage collection. \ + Concurrency without data races. \ + Zero-cost abstractions."; + let mut prev_lines = vec![]; + let mut wrapper = new_wrapper(); + for width in 15..60 { + wrapper.width = width; + let lines = wrapper.wrap(example); + if lines != prev_lines { + let title = format!(" Width: {} ", width); + println!(".{:-^1$}.", title, width + 2); + for line in &lines { + println!("| {:1$} |", line, width); + } + prev_lines = lines; + } + } +} diff --git a/textwrap/examples/termwidth.rs b/textwrap/examples/termwidth.rs new file mode 100644 index 0000000..75db3aa --- /dev/null +++ b/textwrap/examples/termwidth.rs @@ -0,0 +1,41 @@ +#[cfg(feature = "hyphenation")] +extern crate hyphenation; +extern crate textwrap; + +#[cfg(feature = "hyphenation")] +use hyphenation::{Language, Load, Standard}; +#[cfg(feature = "term_size")] +use textwrap::Wrapper; + +#[cfg(not(feature = "term_size"))] +fn main() { + println!("Please enable the term_size feature to run this example."); +} + +#[cfg(feature = "term_size")] +fn main() { + #[cfg(not(feature = "hyphenation"))] + fn new_wrapper<'a>() -> (&'static str, Wrapper<'a, textwrap::HyphenSplitter>) { + ("without hyphenation", Wrapper::with_termwidth()) + } + + #[cfg(feature = "hyphenation")] + fn new_wrapper<'a>() -> (&'static str, Wrapper<'a, Standard>) { + let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap(); + ( + "with hyphenation", + Wrapper::with_splitter(textwrap::termwidth(), dictionary), + ) + } + + let example = "Memory safety without garbage collection. \ + Concurrency without data races. \ + Zero-cost abstractions."; + // Create a new Wrapper -- automatically set the width to the + // current terminal width. + let (msg, wrapper) = new_wrapper(); + println!("Formatted {} in {} columns:", msg, wrapper.width); + println!("----"); + println!("{}", wrapper.fill(example)); + println!("----"); +} diff --git a/textwrap/src/indentation.rs b/textwrap/src/indentation.rs new file mode 100644 index 0000000..276ba10 --- /dev/null +++ b/textwrap/src/indentation.rs @@ -0,0 +1,294 @@ +//! Functions related to adding and removing indentation from lines of +//! text. +//! +//! The functions here can be used to uniformly indent or dedent +//! (unindent) word wrapped lines of text. + +/// Add prefix to each non-empty line. +/// +/// ``` +/// use textwrap::indent; +/// +/// assert_eq!(indent(" +/// Foo +/// Bar +/// ", " "), " +/// Foo +/// Bar +/// "); +/// ``` +/// +/// Empty lines (lines consisting only of whitespace) are not indented +/// and the whitespace is replaced by a single newline (`\n`): +/// +/// ``` +/// use textwrap::indent; +/// +/// assert_eq!(indent(" +/// Foo +/// +/// Bar +/// \t +/// Baz +/// ", "->"), " +/// ->Foo +/// +/// ->Bar +/// +/// ->Baz +/// "); +/// ``` +/// +/// Leading and trailing whitespace on non-empty lines is kept +/// unchanged: +/// +/// ``` +/// use textwrap::indent; +/// +/// assert_eq!(indent(" \t Foo ", "->"), "-> \t Foo \n"); +/// ``` +pub fn indent(s: &str, prefix: &str) -> String { + let mut result = String::new(); + for line in s.lines() { + if line.chars().any(|c| !c.is_whitespace()) { + result.push_str(prefix); + result.push_str(line); + } + result.push('\n'); + } + result +} + +/// Removes common leading whitespace from each line. +/// +/// This function will look at each non-empty line and determine the +/// maximum amount of whitespace that can be removed from all lines: +/// +/// ``` +/// use textwrap::dedent; +/// +/// assert_eq!(dedent(" +/// 1st line +/// 2nd line +/// 3rd line +/// "), " +/// 1st line +/// 2nd line +/// 3rd line +/// "); +/// ``` +pub fn dedent(s: &str) -> String { + let mut prefix = ""; + let mut lines = s.lines(); + + // We first search for a non-empty line to find a prefix. + for line in &mut lines { + let mut whitespace_idx = line.len(); + for (idx, ch) in line.char_indices() { + if !ch.is_whitespace() { + whitespace_idx = idx; + break; + } + } + + // Check if the line had anything but whitespace + if whitespace_idx < line.len() { + prefix = &line[..whitespace_idx]; + break; + } + } + + // We then continue looking through the remaining lines to + // possibly shorten the prefix. + for line in &mut lines { + let mut whitespace_idx = line.len(); + for ((idx, a), b) in line.char_indices().zip(prefix.chars()) { + if a != b { + whitespace_idx = idx; + break; + } + } + + // Check if the line had anything but whitespace and if we + // have found a shorter prefix + if whitespace_idx < line.len() && whitespace_idx < prefix.len() { + prefix = &line[..whitespace_idx]; + } + } + + // We now go over the lines a second time to build the result. + let mut result = String::new(); + for line in s.lines() { + if line.starts_with(&prefix) && line.chars().any(|c| !c.is_whitespace()) { + let (_, tail) = line.split_at(prefix.len()); + result.push_str(tail); + } + result.push('\n'); + } + + if result.ends_with('\n') && !s.ends_with('\n') { + let new_len = result.len() - 1; + result.truncate(new_len); + } + + result +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Add newlines. Ensures that the final line in the vector also + /// has a newline. + fn add_nl(lines: &[&str]) -> String { + lines.join("\n") + "\n" + } + + #[test] + fn indent_empty() { + assert_eq!(indent("\n", " "), "\n"); + } + + #[test] + #[cfg_attr(rustfmt, rustfmt_skip)] + fn indent_nonempty() { + let x = vec![" foo", + "bar", + " baz"]; + let y = vec!["// foo", + "//bar", + "// baz"]; + assert_eq!(indent(&add_nl(&x), "//"), add_nl(&y)); + } + + #[test] + #[cfg_attr(rustfmt, rustfmt_skip)] + fn indent_empty_line() { + let x = vec![" foo", + "bar", + "", + " baz"]; + let y = vec!["// foo", + "//bar", + "", + "// baz"]; + assert_eq!(indent(&add_nl(&x), "//"), add_nl(&y)); + } + + #[test] + fn dedent_empty() { + assert_eq!(dedent(""), ""); + } + + #[test] + #[cfg_attr(rustfmt, rustfmt_skip)] + fn dedent_multi_line() { + let x = vec![" foo", + " bar", + " baz"]; + let y = vec![" foo", + "bar", + " baz"]; + assert_eq!(dedent(&add_nl(&x)), add_nl(&y)); + } + + #[test] + #[cfg_attr(rustfmt, rustfmt_skip)] + fn dedent_empty_line() { + let x = vec![" foo", + " bar", + " ", + " baz"]; + let y = vec![" foo", + "bar", + "", + " baz"]; + assert_eq!(dedent(&add_nl(&x)), add_nl(&y)); + } + + #[test] + #[cfg_attr(rustfmt, rustfmt_skip)] + fn dedent_blank_line() { + let x = vec![" foo", + "", + " bar", + " foo", + " bar", + " baz"]; + let y = vec!["foo", + "", + " bar", + " foo", + " bar", + " baz"]; + assert_eq!(dedent(&add_nl(&x)), add_nl(&y)); + } + + #[test] + #[cfg_attr(rustfmt, rustfmt_skip)] + fn dedent_whitespace_line() { + let x = vec![" foo", + " ", + " bar", + " foo", + " bar", + " baz"]; + let y = vec!["foo", + "", + " bar", + " foo", + " bar", + " baz"]; + assert_eq!(dedent(&add_nl(&x)), add_nl(&y)); + } + + #[test] + #[cfg_attr(rustfmt, rustfmt_skip)] + fn dedent_mixed_whitespace() { + let x = vec!["\tfoo", + " bar"]; + let y = vec!["\tfoo", + " bar"]; + assert_eq!(dedent(&add_nl(&x)), add_nl(&y)); + } + + #[test] + #[cfg_attr(rustfmt, rustfmt_skip)] + fn dedent_tabbed_whitespace() { + let x = vec!["\t\tfoo", + "\t\t\tbar"]; + let y = vec!["foo", + "\tbar"]; + assert_eq!(dedent(&add_nl(&x)), add_nl(&y)); + } + + #[test] + #[cfg_attr(rustfmt, rustfmt_skip)] + fn dedent_mixed_tabbed_whitespace() { + let x = vec!["\t \tfoo", + "\t \t\tbar"]; + let y = vec!["foo", + "\tbar"]; + assert_eq!(dedent(&add_nl(&x)), add_nl(&y)); + } + + #[test] + #[cfg_attr(rustfmt, rustfmt_skip)] + fn dedent_mixed_tabbed_whitespace2() { + let x = vec!["\t \tfoo", + "\t \tbar"]; + let y = vec!["\tfoo", + " \tbar"]; + assert_eq!(dedent(&add_nl(&x)), add_nl(&y)); + } + + #[test] + #[cfg_attr(rustfmt, rustfmt_skip)] + fn dedent_preserve_no_terminating_newline() { + let x = vec![" foo", + " bar"].join("\n"); + let y = vec!["foo", + " bar"].join("\n"); + assert_eq!(dedent(&x), y); + } +} diff --git a/textwrap/src/lib.rs b/textwrap/src/lib.rs new file mode 100644 index 0000000..2f82325 --- /dev/null +++ b/textwrap/src/lib.rs @@ -0,0 +1,987 @@ +//! `textwrap` provides functions for word wrapping and filling text. +//! +//! Wrapping text can be very useful in commandline programs where you +//! want to format dynamic output nicely so it looks good in a +//! terminal. A quick example: +//! +//! ```no_run +//! extern crate textwrap; +//! use textwrap::fill; +//! +//! fn main() { +//! let text = "textwrap: a small library for wrapping text."; +//! println!("{}", fill(text, 18)); +//! } +//! ``` +//! +//! This will display the following output: +//! +//! ```text +//! textwrap: a small +//! library for +//! wrapping text. +//! ``` +//! +//! # Displayed Width vs Byte Size +//! +//! To word wrap text, one must know the width of each word so one can +//! know when to break lines. This library measures the width of text +//! using the [displayed width][unicode-width], not the size in bytes. +//! +//! This is important for non-ASCII text. ASCII characters such as `a` +//! and `!` are simple and take up one column each. This means that +//! the displayed width is equal to the string length in bytes. +//! However, non-ASCII characters and symbols take up more than one +//! byte when UTF-8 encoded: `é` is `0xc3 0xa9` (two bytes) and `⚙` is +//! `0xe2 0x9a 0x99` (three bytes) in UTF-8, respectively. +//! +//! This is why we take care to use the displayed width instead of the +//! byte count when computing line lengths. All functions in this +//! library handle Unicode characters like this. +//! +//! [unicode-width]: https://docs.rs/unicode-width/ + +#![doc(html_root_url = "https://docs.rs/textwrap/0.11.0")] +#![deny(missing_docs)] +#![deny(missing_debug_implementations)] + +#[cfg(feature = "hyphenation")] +extern crate hyphenation; +#[cfg(feature = "term_size")] +extern crate term_size; +extern crate unicode_width; + +use std::borrow::Cow; +use std::str::CharIndices; + +use unicode_width::UnicodeWidthChar; +use unicode_width::UnicodeWidthStr; + +/// A non-breaking space. +const NBSP: char = '\u{a0}'; + +mod indentation; +pub use indentation::dedent; +pub use indentation::indent; + +mod splitting; +pub use splitting::{HyphenSplitter, NoHyphenation, WordSplitter}; + +/// A Wrapper holds settings for wrapping and filling text. Use it +/// when the convenience [`wrap_iter`], [`wrap`] and [`fill`] functions +/// are not flexible enough. +/// +/// [`wrap_iter`]: fn.wrap_iter.html +/// [`wrap`]: fn.wrap.html +/// [`fill`]: fn.fill.html +/// +/// The algorithm used by the `WrapIter` iterator (returned from the +/// `wrap_iter` method) works by doing successive partial scans over +/// words in the input string (where each single scan yields a single +/// line) so that the overall time and memory complexity is O(*n*) where +/// *n* is the length of the input string. +#[derive(Clone, Debug)] +pub struct Wrapper<'a, S: WordSplitter> { + /// The width in columns at which the text will be wrapped. + pub width: usize, + /// Indentation used for the first line of output. + pub initial_indent: &'a str, + /// Indentation used for subsequent lines of output. + pub subsequent_indent: &'a str, + /// Allow long words to be broken if they cannot fit on a line. + /// When set to `false`, some lines may be longer than + /// `self.width`. + pub break_words: bool, + /// The method for splitting words. If the `hyphenation` feature + /// is enabled, you can use a `hyphenation::Standard` dictionary + /// here to get language-aware hyphenation. + pub splitter: S, +} + +impl<'a> Wrapper<'a, HyphenSplitter> { + /// Create a new Wrapper for wrapping at the specified width. By + /// default, we allow words longer than `width` to be broken. A + /// [`HyphenSplitter`] will be used by default for splitting + /// words. See the [`WordSplitter`] trait for other options. + /// + /// [`HyphenSplitter`]: struct.HyphenSplitter.html + /// [`WordSplitter`]: trait.WordSplitter.html + pub fn new(width: usize) -> Wrapper<'a, HyphenSplitter> { + Wrapper::with_splitter(width, HyphenSplitter) + } + + /// Create a new Wrapper for wrapping text at the current terminal + /// width. If the terminal width cannot be determined (typically + /// because the standard input and output is not connected to a + /// terminal), a width of 80 characters will be used. Other + /// settings use the same defaults as `Wrapper::new`. + /// + /// Equivalent to: + /// + /// ```no_run + /// # #![allow(unused_variables)] + /// use textwrap::{Wrapper, termwidth}; + /// + /// let wrapper = Wrapper::new(termwidth()); + /// ``` + #[cfg(feature = "term_size")] + pub fn with_termwidth() -> Wrapper<'a, HyphenSplitter> { + Wrapper::new(termwidth()) + } +} + +impl<'a, S: WordSplitter> Wrapper<'a, S> { + /// Use the given [`WordSplitter`] to create a new Wrapper for + /// wrapping at the specified width. By default, we allow words + /// longer than `width` to be broken. + /// + /// [`WordSplitter`]: trait.WordSplitter.html + pub fn with_splitter(width: usize, splitter: S) -> Wrapper<'a, S> { + Wrapper { + width: width, + initial_indent: "", + subsequent_indent: "", + break_words: true, + splitter: splitter, + } + } + + /// Change [`self.initial_indent`]. The initial indentation is + /// used on the very first line of output. + /// + /// # Examples + /// + /// Classic paragraph indentation can be achieved by specifying an + /// initial indentation and wrapping each paragraph by itself: + /// + /// ```no_run + /// # #![allow(unused_variables)] + /// use textwrap::Wrapper; + /// + /// let wrapper = Wrapper::new(15).initial_indent(" "); + /// ``` + /// + /// [`self.initial_indent`]: #structfield.initial_indent + pub fn initial_indent(self, indent: &'a str) -> Wrapper<'a, S> { + Wrapper { + initial_indent: indent, + ..self + } + } + + /// Change [`self.subsequent_indent`]. The subsequent indentation + /// is used on lines following the first line of output. + /// + /// # Examples + /// + /// Combining initial and subsequent indentation lets you format a + /// single paragraph as a bullet list: + /// + /// ```no_run + /// # #![allow(unused_variables)] + /// use textwrap::Wrapper; + /// + /// let wrapper = Wrapper::new(15) + /// .initial_indent("* ") + /// .subsequent_indent(" "); + /// ``` + /// + /// [`self.subsequent_indent`]: #structfield.subsequent_indent + pub fn subsequent_indent(self, indent: &'a str) -> Wrapper<'a, S> { + Wrapper { + subsequent_indent: indent, + ..self + } + } + + /// Change [`self.break_words`]. This controls if words longer + /// than `self.width` can be broken, or if they will be left + /// sticking out into the right margin. + /// + /// [`self.break_words`]: #structfield.break_words + pub fn break_words(self, setting: bool) -> Wrapper<'a, S> { + Wrapper { + break_words: setting, + ..self + } + } + + /// Fill a line of text at `self.width` characters. Strings are + /// wrapped based on their displayed width, not their size in + /// bytes. + /// + /// The result is a string with newlines between each line. Use + /// the `wrap` method if you need access to the individual lines. + /// + /// # Complexities + /// + /// This method simply joins the lines produced by `wrap_iter`. As + /// such, it inherits the O(*n*) time and memory complexity where + /// *n* is the input string length. + /// + /// # Examples + /// + /// ``` + /// use textwrap::Wrapper; + /// + /// let wrapper = Wrapper::new(15); + /// assert_eq!(wrapper.fill("Memory safety without garbage collection."), + /// "Memory safety\nwithout garbage\ncollection."); + /// ``` + pub fn fill(&self, s: &str) -> String { + // This will avoid reallocation in simple cases (no + // indentation, no hyphenation). + let mut result = String::with_capacity(s.len()); + + for (i, line) in self.wrap_iter(s).enumerate() { + if i > 0 { + result.push('\n'); + } + result.push_str(&line); + } + + result + } + + /// Wrap a line of text at `self.width` characters. Strings are + /// wrapped based on their displayed width, not their size in + /// bytes. + /// + /// # Complexities + /// + /// This method simply collects the lines produced by `wrap_iter`. + /// As such, it inherits the O(*n*) overall time and memory + /// complexity where *n* is the input string length. + /// + /// # Examples + /// + /// ``` + /// use textwrap::Wrapper; + /// + /// let wrap15 = Wrapper::new(15); + /// assert_eq!(wrap15.wrap("Concurrency without data races."), + /// vec!["Concurrency", + /// "without data", + /// "races."]); + /// + /// let wrap20 = Wrapper::new(20); + /// assert_eq!(wrap20.wrap("Concurrency without data races."), + /// vec!["Concurrency without", + /// "data races."]); + /// ``` + /// + /// Notice that newlines in the input are preserved. This means + /// that they force a line break, regardless of how long the + /// current line is: + /// + /// ``` + /// use textwrap::Wrapper; + /// + /// let wrapper = Wrapper::new(40); + /// assert_eq!(wrapper.wrap("First line.\nSecond line."), + /// vec!["First line.", "Second line."]); + /// ``` + /// + pub fn wrap(&self, s: &'a str) -> Vec<Cow<'a, str>> { + self.wrap_iter(s).collect::<Vec<_>>() + } + + /// Lazily wrap a line of text at `self.width` characters. Strings + /// are wrapped based on their displayed width, not their size in + /// bytes. + /// + /// The [`WordSplitter`] stored in [`self.splitter`] is used + /// whenever when a word is too large to fit on the current line. + /// By changing the field, different hyphenation strategies can be + /// implemented. + /// + /// # Complexities + /// + /// This method returns a [`WrapIter`] iterator which borrows this + /// `Wrapper`. The algorithm used has a linear complexity, so + /// getting the next line from the iterator will take O(*w*) time, + /// where *w* is the wrapping width. Fully processing the iterator + /// will take O(*n*) time for an input string of length *n*. + /// + /// When no indentation is used, each line returned is a slice of + /// the input string and the memory overhead is thus constant. + /// Otherwise new memory is allocated for each line returned. + /// + /// # Examples + /// + /// ``` + /// use std::borrow::Cow; + /// use textwrap::Wrapper; + /// + /// let wrap20 = Wrapper::new(20); + /// let mut wrap20_iter = wrap20.wrap_iter("Zero-cost abstractions."); + /// assert_eq!(wrap20_iter.next(), Some(Cow::from("Zero-cost"))); + /// assert_eq!(wrap20_iter.next(), Some(Cow::from("abstractions."))); + /// assert_eq!(wrap20_iter.next(), None); + /// + /// let wrap25 = Wrapper::new(25); + /// let mut wrap25_iter = wrap25.wrap_iter("Zero-cost abstractions."); + /// assert_eq!(wrap25_iter.next(), Some(Cow::from("Zero-cost abstractions."))); + /// assert_eq!(wrap25_iter.next(), None); + /// ``` + /// + /// [`self.splitter`]: #structfield.splitter + /// [`WordSplitter`]: trait.WordSplitter.html + /// [`WrapIter`]: struct.WrapIter.html + pub fn wrap_iter<'w>(&'w self, s: &'a str) -> WrapIter<'w, 'a, S> { + WrapIter { + wrapper: self, + inner: WrapIterImpl::new(self, s), + } + } + + /// Lazily wrap a line of text at `self.width` characters. Strings + /// are wrapped based on their displayed width, not their size in + /// bytes. + /// + /// The [`WordSplitter`] stored in [`self.splitter`] is used + /// whenever when a word is too large to fit on the current line. + /// By changing the field, different hyphenation strategies can be + /// implemented. + /// + /// # Complexities + /// + /// This method consumes the `Wrapper` and returns a + /// [`IntoWrapIter`] iterator. Fully processing the iterator has + /// the same O(*n*) time complexity as [`wrap_iter`], where *n* is + /// the length of the input string. + /// + /// # Examples + /// + /// ``` + /// use std::borrow::Cow; + /// use textwrap::Wrapper; + /// + /// let wrap20 = Wrapper::new(20); + /// let mut wrap20_iter = wrap20.into_wrap_iter("Zero-cost abstractions."); + /// assert_eq!(wrap20_iter.next(), Some(Cow::from("Zero-cost"))); + /// assert_eq!(wrap20_iter.next(), Some(Cow::from("abstractions."))); + /// assert_eq!(wrap20_iter.next(), None); + /// ``` + /// + /// [`self.splitter`]: #structfield.splitter + /// [`WordSplitter`]: trait.WordSplitter.html + /// [`IntoWrapIter`]: struct.IntoWrapIter.html + /// [`wrap_iter`]: #method.wrap_iter + pub fn into_wrap_iter(self, s: &'a str) -> IntoWrapIter<'a, S> { + let inner = WrapIterImpl::new(&self, s); + + IntoWrapIter { + wrapper: self, + inner: inner, + } + } +} + +/// An iterator over the lines of the input string which owns a +/// `Wrapper`. An instance of `IntoWrapIter` is typically obtained +/// through either [`wrap_iter`] or [`Wrapper::into_wrap_iter`]. +/// +/// Each call of `.next()` method yields a line wrapped in `Some` if the +/// input hasn't been fully processed yet. Otherwise it returns `None`. +/// +/// [`wrap_iter`]: fn.wrap_iter.html +/// [`Wrapper::into_wrap_iter`]: struct.Wrapper.html#method.into_wrap_iter +#[derive(Debug)] +pub struct IntoWrapIter<'a, S: WordSplitter> { + wrapper: Wrapper<'a, S>, + inner: WrapIterImpl<'a>, +} + +impl<'a, S: WordSplitter> Iterator for IntoWrapIter<'a, S> { + type Item = Cow<'a, str>; + + fn next(&mut self) -> Option<Cow<'a, str>> { + self.inner.next(&self.wrapper) + } +} + +/// An iterator over the lines of the input string which borrows a +/// `Wrapper`. An instance of `WrapIter` is typically obtained +/// through the [`Wrapper::wrap_iter`] method. +/// +/// Each call of `.next()` method yields a line wrapped in `Some` if the +/// input hasn't been fully processed yet. Otherwise it returns `None`. +/// +/// [`Wrapper::wrap_iter`]: struct.Wrapper.html#method.wrap_iter +#[derive(Debug)] +pub struct WrapIter<'w, 'a: 'w, S: WordSplitter + 'w> { + wrapper: &'w Wrapper<'a, S>, + inner: WrapIterImpl<'a>, +} + +impl<'w, 'a: 'w, S: WordSplitter> Iterator for WrapIter<'w, 'a, S> { + type Item = Cow<'a, str>; + + fn next(&mut self) -> Option<Cow<'a, str>> { + self.inner.next(self.wrapper) + } +} + +/// Like `char::is_whitespace`, but non-breaking spaces don't count. +#[inline] +fn is_whitespace(ch: char) -> bool { + ch.is_whitespace() && ch != NBSP +} + +/// Common implementation details for `WrapIter` and `IntoWrapIter`. +#[derive(Debug)] +struct WrapIterImpl<'a> { + // String to wrap. + source: &'a str, + // CharIndices iterator over self.source. + char_indices: CharIndices<'a>, + // Byte index where the current line starts. + start: usize, + // Byte index of the last place where the string can be split. + split: usize, + // Size in bytes of the character at self.source[self.split]. + split_len: usize, + // Width of self.source[self.start..idx]. + line_width: usize, + // Width of self.source[self.start..self.split]. + line_width_at_split: usize, + // Tracking runs of whitespace characters. + in_whitespace: bool, + // Has iterator finished producing elements? + finished: bool, +} + +impl<'a> WrapIterImpl<'a> { + fn new<S: WordSplitter>(wrapper: &Wrapper<'a, S>, s: &'a str) -> WrapIterImpl<'a> { + WrapIterImpl { + source: s, + char_indices: s.char_indices(), + start: 0, + split: 0, + split_len: 0, + line_width: wrapper.initial_indent.width(), + line_width_at_split: wrapper.initial_indent.width(), + in_whitespace: false, + finished: false, + } + } + + fn create_result_line<S: WordSplitter>(&self, wrapper: &Wrapper<'a, S>) -> Cow<'a, str> { + if self.start == 0 { + Cow::from(wrapper.initial_indent) + } else { + Cow::from(wrapper.subsequent_indent) + } + } + + fn next<S: WordSplitter>(&mut self, wrapper: &Wrapper<'a, S>) -> Option<Cow<'a, str>> { + if self.finished { + return None; + } + + while let Some((idx, ch)) = self.char_indices.next() { + let char_width = ch.width().unwrap_or(0); + let char_len = ch.len_utf8(); + + if ch == '\n' { + self.split = idx; + self.split_len = char_len; + self.line_width_at_split = self.line_width; + self.in_whitespace = false; + + // If this is not the final line, return the current line. Otherwise, + // we will return the line with its line break after exiting the loop + if self.split + self.split_len < self.source.len() { + let mut line = self.create_result_line(wrapper); + line += &self.source[self.start..self.split]; + + self.start = self.split + self.split_len; + self.line_width = wrapper.subsequent_indent.width(); + + return Some(line); + } + } else if is_whitespace(ch) { + // Extend the previous split or create a new one. + if self.in_whitespace { + self.split_len += char_len; + } else { + self.split = idx; + self.split_len = char_len; + } + self.line_width_at_split = self.line_width + char_width; + self.in_whitespace = true; + } else if self.line_width + char_width > wrapper.width { + // There is no room for this character on the current + // line. Try to split the final word. + self.in_whitespace = false; + let remaining_text = &self.source[self.split + self.split_len..]; + let final_word = match remaining_text.find(is_whitespace) { + Some(i) => &remaining_text[..i], + None => remaining_text, + }; + + let mut hyphen = ""; + let splits = wrapper.splitter.split(final_word); + for &(head, hyp, _) in splits.iter().rev() { + if self.line_width_at_split + head.width() + hyp.width() <= wrapper.width { + // We can fit head into the current line. + // Advance the split point by the width of the + // whitespace and the head length. + self.split += self.split_len + head.len(); + self.split_len = 0; + hyphen = hyp; + break; + } + } + + if self.start >= self.split { + // The word is too big to fit on a single line, so we + // need to split it at the current index. + if wrapper.break_words { + // Break work at current index. + self.split = idx; + self.split_len = 0; + self.line_width_at_split = self.line_width; + } else { + // Add smallest split. + self.split = self.start + splits[0].0.len(); + self.split_len = 0; + self.line_width_at_split = self.line_width; + } + } + + if self.start < self.split { + let mut line = self.create_result_line(wrapper); + line += &self.source[self.start..self.split]; + line += hyphen; + + self.start = self.split + self.split_len; + self.line_width += wrapper.subsequent_indent.width(); + self.line_width -= self.line_width_at_split; + self.line_width += char_width; + + return Some(line); + } + } else { + self.in_whitespace = false; + } + self.line_width += char_width; + } + + self.finished = true; + + // Add final line. + if self.start < self.source.len() { + let mut line = self.create_result_line(wrapper); + line += &self.source[self.start..]; + return Some(line); + } + + None + } +} + +/// 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 +/// +/// Create a `Wrapper` for the current terminal with a two column +/// margin: +/// +/// ```no_run +/// # #![allow(unused_variables)] +/// use textwrap::{Wrapper, NoHyphenation, termwidth}; +/// +/// let width = termwidth() - 4; // Two columns on each side. +/// let wrapper = Wrapper::with_splitter(width, NoHyphenation) +/// .initial_indent(" ") +/// .subsequent_indent(" "); +/// ``` +#[cfg(feature = "term_size")] +pub fn termwidth() -> usize { + term_size::dimensions_stdout().map_or(80, |(w, _)| w) +} + +/// Fill a line of text at `width` characters. Strings are wrapped +/// based on their displayed width, not their size in bytes. +/// +/// The result is a string with newlines between each line. Use +/// [`wrap`] if you need access to the individual lines or +/// [`wrap_iter`] for its iterator counterpart. +/// +/// ``` +/// use textwrap::fill; +/// +/// assert_eq!(fill("Memory safety without garbage collection.", 15), +/// "Memory safety\nwithout garbage\ncollection."); +/// ``` +/// +/// This function creates a Wrapper on the fly with default settings. +/// If you need to set a language corpus for automatic hyphenation, or +/// need to fill many strings, then it is suggested to create a Wrapper +/// and call its [`fill` method]. +/// +/// [`wrap`]: fn.wrap.html +/// [`wrap_iter`]: fn.wrap_iter.html +/// [`fill` method]: struct.Wrapper.html#method.fill +pub fn fill(s: &str, width: usize) -> String { + Wrapper::new(width).fill(s) +} + +/// Wrap a line of text at `width` characters. Strings are wrapped +/// based on their displayed width, not their size in bytes. +/// +/// This function creates a Wrapper on the fly with default settings. +/// If you need to set a language corpus for automatic hyphenation, or +/// need to wrap many strings, then it is suggested to create a Wrapper +/// and call its [`wrap` method]. +/// +/// The result is a vector of strings. Use [`wrap_iter`] if you need an +/// iterator version. +/// +/// # Examples +/// +/// ``` +/// use textwrap::wrap; +/// +/// assert_eq!(wrap("Concurrency without data races.", 15), +/// vec!["Concurrency", +/// "without data", +/// "races."]); +/// +/// assert_eq!(wrap("Concurrency without data races.", 20), +/// vec!["Concurrency without", +/// "data races."]); +/// ``` +/// +/// [`wrap_iter`]: fn.wrap_iter.html +/// [`wrap` method]: struct.Wrapper.html#method.wrap +pub fn wrap(s: &str, width: usize) -> Vec<Cow<str>> { + Wrapper::new(width).wrap(s) +} + +/// Lazily wrap a line of text at `width` characters. Strings are +/// wrapped based on their displayed width, not their size in bytes. +/// +/// This function creates a Wrapper on the fly with default settings. +/// It then calls the [`into_wrap_iter`] method. Hence, the return +/// value is an [`IntoWrapIter`], not a [`WrapIter`] as the function +/// name would otherwise suggest. +/// +/// If you need to set a language corpus for automatic hyphenation, or +/// need to wrap many strings, then it is suggested to create a Wrapper +/// and call its [`wrap_iter`] or [`into_wrap_iter`] methods. +/// +/// # Examples +/// +/// ``` +/// use std::borrow::Cow; +/// use textwrap::wrap_iter; +/// +/// let mut wrap20_iter = wrap_iter("Zero-cost abstractions.", 20); +/// assert_eq!(wrap20_iter.next(), Some(Cow::from("Zero-cost"))); +/// assert_eq!(wrap20_iter.next(), Some(Cow::from("abstractions."))); +/// assert_eq!(wrap20_iter.next(), None); +/// +/// let mut wrap25_iter = wrap_iter("Zero-cost abstractions.", 25); +/// assert_eq!(wrap25_iter.next(), Some(Cow::from("Zero-cost abstractions."))); +/// assert_eq!(wrap25_iter.next(), None); +/// ``` +/// +/// [`wrap_iter`]: struct.Wrapper.html#method.wrap_iter +/// [`into_wrap_iter`]: struct.Wrapper.html#method.into_wrap_iter +/// [`IntoWrapIter`]: struct.IntoWrapIter.html +/// [`WrapIter`]: struct.WrapIter.html +pub fn wrap_iter(s: &str, width: usize) -> IntoWrapIter<HyphenSplitter> { + Wrapper::new(width).into_wrap_iter(s) +} + +#[cfg(test)] +mod tests { + #[cfg(feature = "hyphenation")] + extern crate hyphenation; + + use super::*; + #[cfg(feature = "hyphenation")] + use hyphenation::{Language, Load, Standard}; + + #[test] + fn no_wrap() { + assert_eq!(wrap("foo", 10), vec!["foo"]); + } + + #[test] + fn simple() { + assert_eq!(wrap("foo bar baz", 5), vec!["foo", "bar", "baz"]); + } + + #[test] + fn multi_word_on_line() { + assert_eq!(wrap("foo bar baz", 10), vec!["foo bar", "baz"]); + } + + #[test] + fn long_word() { + assert_eq!(wrap("foo", 0), vec!["f", "o", "o"]); + } + + #[test] + fn long_words() { + assert_eq!(wrap("foo bar", 0), vec!["f", "o", "o", "b", "a", "r"]); + } + + #[test] + fn max_width() { + assert_eq!(wrap("foo bar", usize::max_value()), vec!["foo bar"]); + } + + #[test] + fn leading_whitespace() { + assert_eq!(wrap(" foo bar", 6), vec![" foo", "bar"]); + } + + #[test] + fn trailing_whitespace() { + assert_eq!(wrap("foo bar ", 6), vec!["foo", "bar "]); + } + + #[test] + fn interior_whitespace() { + assert_eq!(wrap("foo: bar baz", 10), vec!["foo: bar", "baz"]); + } + + #[test] + fn extra_whitespace_start_of_line() { + // Whitespace is only significant inside a line. After a line + // gets too long and is broken, the first word starts in + // column zero and is not indented. The line before might end + // up with trailing whitespace. + assert_eq!(wrap("foo bar", 5), vec!["foo", "bar"]); + } + + #[test] + fn issue_99() { + // We did not reset the in_whitespace flag correctly and did + // not handle single-character words after a line break. + assert_eq!( + wrap("aaabbbccc x yyyzzzwww", 9), + vec!["aaabbbccc", "x", "yyyzzzwww"] + ); + } + + #[test] + 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"]); + } + + #[test] + fn wide_character_handling() { + assert_eq!(wrap("Hello, World!", 15), vec!["Hello, World!"]); + assert_eq!( + wrap("Hello, World!", 15), + vec!["Hello,", "World!"] + ); + } + + #[test] + fn empty_input_not_indented() { + let wrapper = Wrapper::new(10).initial_indent("!!!"); + assert_eq!(wrapper.fill(""), ""); + } + + #[test] + fn indent_single_line() { + let wrapper = Wrapper::new(10).initial_indent(">>>"); // No trailing space + assert_eq!(wrapper.fill("foo"), ">>>foo"); + } + + #[test] + fn indent_multiple_lines() { + let wrapper = Wrapper::new(6).initial_indent("* ").subsequent_indent(" "); + assert_eq!(wrapper.wrap("foo bar baz"), vec!["* foo", " bar", " baz"]); + } + + #[test] + fn indent_break_words() { + let wrapper = Wrapper::new(5).initial_indent("* ").subsequent_indent(" "); + assert_eq!(wrapper.wrap("foobarbaz"), vec!["* foo", " bar", " baz"]); + } + + #[test] + fn hyphens() { + assert_eq!(wrap("foo-bar", 5), vec!["foo-", "bar"]); + } + + #[test] + fn trailing_hyphen() { + let wrapper = Wrapper::new(5).break_words(false); + assert_eq!(wrapper.wrap("foobar-"), vec!["foobar-"]); + } + + #[test] + fn multiple_hyphens() { + assert_eq!(wrap("foo-bar-baz", 5), vec!["foo-", "bar-", "baz"]); + } + + #[test] + fn hyphens_flag() { + let wrapper = Wrapper::new(5).break_words(false); + assert_eq!( + wrapper.wrap("The --foo-bar flag."), + vec!["The", "--foo-", "bar", "flag."] + ); + } + + #[test] + fn repeated_hyphens() { + let wrapper = Wrapper::new(4).break_words(false); + assert_eq!(wrapper.wrap("foo--bar"), vec!["foo--bar"]); + } + + #[test] + fn hyphens_alphanumeric() { + assert_eq!(wrap("Na2-CH4", 5), vec!["Na2-", "CH4"]); + } + + #[test] + fn hyphens_non_alphanumeric() { + let wrapper = Wrapper::new(5).break_words(false); + assert_eq!(wrapper.wrap("foo(-)bar"), vec!["foo(-)bar"]); + } + + #[test] + fn multiple_splits() { + assert_eq!(wrap("foo-bar-baz", 9), vec!["foo-bar-", "baz"]); + } + + #[test] + fn forced_split() { + let wrapper = Wrapper::new(5).break_words(false); + assert_eq!(wrapper.wrap("foobar-baz"), vec!["foobar-", "baz"]); + } + + #[test] + fn no_hyphenation() { + let wrapper = Wrapper::with_splitter(8, NoHyphenation); + assert_eq!(wrapper.wrap("foo bar-baz"), vec!["foo", "bar-baz"]); + } + + #[test] + #[cfg(feature = "hyphenation")] + fn auto_hyphenation() { + let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap(); + let wrapper = Wrapper::new(10); + assert_eq!( + wrapper.wrap("Internationalization"), + vec!["Internatio", "nalization"] + ); + + let wrapper = Wrapper::with_splitter(10, dictionary); + assert_eq!( + wrapper.wrap("Internationalization"), + vec!["Interna-", "tionaliza-", "tion"] + ); + } + + #[test] + #[cfg(feature = "hyphenation")] + fn split_len_hyphenation() { + // Test that hyphenation takes the width of the wihtespace + // into account. + let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap(); + let wrapper = Wrapper::with_splitter(15, dictionary); + assert_eq!( + wrapper.wrap("garbage collection"), + vec!["garbage col-", "lection"] + ); + } + + #[test] + #[cfg(feature = "hyphenation")] + fn borrowed_lines() { + // Lines that end with an extra hyphen are owned, the final + // line is borrowed. + use std::borrow::Cow::{Borrowed, Owned}; + let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap(); + let wrapper = Wrapper::with_splitter(10, dictionary); + let lines = wrapper.wrap("Internationalization"); + if let Borrowed(s) = lines[0] { + assert!(false, "should not have been borrowed: {:?}", s); + } + if let Borrowed(s) = lines[1] { + assert!(false, "should not have been borrowed: {:?}", s); + } + if let Owned(ref s) = lines[2] { + assert!(false, "should not have been owned: {:?}", s); + } + } + + #[test] + #[cfg(feature = "hyphenation")] + fn auto_hyphenation_with_hyphen() { + let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap(); + let wrapper = Wrapper::new(8).break_words(false); + assert_eq!(wrapper.wrap("over-caffinated"), vec!["over-", "caffinated"]); + + let wrapper = Wrapper::with_splitter(8, dictionary).break_words(false); + assert_eq!( + wrapper.wrap("over-caffinated"), + vec!["over-", "caffi-", "nated"] + ); + } + + #[test] + fn break_words() { + assert_eq!(wrap("foobarbaz", 3), vec!["foo", "bar", "baz"]); + } + + #[test] + fn break_words_wide_characters() { + assert_eq!(wrap("Hello", 5), vec!["He", "ll", "o"]); + } + + #[test] + fn break_words_zero_width() { + assert_eq!(wrap("foobar", 0), vec!["f", "o", "o", "b", "a", "r"]); + } + + #[test] + fn break_words_line_breaks() { + assert_eq!(fill("ab\ncdefghijkl", 5), "ab\ncdefg\nhijkl"); + assert_eq!(fill("abcdefgh\nijkl", 5), "abcde\nfgh\nijkl"); + } + + #[test] + fn preserve_line_breaks() { + assert_eq!(fill("test\n", 11), "test\n"); + assert_eq!(fill("test\n\na\n\n", 11), "test\n\na\n\n"); + assert_eq!(fill("1 3 5 7\n1 3 5 7", 7), "1 3 5 7\n1 3 5 7"); + } + + #[test] + fn wrap_preserve_line_breaks() { + assert_eq!(fill("1 3 5 7\n1 3 5 7", 5), "1 3 5\n7\n1 3 5\n7"); + } + + #[test] + fn non_breaking_space() { + let wrapper = Wrapper::new(5).break_words(false); + assert_eq!(wrapper.fill("foo bar baz"), "foo bar baz"); + } + + #[test] + fn non_breaking_hyphen() { + let wrapper = Wrapper::new(5).break_words(false); + assert_eq!(wrapper.fill("foo‑bar‑baz"), "foo‑bar‑baz"); + } + + #[test] + fn fill_simple() { + assert_eq!(fill("foo bar baz", 10), "foo bar\nbaz"); + } +} diff --git a/textwrap/src/splitting.rs b/textwrap/src/splitting.rs new file mode 100644 index 0000000..f6b65af --- /dev/null +++ b/textwrap/src/splitting.rs @@ -0,0 +1,139 @@ +//! 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. + +#[cfg(feature = "hyphenation")] +use hyphenation::{Hyphenator, Standard}; + +/// An interface for splitting words. +/// +/// When the [`wrap_iter`] method will try to fit text into a line, it +/// will eventually find a word that it too large the current text +/// width. It will then call the currently configured `WordSplitter` to +/// have it attempt to split the word into smaller parts. This trait +/// describes that functionality via the [`split`] method. +/// +/// If the `textwrap` crate has been compiled with the `hyphenation` +/// feature enabled, you will find an implementation of `WordSplitter` +/// by the `hyphenation::language::Corpus` struct. Use this struct for +/// language-aware hyphenation. See the [`hyphenation` documentation] +/// for details. +/// +/// [`wrap_iter`]: ../struct.Wrapper.html#method.wrap_iter +/// [`split`]: #tymethod.split +/// [`hyphenation` documentation]: https://docs.rs/hyphenation/ +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 + /// + /// ```no_run + /// vec![("tech", "-", "nology"), + /// ("technol", "-", "ogy"), + /// ("technolo", "-", "gy"), + /// ("technology", "", "")]; + /// ``` + fn split<'w>(&self, word: &'w str) -> Vec<(&'w str, &'w str, &'w str)>; +} + +/// Use this as a [`Wrapper.splitter`] to avoid any kind of +/// hyphenation: +/// +/// ``` +/// use textwrap::{Wrapper, NoHyphenation}; +/// +/// let wrapper = Wrapper::with_splitter(8, NoHyphenation); +/// assert_eq!(wrapper.wrap("foo bar-baz"), vec!["foo", "bar-baz"]); +/// ``` +/// +/// [`Wrapper.splitter`]: ../struct.Wrapper.html#structfield.splitter +#[derive(Clone, Debug)] +pub struct NoHyphenation; + +/// `NoHyphenation` implements `WordSplitter` by not splitting the +/// word at all. +impl WordSplitter for NoHyphenation { + fn split<'w>(&self, word: &'w str) -> Vec<(&'w str, &'w str, &'w str)> { + vec![(word, "", "")] + } +} + +/// 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 `Wrapper::new`. +#[derive(Clone, Debug)] +pub struct HyphenSplitter; + +/// `HyphenSplitter` is the default `WordSplitter` used by +/// `Wrapper::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 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 mut char_indices = word.char_indices(); + // Early return if the word is empty. + let mut prev = match char_indices.next() { + None => return vec![(word, "", "")], + Some((_, ch)) => ch, + }; + + // Find current word, or return early if the word only has a + // single character. + let (mut idx, mut cur) = match char_indices.next() { + None => return vec![(word, "", "")], + Some((idx, cur)) => (idx, cur), + }; + + for (i, next) in char_indices { + if prev.is_alphanumeric() && cur == '-' && next.is_alphanumeric() { + let (head, tail) = word.split_at(idx + 1); + triples.push((head, "", tail)); + } + prev = cur; + idx = i; + cur = next; + } + + // Finally option is no split at all. + triples.push((word, "", "")); + + triples + } +} + +/// A hyphenation dictionary can be used to do language-specific +/// hyphenation using patterns from the hyphenation crate. +#[cfg(feature = "hyphenation")] +impl WordSplitter for Standard { + fn split<'w>(&self, word: &'w str) -> Vec<(&'w str, &'w str, &'w str)> { + // Find splits based on language dictionary. + let mut triples = Vec::new(); + for n in self.hyphenate(word).breaks { + 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 + } +} diff --git a/textwrap/tests/version-numbers.rs b/textwrap/tests/version-numbers.rs new file mode 100644 index 0000000..85c52e3 --- /dev/null +++ b/textwrap/tests/version-numbers.rs @@ -0,0 +1,17 @@ +#[macro_use] +extern crate version_sync; + +#[test] +fn test_readme_deps() { + assert_markdown_deps_updated!("README.md"); +} + +#[test] +fn test_readme_changelog() { + assert_contains_regex!("README.md", r"^### Version {version} — .* \d\d?.., 20\d\d$"); +} + +#[test] +fn test_html_root_url() { + assert_html_root_url_updated!("src/lib.rs"); +} |