/rust/registry/src/index.crates.io-1949cf8c6b5b557f/textwrap-0.16.2/src/refill.rs
Line | Count | Source |
1 | | //! Functionality for unfilling and refilling text. |
2 | | |
3 | | use crate::core::display_width; |
4 | | use crate::line_ending::NonEmptyLines; |
5 | | use crate::{fill, LineEnding, Options}; |
6 | | |
7 | | /// Unpack a paragraph of already-wrapped text. |
8 | | /// |
9 | | /// This function attempts to recover the original text from a single |
10 | | /// paragraph of wrapped text, such as what [`fill()`] would produce. |
11 | | /// This means that it turns |
12 | | /// |
13 | | /// ```text |
14 | | /// textwrap: a small |
15 | | /// library for |
16 | | /// wrapping text. |
17 | | /// ``` |
18 | | /// |
19 | | /// back into |
20 | | /// |
21 | | /// ```text |
22 | | /// textwrap: a small library for wrapping text. |
23 | | /// ``` |
24 | | /// |
25 | | /// In addition, it will recognize a common prefix and a common line |
26 | | /// ending among the lines. |
27 | | /// |
28 | | /// The prefix of the first line is returned in |
29 | | /// [`Options::initial_indent`] and the prefix (if any) of the the |
30 | | /// other lines is returned in [`Options::subsequent_indent`]. |
31 | | /// |
32 | | /// Line ending is returned in [`Options::line_ending`]. If line ending |
33 | | /// can not be confidently detected (mixed or no line endings in the |
34 | | /// input), [`LineEnding::LF`] will be returned. |
35 | | /// |
36 | | /// In addition to `' '`, the prefixes can consist of characters used |
37 | | /// for unordered lists (`'-'`, `'+'`, and `'*'`) and block quotes |
38 | | /// (`'>'`) in Markdown as well as characters often used for inline |
39 | | /// comments (`'#'` and `'/'`). |
40 | | /// |
41 | | /// The text must come from a single wrapped paragraph. This means |
42 | | /// that there can be no empty lines (`"\n\n"` or `"\r\n\r\n"`) within |
43 | | /// the text. It is unspecified what happens if `unfill` is called on |
44 | | /// more than one paragraph of text. |
45 | | /// |
46 | | /// # Examples |
47 | | /// |
48 | | /// ``` |
49 | | /// use textwrap::{LineEnding, unfill}; |
50 | | /// |
51 | | /// let (text, options) = unfill("\ |
52 | | /// * This is an |
53 | | /// example of |
54 | | /// a list item. |
55 | | /// "); |
56 | | /// |
57 | | /// assert_eq!(text, "This is an example of a list item.\n"); |
58 | | /// assert_eq!(options.initial_indent, "* "); |
59 | | /// assert_eq!(options.subsequent_indent, " "); |
60 | | /// assert_eq!(options.line_ending, LineEnding::LF); |
61 | | /// ``` |
62 | 0 | pub fn unfill(text: &str) -> (String, Options<'_>) { |
63 | 0 | let prefix_chars: &[_] = &[' ', '-', '+', '*', '>', '#', '/']; |
64 | | |
65 | 0 | let mut options = Options::new(0); |
66 | 0 | for (idx, line) in text.lines().enumerate() { |
67 | 0 | options.width = std::cmp::max(options.width, display_width(line)); |
68 | 0 | let without_prefix = line.trim_start_matches(prefix_chars); |
69 | 0 | let prefix = &line[..line.len() - without_prefix.len()]; |
70 | | |
71 | 0 | if idx == 0 { |
72 | 0 | options.initial_indent = prefix; |
73 | 0 | } else if idx == 1 { |
74 | 0 | options.subsequent_indent = prefix; |
75 | 0 | } else if idx > 1 { |
76 | 0 | for ((idx, x), y) in prefix.char_indices().zip(options.subsequent_indent.chars()) { |
77 | 0 | if x != y { |
78 | 0 | options.subsequent_indent = &prefix[..idx]; |
79 | 0 | break; |
80 | 0 | } |
81 | | } |
82 | 0 | if prefix.len() < options.subsequent_indent.len() { |
83 | 0 | options.subsequent_indent = prefix; |
84 | 0 | } |
85 | 0 | } |
86 | | } |
87 | | |
88 | 0 | let mut unfilled = String::with_capacity(text.len()); |
89 | 0 | let mut detected_line_ending = None; |
90 | | |
91 | 0 | for (idx, (line, ending)) in NonEmptyLines(text).enumerate() { |
92 | 0 | if idx == 0 { |
93 | 0 | unfilled.push_str(&line[options.initial_indent.len()..]); |
94 | 0 | } else { |
95 | 0 | unfilled.push(' '); |
96 | 0 | unfilled.push_str(&line[options.subsequent_indent.len()..]); |
97 | 0 | } |
98 | 0 | match (detected_line_ending, ending) { |
99 | 0 | (None, Some(_)) => detected_line_ending = ending, |
100 | 0 | (Some(LineEnding::CRLF), Some(LineEnding::LF)) => detected_line_ending = ending, |
101 | 0 | _ => (), |
102 | | } |
103 | | } |
104 | | |
105 | | // Add back a line ending if `text` ends with the one we detect. |
106 | 0 | if let Some(line_ending) = detected_line_ending { |
107 | 0 | if text.ends_with(line_ending.as_str()) { |
108 | 0 | unfilled.push_str(line_ending.as_str()); |
109 | 0 | } |
110 | 0 | } |
111 | | |
112 | 0 | options.line_ending = detected_line_ending.unwrap_or(LineEnding::LF); |
113 | 0 | (unfilled, options) |
114 | 0 | } |
115 | | |
116 | | /// Refill a paragraph of wrapped text with a new width. |
117 | | /// |
118 | | /// This function will first use [`unfill()`] to remove newlines from |
119 | | /// the text. Afterwards the text is filled again using [`fill()`]. |
120 | | /// |
121 | | /// The `new_width_or_options` argument specify the new width and can |
122 | | /// specify other options as well — except for |
123 | | /// [`Options::initial_indent`] and [`Options::subsequent_indent`], |
124 | | /// which are deduced from `filled_text`. |
125 | | /// |
126 | | /// # Examples |
127 | | /// |
128 | | /// ``` |
129 | | /// use textwrap::refill; |
130 | | /// |
131 | | /// // Some loosely wrapped text. The "> " prefix is recognized automatically. |
132 | | /// let text = "\ |
133 | | /// > Memory |
134 | | /// > safety without garbage |
135 | | /// > collection. |
136 | | /// "; |
137 | | /// |
138 | | /// assert_eq!(refill(text, 20), "\ |
139 | | /// > Memory safety |
140 | | /// > without garbage |
141 | | /// > collection. |
142 | | /// "); |
143 | | /// |
144 | | /// assert_eq!(refill(text, 40), "\ |
145 | | /// > Memory safety without garbage |
146 | | /// > collection. |
147 | | /// "); |
148 | | /// |
149 | | /// assert_eq!(refill(text, 60), "\ |
150 | | /// > Memory safety without garbage collection. |
151 | | /// "); |
152 | | /// ``` |
153 | | /// |
154 | | /// You can also reshape bullet points: |
155 | | /// |
156 | | /// ``` |
157 | | /// use textwrap::refill; |
158 | | /// |
159 | | /// let text = "\ |
160 | | /// - This is my |
161 | | /// list item. |
162 | | /// "; |
163 | | /// |
164 | | /// assert_eq!(refill(text, 20), "\ |
165 | | /// - This is my list |
166 | | /// item. |
167 | | /// "); |
168 | | /// ``` |
169 | 0 | pub fn refill<'a, Opt>(filled_text: &str, new_width_or_options: Opt) -> String |
170 | 0 | where |
171 | 0 | Opt: Into<Options<'a>>, |
172 | | { |
173 | 0 | let mut new_options = new_width_or_options.into(); |
174 | 0 | let (text, options) = unfill(filled_text); |
175 | | // The original line ending is kept by `unfill`. |
176 | 0 | let stripped = text.strip_suffix(options.line_ending.as_str()); |
177 | 0 | let new_line_ending = new_options.line_ending.as_str(); |
178 | | |
179 | 0 | new_options.initial_indent = options.initial_indent; |
180 | 0 | new_options.subsequent_indent = options.subsequent_indent; |
181 | 0 | let mut refilled = fill(stripped.unwrap_or(&text), new_options); |
182 | | |
183 | | // Add back right line ending if we stripped one off above. |
184 | 0 | if stripped.is_some() { |
185 | 0 | refilled.push_str(new_line_ending); |
186 | 0 | } |
187 | 0 | refilled |
188 | 0 | } |
189 | | |
190 | | #[cfg(test)] |
191 | | mod tests { |
192 | | use super::*; |
193 | | |
194 | | #[test] |
195 | | fn unfill_simple() { |
196 | | let (text, options) = unfill("foo\nbar"); |
197 | | assert_eq!(text, "foo bar"); |
198 | | assert_eq!(options.width, 3); |
199 | | assert_eq!(options.line_ending, LineEnding::LF); |
200 | | } |
201 | | |
202 | | #[test] |
203 | | fn unfill_no_new_line() { |
204 | | let (text, options) = unfill("foo bar"); |
205 | | assert_eq!(text, "foo bar"); |
206 | | assert_eq!(options.width, 7); |
207 | | assert_eq!(options.line_ending, LineEnding::LF); |
208 | | } |
209 | | |
210 | | #[test] |
211 | | fn unfill_simple_crlf() { |
212 | | let (text, options) = unfill("foo\r\nbar"); |
213 | | assert_eq!(text, "foo bar"); |
214 | | assert_eq!(options.width, 3); |
215 | | assert_eq!(options.line_ending, LineEnding::CRLF); |
216 | | } |
217 | | |
218 | | #[test] |
219 | | fn unfill_mixed_new_lines() { |
220 | | let (text, options) = unfill("foo\r\nbar\nbaz"); |
221 | | assert_eq!(text, "foo bar baz"); |
222 | | assert_eq!(options.width, 3); |
223 | | assert_eq!(options.line_ending, LineEnding::LF); |
224 | | } |
225 | | |
226 | | #[test] |
227 | | fn test_unfill_consecutive_different_prefix() { |
228 | | let (text, options) = unfill("foo\n*\n/"); |
229 | | assert_eq!(text, "foo * /"); |
230 | | assert_eq!(options.width, 3); |
231 | | assert_eq!(options.line_ending, LineEnding::LF); |
232 | | } |
233 | | |
234 | | #[test] |
235 | | fn unfill_trailing_newlines() { |
236 | | let (text, options) = unfill("foo\nbar\n\n\n"); |
237 | | assert_eq!(text, "foo bar\n"); |
238 | | assert_eq!(options.width, 3); |
239 | | } |
240 | | |
241 | | #[test] |
242 | | fn unfill_mixed_trailing_newlines() { |
243 | | let (text, options) = unfill("foo\r\nbar\n\r\n\n"); |
244 | | assert_eq!(text, "foo bar\n"); |
245 | | assert_eq!(options.width, 3); |
246 | | assert_eq!(options.line_ending, LineEnding::LF); |
247 | | } |
248 | | |
249 | | #[test] |
250 | | fn unfill_trailing_crlf() { |
251 | | let (text, options) = unfill("foo bar\r\n"); |
252 | | assert_eq!(text, "foo bar\r\n"); |
253 | | assert_eq!(options.width, 7); |
254 | | assert_eq!(options.line_ending, LineEnding::CRLF); |
255 | | } |
256 | | |
257 | | #[test] |
258 | | fn unfill_initial_indent() { |
259 | | let (text, options) = unfill(" foo\nbar\nbaz"); |
260 | | assert_eq!(text, "foo bar baz"); |
261 | | assert_eq!(options.width, 5); |
262 | | assert_eq!(options.initial_indent, " "); |
263 | | } |
264 | | |
265 | | #[test] |
266 | | fn unfill_differing_indents() { |
267 | | let (text, options) = unfill(" foo\n bar\n baz"); |
268 | | assert_eq!(text, "foo bar baz"); |
269 | | assert_eq!(options.width, 7); |
270 | | assert_eq!(options.initial_indent, " "); |
271 | | assert_eq!(options.subsequent_indent, " "); |
272 | | } |
273 | | |
274 | | #[test] |
275 | | fn unfill_list_item() { |
276 | | let (text, options) = unfill("* foo\n bar\n baz"); |
277 | | assert_eq!(text, "foo bar baz"); |
278 | | assert_eq!(options.width, 5); |
279 | | assert_eq!(options.initial_indent, "* "); |
280 | | assert_eq!(options.subsequent_indent, " "); |
281 | | } |
282 | | |
283 | | #[test] |
284 | | fn unfill_multiple_char_prefix() { |
285 | | let (text, options) = unfill(" // foo bar\n // baz\n // quux"); |
286 | | assert_eq!(text, "foo bar baz quux"); |
287 | | assert_eq!(options.width, 14); |
288 | | assert_eq!(options.initial_indent, " // "); |
289 | | assert_eq!(options.subsequent_indent, " // "); |
290 | | } |
291 | | |
292 | | #[test] |
293 | | fn unfill_block_quote() { |
294 | | let (text, options) = unfill("> foo\n> bar\n> baz"); |
295 | | assert_eq!(text, "foo bar baz"); |
296 | | assert_eq!(options.width, 5); |
297 | | assert_eq!(options.initial_indent, "> "); |
298 | | assert_eq!(options.subsequent_indent, "> "); |
299 | | } |
300 | | |
301 | | #[test] |
302 | | fn unfill_only_prefixes_issue_466() { |
303 | | // Test that we don't crash if the first line has only prefix |
304 | | // chars *and* the second line is shorter than the first line. |
305 | | let (text, options) = unfill("######\nfoo"); |
306 | | assert_eq!(text, " foo"); |
307 | | assert_eq!(options.width, 6); |
308 | | assert_eq!(options.initial_indent, "######"); |
309 | | assert_eq!(options.subsequent_indent, ""); |
310 | | } |
311 | | |
312 | | #[test] |
313 | | fn unfill_trailing_newlines_issue_466() { |
314 | | // Test that we don't crash on a '\r' following a string of |
315 | | // '\n'. The problem was that we removed both kinds of |
316 | | // characters in one code path, but not in the other. |
317 | | let (text, options) = unfill("foo\n##\n\n\r"); |
318 | | // The \n\n changes subsequent_indent to "". |
319 | | assert_eq!(text, "foo ## \r"); |
320 | | assert_eq!(options.width, 3); |
321 | | assert_eq!(options.initial_indent, ""); |
322 | | assert_eq!(options.subsequent_indent, ""); |
323 | | } |
324 | | |
325 | | #[test] |
326 | | fn unfill_whitespace() { |
327 | | assert_eq!(unfill("foo bar").0, "foo bar"); |
328 | | } |
329 | | |
330 | | #[test] |
331 | | fn refill_convert_lf_to_crlf() { |
332 | | let options = Options::new(5).line_ending(LineEnding::CRLF); |
333 | | assert_eq!(refill("foo\nbar\n", options), "foo\r\nbar\r\n",); |
334 | | } |
335 | | |
336 | | #[test] |
337 | | fn refill_convert_crlf_to_lf() { |
338 | | let options = Options::new(5).line_ending(LineEnding::LF); |
339 | | assert_eq!(refill("foo\r\nbar\r\n", options), "foo\nbar\n",); |
340 | | } |
341 | | |
342 | | #[test] |
343 | | fn refill_convert_mixed_newlines() { |
344 | | let options = Options::new(5).line_ending(LineEnding::CRLF); |
345 | | assert_eq!(refill("foo\r\nbar\n", options), "foo\r\nbar\r\n",); |
346 | | } |
347 | | |
348 | | #[test] |
349 | | fn refill_defaults_to_lf() { |
350 | | assert_eq!(refill("foo bar baz", 5), "foo\nbar\nbaz"); |
351 | | } |
352 | | } |