textwrap/
fill.rs

1//! Functions for filling text.
2
3use crate::{wrap, wrap_algorithms, Options, WordSeparator};
4
5/// Fill a line of text at a given width.
6///
7/// The result is a [`String`], complete with newlines between each
8/// line. Use [`wrap()`] if you need access to the individual lines.
9///
10/// The easiest way to use this function is to pass an integer for
11/// `width_or_options`:
12///
13/// ```
14/// use textwrap::fill;
15///
16/// assert_eq!(
17///     fill("Memory safety without garbage collection.", 15),
18///     "Memory safety\nwithout garbage\ncollection."
19/// );
20/// ```
21///
22/// If you need to customize the wrapping, you can pass an [`Options`]
23/// instead of an `usize`:
24///
25/// ```
26/// use textwrap::{fill, Options};
27///
28/// let options = Options::new(15)
29///     .initial_indent("- ")
30///     .subsequent_indent("  ");
31/// assert_eq!(
32///     fill("Memory safety without garbage collection.", &options),
33///     "- Memory safety\n  without\n  garbage\n  collection."
34/// );
35/// ```
36pub fn fill<'a, Opt>(text: &str, width_or_options: Opt) -> String
37where
38    Opt: Into<Options<'a>>,
39{
40    let options = width_or_options.into();
41
42    if text.len() < options.width && !text.contains('\n') && options.initial_indent.is_empty() {
43        String::from(text.trim_end_matches(' '))
44    } else {
45        fill_slow_path(text, options)
46    }
47}
48
49/// Slow path for fill.
50///
51/// This is taken when `text` is longer than `options.width`.
52pub(crate) fn fill_slow_path(text: &str, options: Options<'_>) -> String {
53    // This will avoid reallocation in simple cases (no
54    // indentation, no hyphenation).
55    let mut result = String::with_capacity(text.len());
56
57    let line_ending_str = options.line_ending.as_str();
58    for (i, line) in wrap(text, options).iter().enumerate() {
59        if i > 0 {
60            result.push_str(line_ending_str);
61        }
62        result.push_str(line);
63    }
64
65    result
66}
67
68/// Fill `text` in-place without reallocating the input string.
69///
70/// This function works by modifying the input string: some `' '`
71/// characters will be replaced by `'\n'` characters. The rest of the
72/// text remains untouched.
73///
74/// Since we can only replace existing whitespace in the input with
75/// `'\n'` (there is no space for `"\r\n"`), we cannot do hyphenation
76/// nor can we split words longer than the line width. We also need to
77/// use `AsciiSpace` as the word separator since we need `' '`
78/// characters between words in order to replace some of them with a
79/// `'\n'`. Indentation is also ruled out. In other words,
80/// `fill_inplace(width)` behaves as if you had called [`fill()`] with
81/// these options:
82///
83/// ```
84/// # use textwrap::{core, LineEnding, Options, WordSplitter, WordSeparator, WrapAlgorithm};
85/// # let width = 80;
86/// Options::new(width)
87///     .break_words(false)
88///     .line_ending(LineEnding::LF)
89///     .word_separator(WordSeparator::AsciiSpace)
90///     .wrap_algorithm(WrapAlgorithm::FirstFit)
91///     .word_splitter(WordSplitter::NoHyphenation);
92/// ```
93///
94/// The wrap algorithm is
95/// [`WrapAlgorithm::FirstFit`](crate::WrapAlgorithm::FirstFit) since
96/// this is the fastest algorithm — and the main reason to use
97/// `fill_inplace` is to get the string broken into newlines as fast
98/// as possible.
99///
100/// A last difference is that (unlike [`fill()`]) `fill_inplace` can
101/// leave trailing whitespace on lines. This is because we wrap by
102/// inserting a `'\n'` at the final whitespace in the input string:
103///
104/// ```
105/// let mut text = String::from("Hello   World!");
106/// textwrap::fill_inplace(&mut text, 10);
107/// assert_eq!(text, "Hello  \nWorld!");
108/// ```
109///
110/// If we didn't do this, the word `World!` would end up being
111/// indented. You can avoid this if you make sure that your input text
112/// has no double spaces.
113///
114/// # Performance
115///
116/// In benchmarks, `fill_inplace` is about twice as fast as
117/// [`fill()`]. Please see the [`linear`
118/// benchmark](https://github.com/mgeisler/textwrap/blob/master/benchmarks/linear.rs)
119/// for details.
120pub fn fill_inplace(text: &mut String, width: usize) {
121    let mut indices = Vec::new();
122
123    let mut offset = 0;
124    for line in text.split('\n') {
125        let words = WordSeparator::AsciiSpace
126            .find_words(line)
127            .collect::<Vec<_>>();
128        let wrapped_words = wrap_algorithms::wrap_first_fit(&words, &[width as f64]);
129
130        let mut line_offset = offset;
131        for words in &wrapped_words[..wrapped_words.len() - 1] {
132            let line_len = words
133                .iter()
134                .map(|word| word.len() + word.whitespace.len())
135                .sum::<usize>();
136
137            line_offset += line_len;
138            // We've advanced past all ' ' characters -- want to move
139            // one ' ' backwards and insert our '\n' there.
140            indices.push(line_offset - 1);
141        }
142
143        // Advance past entire line, plus the '\n' which was removed
144        // by the split call above.
145        offset += line.len() + 1;
146    }
147
148    let mut bytes = std::mem::take(text).into_bytes();
149    for idx in indices {
150        bytes[idx] = b'\n';
151    }
152    *text = String::from_utf8(bytes).unwrap();
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use crate::WrapAlgorithm;
159
160    #[test]
161    fn fill_simple() {
162        assert_eq!(fill("foo bar baz", 10), "foo bar\nbaz");
163    }
164
165    #[test]
166    fn fill_unicode_boundary() {
167        // https://github.com/mgeisler/textwrap/issues/390
168        fill("\u{1b}!Ͽ", 10);
169    }
170
171    #[test]
172    fn non_breaking_space() {
173        let options = Options::new(5).break_words(false);
174        assert_eq!(fill("foo bar baz", &options), "foo bar baz");
175    }
176
177    #[test]
178    fn non_breaking_hyphen() {
179        let options = Options::new(5).break_words(false);
180        assert_eq!(fill("foo‑bar‑baz", &options), "foo‑bar‑baz");
181    }
182
183    #[test]
184    fn fill_preserves_line_breaks_trims_whitespace() {
185        assert_eq!(fill("  ", 80), "");
186        assert_eq!(fill("  \n  ", 80), "\n");
187        assert_eq!(fill("  \n \n  \n ", 80), "\n\n\n");
188    }
189
190    #[test]
191    fn preserve_line_breaks() {
192        assert_eq!(fill("", 80), "");
193        assert_eq!(fill("\n", 80), "\n");
194        assert_eq!(fill("\n\n\n", 80), "\n\n\n");
195        assert_eq!(fill("test\n", 80), "test\n");
196        assert_eq!(fill("test\n\na\n\n", 80), "test\n\na\n\n");
197        assert_eq!(
198            fill(
199                "1 3 5 7\n1 3 5 7",
200                Options::new(7).wrap_algorithm(WrapAlgorithm::FirstFit)
201            ),
202            "1 3 5 7\n1 3 5 7"
203        );
204        assert_eq!(
205            fill(
206                "1 3 5 7\n1 3 5 7",
207                Options::new(5).wrap_algorithm(WrapAlgorithm::FirstFit)
208            ),
209            "1 3 5\n7\n1 3 5\n7"
210        );
211    }
212
213    #[test]
214    fn break_words_line_breaks() {
215        assert_eq!(fill("ab\ncdefghijkl", 5), "ab\ncdefg\nhijkl");
216        assert_eq!(fill("abcdefgh\nijkl", 5), "abcde\nfgh\nijkl");
217    }
218
219    #[test]
220    fn break_words_empty_lines() {
221        assert_eq!(
222            fill("foo\nbar", &Options::new(2).break_words(false)),
223            "foo\nbar"
224        );
225    }
226
227    #[test]
228    fn fill_inplace_empty() {
229        let mut text = String::from("");
230        fill_inplace(&mut text, 80);
231        assert_eq!(text, "");
232    }
233
234    #[test]
235    fn fill_inplace_simple() {
236        let mut text = String::from("foo bar baz");
237        fill_inplace(&mut text, 10);
238        assert_eq!(text, "foo bar\nbaz");
239    }
240
241    #[test]
242    fn fill_inplace_multiple_lines() {
243        let mut text = String::from("Some text to wrap over multiple lines");
244        fill_inplace(&mut text, 12);
245        assert_eq!(text, "Some text to\nwrap over\nmultiple\nlines");
246    }
247
248    #[test]
249    fn fill_inplace_long_word() {
250        let mut text = String::from("Internationalization is hard");
251        fill_inplace(&mut text, 10);
252        assert_eq!(text, "Internationalization\nis hard");
253    }
254
255    #[test]
256    fn fill_inplace_no_hyphen_splitting() {
257        let mut text = String::from("A well-chosen example");
258        fill_inplace(&mut text, 10);
259        assert_eq!(text, "A\nwell-chosen\nexample");
260    }
261
262    #[test]
263    fn fill_inplace_newlines() {
264        let mut text = String::from("foo bar\n\nbaz\n\n\n");
265        fill_inplace(&mut text, 10);
266        assert_eq!(text, "foo bar\n\nbaz\n\n\n");
267    }
268
269    #[test]
270    fn fill_inplace_newlines_reset_line_width() {
271        let mut text = String::from("1 3 5\n1 3 5 7 9\n1 3 5 7 9 1 3");
272        fill_inplace(&mut text, 10);
273        assert_eq!(text, "1 3 5\n1 3 5 7 9\n1 3 5 7 9\n1 3");
274    }
275
276    #[test]
277    fn fill_inplace_leading_whitespace() {
278        let mut text = String::from("  foo bar baz");
279        fill_inplace(&mut text, 10);
280        assert_eq!(text, "  foo bar\nbaz");
281    }
282
283    #[test]
284    fn fill_inplace_trailing_whitespace() {
285        let mut text = String::from("foo bar baz  ");
286        fill_inplace(&mut text, 10);
287        assert_eq!(text, "foo bar\nbaz  ");
288    }
289
290    #[test]
291    fn fill_inplace_interior_whitespace() {
292        // To avoid an unwanted indentation of "baz", it is important
293        // to replace the final ' ' with '\n'.
294        let mut text = String::from("foo  bar    baz");
295        fill_inplace(&mut text, 10);
296        assert_eq!(text, "foo  bar   \nbaz");
297    }
298}