Coverage Report

Created: 2026-08-13 06:11

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/textwrap/src/core.rs
Line
Count
Source
1
//! Building blocks for advanced wrapping functionality.
2
//!
3
//! The functions and structs in this module can be used to implement
4
//! advanced wrapping functionality when [`wrap()`](crate::wrap())
5
//! [`fill()`](crate::fill()) don't do what you want.
6
//!
7
//! In general, you want to follow these steps when wrapping
8
//! something:
9
//!
10
//! 1. Split your input into [`Fragment`]s. These are abstract blocks
11
//!    of text or content which can be wrapped into lines. See
12
//!    [`WordSeparator`](crate::word_separators::WordSeparator) for
13
//!    how to do this for text.
14
//!
15
//! 2. Potentially split your fragments into smaller pieces. This
16
//!    allows you to implement things like hyphenation. If you use the
17
//!    `Word` type, you can use [`WordSplitter`](crate::WordSplitter)
18
//!    enum for this.
19
//!
20
//! 3. Potentially break apart fragments that are still too large to
21
//!    fit on a single line. This is implemented in [`break_words`].
22
//!
23
//! 4. Finally take your fragments and put them into lines. There are
24
//!    two algorithms for this in the
25
//!    [`wrap_algorithms`](crate::wrap_algorithms) module:
26
//!    [`wrap_optimal_fit`](crate::wrap_algorithms::wrap_optimal_fit)
27
//!    and [`wrap_first_fit`](crate::wrap_algorithms::wrap_first_fit).
28
//!    The former produces better line breaks, the latter is faster.
29
//!
30
//! 5. Iterate through the slices returned by the wrapping functions
31
//!    and construct your lines of output.
32
//!
33
//! Please [open an issue](https://github.com/mgeisler/textwrap/) if
34
//! the functionality here is not sufficient or if you have ideas for
35
//! improving it. We would love to hear from you!
36
37
/// The CSI or “Control Sequence Introducer” introduces an ANSI escape
38
/// sequence. This is typically used for colored text and will be
39
/// ignored when computing the text width.
40
const CSI: (char, char) = ('\x1b', '[');
41
/// The final bytes of an ANSI escape sequence must be in this range.
42
const ANSI_FINAL_BYTE: std::ops::RangeInclusive<char> = '\x40'..='\x7e';
43
44
/// Skip ANSI escape sequences.
45
///
46
/// The `ch` is the current `char`, the `chars` provide the following
47
/// characters. The `chars` will be modified if `ch` is the start of
48
/// an ANSI escape sequence.
49
///
50
/// Returns `true` if one or more chars were skipped.
51
#[inline]
52
237M
pub(crate) fn skip_ansi_escape_sequence<I: Iterator<Item = char>>(ch: char, chars: &mut I) -> bool {
53
237M
    if ch != CSI.0 {
54
237M
        return false; // Nothing to skip here.
55
744k
    }
56
57
744k
    let next = chars.next();
58
744k
    if next == Some(CSI.1) {
59
        // We have found the start of an ANSI escape code, typically
60
        // used for colored terminal text. We skip until we find a
61
        // "final byte" in the range 0x40–0x7E.
62
3.36M
        for ch in chars {
63
3.34M
            if ANSI_FINAL_BYTE.contains(&ch) {
64
45.7k
                break;
65
3.29M
            }
66
        }
67
678k
    } else if next == Some(']') {
68
        // We have found the start of an Operating System Command,
69
        // which extends until the next sequence "\x1b\\" (the String
70
        // Terminator sequence) or the BEL character. The BEL
71
        // character is non-standard, but it is still used quite
72
        // often, for example, by GNU ls.
73
108k
        let mut last = ']';
74
25.7M
        for new in chars {
75
25.7M
            if new == '\x07' || (new == '\\' && last == CSI.0) {
76
78.1k
                break;
77
25.6M
            }
78
25.6M
            last = new;
79
        }
80
569k
    }
81
82
744k
    true // Indicate that some chars were skipped.
83
237M
}
textwrap::core::skip_ansi_escape_sequence::<core::iter::adapters::map::Map<&mut core::str::iter::CharIndices, <textwrap::core::Word>::break_apart::{closure#0}::{closure#0}>>
Line
Count
Source
52
28.1M
pub(crate) fn skip_ansi_escape_sequence<I: Iterator<Item = char>>(ch: char, chars: &mut I) -> bool {
53
28.1M
    if ch != CSI.0 {
54
28.1M
        return false; // Nothing to skip here.
55
44.9k
    }
56
57
44.9k
    let next = chars.next();
58
44.9k
    if next == Some(CSI.1) {
59
        // We have found the start of an ANSI escape code, typically
60
        // used for colored terminal text. We skip until we find a
61
        // "final byte" in the range 0x40–0x7E.
62
325k
        for ch in chars {
63
322k
            if ANSI_FINAL_BYTE.contains(&ch) {
64
6.28k
                break;
65
316k
            }
66
        }
67
35.1k
    } else if next == Some(']') {
68
        // We have found the start of an Operating System Command,
69
        // which extends until the next sequence "\x1b\\" (the String
70
        // Terminator sequence) or the BEL character. The BEL
71
        // character is non-standard, but it is still used quite
72
        // often, for example, by GNU ls.
73
9.48k
        let mut last = ']';
74
1.12M
        for new in chars {
75
1.11M
            if new == '\x07' || (new == '\\' && last == CSI.0) {
76
4.18k
                break;
77
1.11M
            }
78
1.11M
            last = new;
79
        }
80
25.7k
    }
81
82
44.9k
    true // Indicate that some chars were skipped.
83
28.1M
}
textwrap::core::skip_ansi_escape_sequence::<core::iter::adapters::map::Map<&mut core::str::iter::CharIndices, textwrap::word_separators::find_words_unicode_break_properties::{closure#0}::{closure#0}>>
Line
Count
Source
52
36.7M
pub(crate) fn skip_ansi_escape_sequence<I: Iterator<Item = char>>(ch: char, chars: &mut I) -> bool {
53
36.7M
    if ch != CSI.0 {
54
36.6M
        return false; // Nothing to skip here.
55
65.2k
    }
56
57
65.2k
    let next = chars.next();
58
65.2k
    if next == Some(CSI.1) {
59
        // We have found the start of an ANSI escape code, typically
60
        // used for colored terminal text. We skip until we find a
61
        // "final byte" in the range 0x40–0x7E.
62
595k
        for ch in chars {
63
595k
            if ANSI_FINAL_BYTE.contains(&ch) {
64
7.20k
                break;
65
588k
            }
66
        }
67
58.0k
    } else if next == Some(']') {
68
        // We have found the start of an Operating System Command,
69
        // which extends until the next sequence "\x1b\\" (the String
70
        // Terminator sequence) or the BEL character. The BEL
71
        // character is non-standard, but it is still used quite
72
        // often, for example, by GNU ls.
73
9.56k
        let mut last = ']';
74
1.54M
        for new in chars {
75
1.54M
            if new == '\x07' || (new == '\\' && last == CSI.0) {
76
9.56k
                break;
77
1.53M
            }
78
1.53M
            last = new;
79
        }
80
48.4k
    }
81
82
65.2k
    true // Indicate that some chars were skipped.
83
36.7M
}
textwrap::core::skip_ansi_escape_sequence::<core::str::iter::Chars>
Line
Count
Source
52
172M
pub(crate) fn skip_ansi_escape_sequence<I: Iterator<Item = char>>(ch: char, chars: &mut I) -> bool {
53
172M
    if ch != CSI.0 {
54
172M
        return false; // Nothing to skip here.
55
633k
    }
56
57
633k
    let next = chars.next();
58
633k
    if next == Some(CSI.1) {
59
        // We have found the start of an ANSI escape code, typically
60
        // used for colored terminal text. We skip until we find a
61
        // "final byte" in the range 0x40–0x7E.
62
2.44M
        for ch in chars {
63
2.42M
            if ANSI_FINAL_BYTE.contains(&ch) {
64
32.2k
                break;
65
2.39M
            }
66
        }
67
585k
    } else if next == Some(']') {
68
        // We have found the start of an Operating System Command,
69
        // which extends until the next sequence "\x1b\\" (the String
70
        // Terminator sequence) or the BEL character. The BEL
71
        // character is non-standard, but it is still used quite
72
        // often, for example, by GNU ls.
73
89.7k
        let mut last = ']';
74
23.1M
        for new in chars {
75
23.0M
            if new == '\x07' || (new == '\\' && last == CSI.0) {
76
64.4k
                break;
77
23.0M
            }
78
23.0M
            last = new;
79
        }
80
495k
    }
81
82
633k
    true // Indicate that some chars were skipped.
83
172M
}
84
85
#[cfg(feature = "unicode-width")]
86
#[inline]
87
170M
fn ch_width(ch: char) -> usize {
88
170M
    unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0)
89
170M
}
90
91
/// First character which [`ch_width`] will classify as double-width.
92
/// Please see [`display_width`].
93
#[cfg(not(feature = "unicode-width"))]
94
const DOUBLE_WIDTH_CUTOFF: char = '\u{1100}';
95
96
#[cfg(not(feature = "unicode-width"))]
97
#[inline]
98
fn ch_width(ch: char) -> usize {
99
    if ch < DOUBLE_WIDTH_CUTOFF { 1 } else { 2 }
100
}
101
102
/// Compute the display width of `text` while skipping over ANSI
103
/// escape sequences.
104
///
105
/// # Examples
106
///
107
/// ```
108
/// use textwrap::core::display_width;
109
///
110
/// assert_eq!(display_width("Café Plain"), 10);
111
/// assert_eq!(display_width("\u{1b}[31mCafé Rouge\u{1b}[0m"), 10);
112
/// assert_eq!(display_width("\x1b]8;;http://example.com\x1b\\This is a link\x1b]8;;\x1b\\"), 14);
113
/// ```
114
///
115
/// **Note:** When the `unicode-width` Cargo feature is disabled, the
116
/// width of a `char` is determined by a crude approximation which
117
/// simply counts chars below U+1100 as 1 column wide, and all other
118
/// characters as 2 columns wide. With the feature enabled, function
119
/// will correctly deal with [combining characters] in their
120
/// decomposed form (see [Unicode equivalence]).
121
///
122
/// An example of a decomposed character is “é”, which can be
123
/// decomposed into: “e” followed by a combining acute accent: “◌́”.
124
/// Without the `unicode-width` Cargo feature, every `char` below
125
/// U+1100 has a width of 1. This includes the combining accent:
126
///
127
/// ```
128
/// use textwrap::core::display_width;
129
///
130
/// assert_eq!(display_width("Cafe Plain"), 10);
131
/// #[cfg(feature = "unicode-width")]
132
/// assert_eq!(display_width("Cafe\u{301} Plain"), 10);
133
/// #[cfg(not(feature = "unicode-width"))]
134
/// assert_eq!(display_width("Cafe\u{301} Plain"), 11);
135
/// ```
136
///
137
/// ## Emojis and CJK Characters
138
///
139
/// Characters such as emojis and [CJK characters] used in the
140
/// Chinese, Japanese, and Korean languages are seen as double-width,
141
/// even if the `unicode-width` feature is disabled:
142
///
143
/// ```
144
/// use textwrap::core::display_width;
145
///
146
/// assert_eq!(display_width("😂😭🥺🤣✨😍🙏🥰😊🔥"), 20);
147
/// assert_eq!(display_width("你好"), 4);  // “Nǐ hǎo” or “Hello” in Chinese
148
/// ```
149
///
150
/// # Limitations
151
///
152
/// The displayed width of a string cannot always be computed from the
153
/// string alone. This is because the width depends on the rendering
154
/// engine used. This is particularly visible with [emoji modifier
155
/// sequences] where a base emoji is modified with, e.g., skin tone or
156
/// hair color modifiers. It is up to the rendering engine to detect
157
/// this and to produce a suitable emoji.
158
///
159
/// A simple example is “❤️”, which consists of “❤” (U+2764: Black
160
/// Heart Symbol) followed by U+FE0F (Variation Selector-16). By
161
/// itself, “❤” is a black heart, but if you follow it with the
162
/// variant selector, you may get a wider red heart.
163
///
164
/// A more complex example would be “👨‍🦰” which should depict a man
165
/// with red hair. Here the computed width is too large — and the
166
/// width differs depending on the use of the `unicode-width` feature:
167
///
168
/// ```
169
/// use textwrap::core::display_width;
170
///
171
/// assert_eq!("👨‍🦰".chars().collect::<Vec<char>>(), ['\u{1f468}', '\u{200d}', '\u{1f9b0}']);
172
/// #[cfg(feature = "unicode-width")]
173
/// assert_eq!(display_width("👨‍🦰"), 4);
174
/// #[cfg(not(feature = "unicode-width"))]
175
/// assert_eq!(display_width("👨‍🦰"), 6);
176
/// ```
177
///
178
/// This happens because the grapheme consists of three code points:
179
/// “👨” (U+1F468: Man), Zero Width Joiner (U+200D), and “🦰”
180
/// (U+1F9B0: Red Hair). You can see them above in the test. With
181
/// `unicode-width` enabled, the ZWJ is correctly seen as having zero
182
/// width, without it is counted as a double-width character.
183
///
184
/// ## Terminal Support
185
///
186
/// Modern browsers typically do a great job at combining characters
187
/// as shown above, but terminals often struggle more. As an example,
188
/// Gnome Terminal version 3.38.1, shows “❤️” as a big red heart, but
189
/// shows "👨‍🦰" as “👨🦰”.
190
///
191
/// [combining characters]: https://en.wikipedia.org/wiki/Combining_character
192
/// [Unicode equivalence]: https://en.wikipedia.org/wiki/Unicode_equivalence
193
/// [CJK characters]: https://en.wikipedia.org/wiki/CJK_characters
194
/// [emoji modifier sequences]: https://unicode.org/emoji/charts/full-emoji-modifiers.html
195
29.1M
pub fn display_width(text: &str) -> usize {
196
29.1M
    let mut chars = text.chars();
197
29.1M
    let mut width = 0;
198
153M
    while let Some(ch) = chars.next() {
199
124M
        if skip_ansi_escape_sequence(ch, &mut chars) {
200
501k
            continue;
201
124M
        }
202
124M
        width += ch_width(ch);
203
    }
204
29.1M
    width
205
29.1M
}
206
207
/// A (text) fragment denotes the unit which we wrap into lines.
208
///
209
/// Fragments represent an abstract _word_ plus the _whitespace_
210
/// following the word. In case the word falls at the end of the line,
211
/// the whitespace is dropped and a so-called _penalty_ is inserted
212
/// instead (typically `"-"` if the word was hyphenated).
213
///
214
/// For wrapping purposes, the precise content of the word, the
215
/// whitespace, and the penalty is irrelevant. All we need to know is
216
/// the displayed width of each part, which this trait provides.
217
pub trait Fragment: std::fmt::Debug {
218
    /// Displayed width of word represented by this fragment.
219
    fn width(&self) -> f64;
220
221
    /// Displayed width of the whitespace that must follow the word
222
    /// when the word is not at the end of a line.
223
    fn whitespace_width(&self) -> f64;
224
225
    /// Displayed width of the penalty that must be inserted if the
226
    /// word falls at the end of a line.
227
    fn penalty_width(&self) -> f64;
228
}
229
230
/// A piece of wrappable text, including any trailing whitespace.
231
///
232
/// A `Word` is an example of a [`Fragment`], so it has a width,
233
/// trailing whitespace, and potentially a penalty item.
234
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
235
pub struct Word<'a> {
236
    /// Word content.
237
    pub word: &'a str,
238
    /// Whitespace to insert if the word does not fall at the end of a line.
239
    pub whitespace: &'a str,
240
    /// Penalty string to insert if the word falls at the end of a line.
241
    pub penalty: &'a str,
242
    /// Cached width in columns.
243
    pub width: usize,
244
}
245
246
impl std::ops::Deref for Word<'_> {
247
    type Target = str;
