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