#![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;
const NBSP: char = '\u{a0}';
mod indentation;
pub use indentation::dedent;
pub use indentation::indent;
mod splitting;
pub use splitting::{HyphenSplitter, NoHyphenation, WordSplitter};
#[derive(Clone, Debug)]
pub struct Wrapper<'a, S: WordSplitter> {
pub width: usize,
pub initial_indent: &'a str,
pub subsequent_indent: &'a str,
pub break_words: bool,
pub splitter: S,
}
impl<'a> Wrapper<'a, HyphenSplitter> {
pub fn new(width: usize) -> Wrapper<'a, HyphenSplitter> {
Wrapper::with_splitter(width, HyphenSplitter)
}
#[cfg(feature = "term_size")]
pub fn with_termwidth() -> Wrapper<'a, HyphenSplitter> {
Wrapper::new(termwidth())
}
}
impl<'a, S: WordSplitter> Wrapper<'a, S> {
pub fn with_splitter(width: usize, splitter: S) -> Wrapper<'a, S> {
Wrapper {
width: width,
initial_indent: "",
subsequent_indent: "",
break_words: true,
splitter: splitter,
}
}
pub fn initial_indent(self, indent: &'a str) -> Wrapper<'a, S> {
Wrapper {
initial_indent: indent,
..self
}
}
pub fn subsequent_indent(self, indent: &'a str) -> Wrapper<'a, S> {
Wrapper {
subsequent_indent: indent,
..self
}
}
pub fn break_words(self, setting: bool) -> Wrapper<'a, S> {
Wrapper {
break_words: setting,
..self
}
}
pub fn fill(&self, s: &str) -> String {
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
}
pub fn wrap(&self, s: &'a str) -> Vec<Cow<'a, str>> {
self.wrap_iter(s).collect::<Vec<_>>()
}
pub fn wrap_iter<'w>(&'w self, s: &'a str) -> WrapIter<'w, 'a, S> {
WrapIter {
wrapper: self,
inner: WrapIterImpl::new(self, s),
}
}
pub fn into_wrap_iter(self, s: &'a str) -> IntoWrapIter<'a, S> {
let inner = WrapIterImpl::new(&self, s);
IntoWrapIter {
wrapper: self,
inner: inner,
}
}
}
#[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)
}
}
#[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)
}
}
#[inline]
fn is_whitespace(ch: char) -> bool {
ch.is_whitespace() && ch != NBSP
}
#[derive(Debug)]
struct WrapIterImpl<'a> {
source: &'a str,
char_indices: CharIndices<'a>,
start: usize,
split: usize,
split_len: usize,
line_width: usize,
line_width_at_split: usize,
in_whitespace: bool,
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 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) {
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 {
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 {
self.split += self.split_len + head.len();
self.split_len = 0;
hyphen = hyp;
break;
}
}
if self.start >= self.split {
if wrapper.break_words {
self.split = idx;
self.split_len = 0;
self.line_width_at_split = self.line_width;
} else {
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;
if self.start < self.source.len() {
let mut line = self.create_result_line(wrapper);
line += &self.source[self.start..];
return Some(line);
}
None
}
}
#[cfg(feature = "term_size")]
pub fn termwidth() -> usize {
term_size::dimensions_stdout().map_or(80, |(w, _)| w)
}
pub fn fill(s: &str, width: usize) -> String {
Wrapper::new(width).fill(s)
}
pub fn wrap(s: &str, width: usize) -> Vec<Cow<str>> {
Wrapper::new(width).wrap(s)
}
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() {
assert_eq!(wrap("foo bar", 5), vec!["foo", "bar"]);
}
#[test]
fn issue_99() {
assert_eq!(
wrap("aaabbbccc x yyyzzzwww", 9),
vec!["aaabbbccc", "x", "yyyzzzwww"]
);
}
#[test]
fn issue_129() {
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(">>>"); 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() {
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() {
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");
}
}