• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 /// ```
unfill(text: &str) -> (String, Options<'_>)62 pub fn unfill(text: &str) -> (String, Options<'_>) {
63     let prefix_chars: &[_] = &[' ', '-', '+', '*', '>', '#', '/'];
64 
65     let mut options = Options::new(0);
66     for (idx, line) in text.lines().enumerate() {
67         options.width = std::cmp::max(options.width, display_width(line));
68         let without_prefix = line.trim_start_matches(prefix_chars);
69         let prefix = &line[..line.len() - without_prefix.len()];
70 
71         if idx == 0 {
72             options.initial_indent = prefix;
73         } else if idx == 1 {
74             options.subsequent_indent = prefix;
75         } else if idx > 1 {
76             for ((idx, x), y) in prefix.char_indices().zip(options.subsequent_indent.chars()) {
77                 if x != y {
78                     options.subsequent_indent = &prefix[..idx];
79                     break;
80                 }
81             }
82             if prefix.len() < options.subsequent_indent.len() {
83                 options.subsequent_indent = prefix;
84             }
85         }
86     }
87 
88     let mut unfilled = String::with_capacity(text.len());
89     let mut detected_line_ending = None;
90 
91     for (idx, (line, ending)) in NonEmptyLines(text).enumerate() {
92         if idx == 0 {
93             unfilled.push_str(&line[options.initial_indent.len()..]);
94         } else {
95             unfilled.push(' ');
96             unfilled.push_str(&line[options.subsequent_indent.len()..]);
97         }
98         match (detected_line_ending, ending) {
99             (None, Some(_)) => detected_line_ending = ending,
100             (Some(LineEnding::CRLF), Some(LineEnding::LF)) => detected_line_ending = ending,
101             _ => (),
102         }
103     }
104 
105     // Add back a line ending if `text` ends with the one we detect.
106     if let Some(line_ending) = detected_line_ending {
107         if text.ends_with(line_ending.as_str()) {
108             unfilled.push_str(line_ending.as_str());
109         }
110     }
111 
112     options.line_ending = detected_line_ending.unwrap_or(LineEnding::LF);
113     (unfilled, options)
114 }
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 /// ```
refill<'a, Opt>(filled_text: &str, new_width_or_options: Opt) -> String where Opt: Into<Options<'a>>,169 pub fn refill<'a, Opt>(filled_text: &str, new_width_or_options: Opt) -> String
170 where
171     Opt: Into<Options<'a>>,
172 {
173     let mut new_options = new_width_or_options.into();
174     let (text, options) = unfill(filled_text);
175     // The original line ending is kept by `unfill`.
176     let stripped = text.strip_suffix(options.line_ending.as_str());
177     let new_line_ending = new_options.line_ending.as_str();
178 
179     new_options.initial_indent = options.initial_indent;
180     new_options.subsequent_indent = options.subsequent_indent;
181     let mut refilled = fill(stripped.unwrap_or(&text), new_options);
182 
183     // Add back right line ending if we stripped one off above.
184     if stripped.is_some() {
185         refilled.push_str(new_line_ending);
186     }
187     refilled
188 }
189 
190 #[cfg(test)]
191 mod tests {
192     use super::*;
193 
194     #[test]
unfill_simple()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]
unfill_no_new_line()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]
unfill_simple_crlf()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]
unfill_mixed_new_lines()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]
test_unfill_consecutive_different_prefix()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]
unfill_trailing_newlines()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]
unfill_mixed_trailing_newlines()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]
unfill_trailing_crlf()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]
unfill_initial_indent()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]
unfill_differing_indents()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]
unfill_list_item()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]
unfill_multiple_char_prefix()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]
unfill_block_quote()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]
unfill_only_prefixes_issue_466()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]
unfill_trailing_newlines_issue_466()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]
unfill_whitespace()326     fn unfill_whitespace() {
327         assert_eq!(unfill("foo   bar").0, "foo   bar");
328     }
329 
330     #[test]
refill_convert_lf_to_crlf()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]
refill_convert_crlf_to_lf()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]
refill_convert_mixed_newlines()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]
refill_defaults_to_lf()349     fn refill_defaults_to_lf() {
350         assert_eq!(refill("foo bar baz", 5), "foo\nbar\nbaz");
351     }
352 }
353