Coverage Report

Created: 2026-10-28 07:08

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}