/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 | | } |