Skip to main content

textwrap/
refill.rs

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