248
249
56.1M
    fn deref(&self) -> &Self::Target {
250
56.1M
        self.word
251
56.1M
    }
252
}
253
254
impl<'a> Word<'a> {
255
    /// Construct a `Word` from a string.
256
    ///
257
    /// A trailing stretch of `' '` is automatically taken to be the
258
    /// whitespace part of the word.
259
12.7M
    pub fn from(word: &str) -> Word<'_> {
260
12.7M
        let trimmed = word.trim_end_matches(' ');
261
12.7M
        Word {
262
12.7M
            word: trimmed,
263
12.7M
            width: display_width(trimmed),
264
12.7M
            whitespace: &word[trimmed.len()..],
265
12.7M
            penalty: "",
266
12.7M
        }
267
12.7M
    }
268
269
    /// Break this word into smaller words with a width of at most
270
    /// `line_width`. The whitespace and penalty from this `Word` is
271
    /// added to the last piece.
272
    ///
273
    /// # Examples
274
    ///
275
    /// ```
276
    /// use textwrap::core::Word;
277
    /// assert_eq!(
278
    ///     Word::from("Hello!  ").break_apart(3).collect::<Vec<_>>(),
279
    ///     vec![Word::from("Hel"), Word::from("lo!  ")]
280
    /// );
281
    /// ```
