textwrap/
refill.rs

1//! Functionality for unfilling and refilling text.
2
3use crate::core::display_width;
4use crate::line_ending::NonEmptyLines;
5use 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/// ```
62pub 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/// ```
169pub fn refill<'a, Opt>(filled_text: &str, new_width_or_options: Opt) -> String
170where
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)]
191mod 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}