/src/textwrap/src/fill.rs
Line | Count | Source |
1 | | //! Functions for filling text. |
2 | | |
3 | | use 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 | | /// ``` |
36 | 11.9k | pub fn fill<'a, Opt>(text: &str, width_or_options: Opt) -> String |
37 | 11.9k | where |
38 | 11.9k | Opt: Into<Options<'a>>, |
39 | | { |
40 | 11.9k | let options = width_or_options.into(); |
41 | | |
42 | 11.9k | if text.len() < options.width && !text.contains('\n') && options.initial_indent.is_empty() { |
43 | 1.66k | String::from(text.trim_end_matches(' ')) |
44 | | } else { |
45 | 10.2k | fill_slow_path(text, options) |
46 | | } |
47 | 11.9k | } textwrap::fill::fill::<&textwrap::options::Options> Line | Count | Source | 36 | 2.61k | pub fn fill<'a, Opt>(text: &str, width_or_options: Opt) -> String | 37 | 2.61k | where | 38 | 2.61k | Opt: Into<Options<'a>>, | 39 | | { | 40 | 2.61k | let options = width_or_options.into(); | 41 | | | 42 | 2.61k | if text.len() < options.width && !text.contains('\n') && options.initial_indent.is_empty() { | 43 | 411 | String::from(text.trim_end_matches(' ')) | 44 | | } else { | 45 | 2.19k | fill_slow_path(text, options) | 46 | | } | 47 | 2.61k | } |
textwrap::fill::fill::<textwrap::options::Options> Line | Count | Source | 36 | 3.39k | pub fn fill<'a, Opt>(text: &str, width_or_options: Opt) -> String | 37 | 3.39k | where | 38 | 3.39k | Opt: Into<Options<'a>>, | 39 | | { | 40 | 3.39k | let options = width_or_options.into(); | 41 | | | 42 | 3.39k | if text.len() < options.width && !text.contains('\n') && options.initial_indent.is_empty() { | 43 | 490 | String::from(text.trim_end_matches(' ')) | 44 | | } else { | 45 | 2.90k | fill_slow_path(text, options) | 46 | | } | 47 | 3.39k | } |
textwrap::fill::fill::<&textwrap::options::Options> Line | Count | Source | 36 | 3.06k | pub fn fill<'a, Opt>(text: &str, width_or_options: Opt) -> String | 37 | 3.06k | where | 38 | 3.06k | Opt: Into<Options<'a>>, | 39 | | { | 40 | 3.06k | let options = width_or_options.into(); | 41 | | | 42 | 3.06k | if text.len() < options.width && !text.contains('\n') && options.initial_indent.is_empty() { | 43 | 375 | String::from(text.trim_end_matches(' ')) | 44 | | } else { | 45 | 2.68k | fill_slow_path(text, options) | 46 | | } | 47 | 3.06k | } |
textwrap::fill::fill::<&textwrap::options::Options> Line | Count | Source | 36 | 2.86k | pub fn fill<'a, Opt>(text: &str, width_or_options: Opt) -> String | 37 | 2.86k | where | 38 | 2.86k | Opt: Into<Options<'a>>, | 39 | | { | 40 | 2.86k | let options = width_or_options.into(); | 41 | | | 42 | 2.86k | if text.len() < options.width && !text.contains('\n') && options.initial_indent.is_empty() { | 43 | 392 | String::from(text.trim_end_matches(' ')) | 44 | | } else { | 45 | 2.47k | fill_slow_path(text, options) | 46 | | } | 47 | 2.86k | } |
|
48 | | |
49 | | /// Slow path for fill. |
50 | | /// |
51 | | /// This is taken when `text` is longer than `options.width`. |
52 | 12.8k | pub(crate) fn fill_slow_path(text: &str, options: Options<'_>) -> String { |
53 | | // This will avoid reallocation in simple cases (no |
54 | | // indentation, no hyphenation). |
55 | 12.8k | let mut result = String::with_capacity(text.len()); |
56 | | |
57 | 12.8k | let line_ending_str = options.line_ending.as_str(); |
58 | 15.4M | for (i, line) in wrap(text, options).iter().enumerate() { |
59 | 15.4M | if i > 0 { |
60 | 15.4M | result.push_str(line_ending_str); |
61 | 15.4M | } |
62 | 15.4M | result.push_str(line); |
63 | | } |
64 | | |
65 | 12.8k | result |
66 | 12.8k | } |
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. |
120 | 0 | pub fn fill_inplace(text: &mut String, width: usize) { |
121 | 0 | let mut indices = Vec::new(); |
122 | | |
123 | 0 | let mut offset = 0; |
124 | 0 | for line in text.split('\n') { |
125 | 0 | let words = WordSeparator::AsciiSpace |
126 | 0 | .find_words(line) |
127 | 0 | .collect::<Vec<_>>(); |
128 | 0 | let wrapped_words = wrap_algorithms::wrap_first_fit(&words, &[width as f64]); |
129 | | |
130 | 0 | let mut line_offset = offset; |
131 | 0 | for words in &wrapped_words[..wrapped_words.len() - 1] { |
132 | 0 | let line_len = words |
133 | 0 | .iter() |
134 | 0 | .map(|word| word.len() + word.whitespace.len()) |
135 | 0 | .sum::<usize>(); |
136 | | |
137 | 0 | line_offset += line_len; |
138 | | // We've advanced past all ' ' characters -- want to move |
139 | | // one ' ' backwards and insert our '\n' there. |
140 | 0 | 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 | 0 | offset += line.len() + 1; |
146 | | } |
147 | | |
148 | 0 | let mut bytes = std::mem::take(text).into_bytes(); |
149 | 0 | for idx in indices { |
150 | 0 | bytes[idx] = b'\n'; |
151 | 0 | } |
152 | 0 | *text = String::from_utf8(bytes).unwrap(); |
153 | 0 | } |
154 | | |
155 | | #[cfg(test)] |
156 | | mod 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 | | } |