282
2.80M
    pub fn break_apart<'b>(&'b self, line_width: usize) -> impl Iterator<Item = Word<'a>> + 'b {
283
2.80M
        let mut char_indices = self.word.char_indices();
284
2.80M
        let mut offset = 0;
285
2.80M
        let mut width = 0;
286
287
17.5M
        std::iter::from_fn(move || {
288
33.7M
            while let Some((idx, ch)) = char_indices.next() {
289
28.1M
                if skip_ansi_escape_sequence(ch, &mut char_indices.by_ref().map(|(_, ch)| ch)) {
290
44.9k
                    continue;
291
28.1M
                }
292
293
28.1M
                if width > 0 && width + ch_width(ch) > line_width {
294
11.9M
                    let word = Word {
295
11.9M
                        word: &self.word[offset..idx],
296
11.9M
                        width: width,
297
11.9M
                        whitespace: "",
298
11.9M
                        penalty: "",
299
11.9M
                    };
300
11.9M
                    offset = idx;
301
11.9M
                    width = ch_width(ch);
302
11.9M
                    return Some(word);
303
16.1M
                }
304
305
16.1M
                width += ch_width(ch);
306
            }
307
308
5.61M
            if offset < self.word.len() {
309
2.80M
                let word = Word {
310
2.80M
                    word: &self.word[offset..],
311
2.80M
                    width: width,
312
2.80M
                    whitespace: self.whitespace,
313
2.80M
                    penalty: self.penalty,
314
2.80M
                };
315
2.80M
                offset = self.word.len();
316
2.80M
                return Some(word);
317
2.80M
            }
318
319
2.80M
            None
320
17.5M
        })
321
2.80M
    }
322
}
323
324
impl Fragment for Word<'_> {
325
    #[inline]
326
29.5M
    fn width(&self) -> f64 {
327
29.5M
        self.width as f64
328
29.5M
    }
329
330
    // We assume the whitespace consist of ' ' only. This allows us to
331
    // compute the display width in constant time.
332
    #[inline]
333
224M
    fn whitespace_width(&self) -> f64 {
334
224M
        self.whitespace.len() as f64
335
224M
    }
336
337
    // We assume the penalty is `""` or `"-"`. This allows us to
338
    // compute the display width in constant time.
339
    #[inline]
340
399M
    fn penalty_width(&self) -> f64 {
341
399M
        self.penalty.len() as f64
342
399M
    }
343
}
344
345
/// Forcibly break words wider than `line_width` into smaller words.
346
///
347
/// This simply calls [`Word::break_apart`] on words that are too
348
/// wide. This means that no extra `'-'` is inserted, the word is
349
/// simply broken into smaller pieces.
350
404k
pub fn break_words<'a, I>(words: I, line_width: usize) -> Vec<Word<'a>>
351
404k
where
352
404k
    I: IntoIterator<Item = Word<'a>>,
353
{
354
404k
    let mut shortened_words = Vec::new();
355
15.1M
    for word in words {
356
14.7M
        if word.width > line_width {
357
2.80M
            shortened_words.extend(word.break_apart(line_width));
358
11.9M
        } else {
359
11.9M
            shortened_words.push(word);
360
11.9M
        }
361
    }
362
404k
    shortened_words
363
404k
}
364
365
#[cfg(test)]
366
mod tests {
367
    use super::*;
368
369
    #[cfg(feature = "unicode-width")]
370
    use unicode_width::UnicodeWidthChar;
371
372
    #[test]
373
    fn skip_ansi_escape_sequence_works() {
374
        let blue_text = "\u{1b}[34mHello\u{1b}[0m";
375
        let mut chars = blue_text.chars();
376
        let ch = chars.next().unwrap();
377
        assert!(skip_ansi_escape_sequence(ch, &mut chars));
378
        assert_eq!(chars.next(), Some('H'));
379
    }
380
381
    #[test]
382
    fn emojis_have_correct_width() {
383
        use unic_emoji_char::is_emoji;
384
385
        // Emojis in the Basic Latin (ASCII) and Latin-1 Supplement
386
        // blocks all have a width of 1 column. This includes
387
        // characters such as '#' and '©'.
388
        for ch in '\u{1}'..'\u{FF}' {
389
            if is_emoji(ch) {
390
                let desc = format!("{:?} U+{:04X}", ch, ch as u32);
391
392
                #[cfg(feature = "unicode-width")]
393
                assert_eq!(ch.width().unwrap(), 1, "char: {}", desc);
394
395
                #[cfg(not(feature = "unicode-width"))]
396
                assert_eq!(ch_width(ch), 1, "char: {}", desc);
397
            }
398
        }
399
400
        // Emojis in the remaining blocks of the Basic Multilingual
401
        // Plane (BMP), in the Supplementary Multilingual Plane (SMP),
402
        // and in the Supplementary Ideographic Plane (SIP), are all 1
403
        // or 2 columns wide when unicode-width is used, and always 2
404
        // columns wide otherwise. This includes all of our favorite
405
        // emojis such as 😊.
406
        for ch in '\u{FF}'..'\u{2FFFF}' {
407
            if is_emoji(ch) {
408
                let desc = format!("{:?} U+{:04X}", ch, ch as u32);
409
410
                #[cfg(feature = "unicode-width")]
411
                assert!(ch.width().unwrap() <= 2, "char: {}", desc);
412
413
                #[cfg(not(feature = "unicode-width"))]
414
                assert_eq!(ch_width(ch), 2, "char: {}", desc);
415
            }
416
        }
417
418
        // The remaining planes contain almost no assigned code points
419
        // and thus also no emojis.
420
    }
421
422
    #[test]
423
    fn display_width_works() {
424
        assert_eq!("Café Plain".len(), 11); // “é” is two bytes
425
        assert_eq!(display_width("Café Plain"), 10);
426
        assert_eq!(display_width("\u{1b}[31mCafé Rouge\u{1b}[0m"), 10);
427
        assert_eq!(
428
            display_width("\x1b]8;;http://example.com\x1b\\This is a link\x1b]8;;\x1b\\"),
429
            14
430
        );
431
    }
432
433
    #[test]
434
    fn display_width_narrow_emojis() {
435
        #[cfg(feature = "unicode-width")]
436
        assert_eq!(display_width("⁉"), 1);
437
438
        // The ⁉ character is above DOUBLE_WIDTH_CUTOFF.
439
        #[cfg(not(feature = "unicode-width"))]
440
        assert_eq!(display_width("⁉"), 2);
441
    }
442
443
    #[test]
444
    fn display_width_narrow_emojis_variant_selector() {
445
        #[cfg(feature = "unicode-width")]
446
        assert_eq!(display_width("⁉\u{fe0f}"), 1);
447
448
        // The variant selector-16 is also counted.
449
        #[cfg(not(feature = "unicode-width"))]
450
        assert_eq!(display_width("⁉\u{fe0f}"), 4);
451
    }
452
453
    #[test]
454
    fn display_width_emojis() {
455
        assert_eq!(display_width("😂😭🥺🤣✨😍🙏🥰😊🔥"), 20);
456
    }
457
}