tabled/settings/width/
wrap.rs

1//! This module contains [`Wrap`] structure, used to decrease width of a [`Table`]s or a cell on a [`Table`] by wrapping it's content
2//! to a new line.
3//!
4//! [`Table`]: crate::Table
5
6use papergrid::dimension::{iterable::IterGridDimension, Estimate};
7
8use crate::{
9    grid::{
10        config::{ColoredConfig, Entity, Position, SpannedConfig},
11        dimension::CompleteDimension,
12        records::{
13            vec_records::Cell, EmptyRecords, ExactRecords, IntoRecords, PeekableRecords, Records,
14            RecordsMut,
15        },
16        util::string::{get_char_width, get_string_width},
17    },
18    settings::{
19        measurement::Measurement,
20        peaker::{Peaker, PriorityNone},
21        width::Width,
22        CellOption, TableOption,
23    },
24};
25
26use super::util::get_table_total_width;
27
28/// Wrap wraps a string to a new line in case it exceeds the provided max boundary.
29/// Otherwise keeps the content of a cell untouched.
30///
31/// The function is color aware if a `color` feature is on.
32///
33/// Be aware that it doesn't consider padding.
34/// So if you want to set a exact width you might need to use [`Padding`] to set it to 0.
35///
36/// ## Example
37///
38/// ```
39/// use tabled::{Table, settings::{object::Segment, width::Width, Modify}};
40///
41/// let table = Table::new(&["Hello World!"])
42///     .with(Modify::new(Segment::all()).with(Width::wrap(3)));
43/// ```
44///
45/// [`Padding`]: crate::settings::Padding
46#[derive(Debug, Clone)]
47pub struct Wrap<W = usize, P = PriorityNone> {
48    width: W,
49    keep_words: bool,
50    priority: P,
51}
52
53impl<W> Wrap<W> {
54    /// Creates a [`Wrap`] object
55    pub fn new(width: W) -> Self
56    where
57        W: Measurement<Width>,
58    {
59        Wrap {
60            width,
61            keep_words: false,
62            priority: PriorityNone::new(),
63        }
64    }
65}
66
67impl<W, P> Wrap<W, P> {
68    /// Priority defines the logic by which a truncate will be applied when is done for the whole table.
69    ///
70    /// - [`PriorityNone`] which cuts the columns one after another.
71    /// - [`PriorityMax`] cuts the biggest columns first.
72    /// - [`PriorityMin`] cuts the lowest columns first.
73    ///
74    /// Be aware that it doesn't consider padding.
75    /// So if you want to set a exact width you might need to use [`Padding`] to set it to 0.
76    ///
77    /// [`Padding`]: crate::settings::Padding
78    /// [`PriorityMax`]: crate::settings::peaker::PriorityMax
79    /// [`PriorityMin`]: crate::settings::peaker::PriorityMin
80    pub fn priority<PP: Peaker>(self, priority: PP) -> Wrap<W, PP> {
81        Wrap {
82            width: self.width,
83            keep_words: self.keep_words,
84            priority,
85        }
86    }
87
88    /// Set the keep words option.
89    ///
90    /// If a wrapping point will be in a word, [`Wrap`] will
91    /// preserve a word (if possible) and wrap the string before it.
92    pub fn keep_words(mut self, on: bool) -> Self {
93        self.keep_words = on;
94        self
95    }
96}
97
98impl Wrap<(), ()> {
99    /// Wrap a given string
100    pub fn wrap(text: &str, width: usize, keeping_words: bool) -> String {
101        wrap_text(text, width, keeping_words)
102    }
103}
104
105impl<W, P, R> TableOption<R, ColoredConfig, CompleteDimension> for Wrap<W, P>
106where
107    W: Measurement<Width>,
108    P: Peaker,
109    R: Records + ExactRecords + PeekableRecords + RecordsMut<String>,
110    for<'a> &'a R: Records,
111    for<'a> <<&'a R as Records>::Iter as IntoRecords>::Cell: Cell + AsRef<str>,
112{
113    fn change(self, records: &mut R, cfg: &mut ColoredConfig, dims: &mut CompleteDimension) {
114        if records.count_rows() == 0 || records.count_columns() == 0 {
115            return;
116        }
117
118        let width = self.width.measure(&*records, cfg);
119
120        dims.estimate(&*records, cfg);
121        let widths = dims.get_widths().expect("must be found");
122
123        let total = get_table_total_width(widths, cfg);
124        if width >= total {
125            return;
126        }
127
128        let w = Wrap {
129            keep_words: self.keep_words,
130            priority: self.priority,
131            width,
132        };
133        let widths = wrap_total_width(records, cfg, widths, total, w);
134
135        dims.set_widths(widths);
136    }
137
138    fn hint_change(&self) -> Option<Entity> {
139        // NOTE: We need to recalculate heights
140        // TODO: It's actually could be fixed; we sort of already have new strings
141        Some(Entity::Row(0))
142    }
143}
144
145impl<W, R, P> CellOption<R, ColoredConfig> for Wrap<W, P>
146where
147    W: Measurement<Width>,
148    R: Records + ExactRecords + PeekableRecords + RecordsMut<String>,
149    for<'a> &'a R: Records,
150    for<'a> <<&'a R as Records>::Iter as IntoRecords>::Cell: AsRef<str>,
151{
152    fn change(self, records: &mut R, cfg: &mut ColoredConfig, entity: Entity) {
153        let count_rows = records.count_rows();
154        let count_columns = records.count_columns();
155        let max_pos = Position::new(count_rows, count_columns);
156
157        let width = self.width.measure(&*records, cfg);
158
159        for pos in entity.iter(count_rows, count_columns) {
160            if !max_pos.has_coverage(pos) {
161                continue;
162            }
163
164            let cell_width = records.get_width(pos);
165            if cell_width <= width {
166                continue;
167            }
168
169            let text = records.get_text(pos);
170            let wrapped = wrap_text(text, width, self.keep_words);
171            records.set(pos, wrapped);
172        }
173    }
174}
175
176fn wrap_total_width<R, P>(
177    records: &mut R,
178    cfg: &mut ColoredConfig,
179    widths: &[usize],
180    total: usize,
181    w: Wrap<usize, P>,
182) -> Vec<usize>
183where
184    R: Records + ExactRecords + PeekableRecords + RecordsMut<String>,
185    P: Peaker,
186    for<'a> &'a R: Records,
187    for<'a> <<&'a R as Records>::Iter as IntoRecords>::Cell: AsRef<str>,
188{
189    let count_rows = records.count_rows();
190    let count_columns = records.count_columns();
191
192    // TODO: Could be optimized by calculating width and min_width together
193    //       I just don't like the boiler plate we will add :(
194    //       But the benefit is clear.
195    let min_widths = IterGridDimension::width(EmptyRecords::new(count_rows, count_columns), cfg);
196
197    let mut widths = widths.to_vec();
198    decrease_widths(&mut widths, &min_widths, total, w.width, w.priority);
199
200    let points = get_decrease_cell_list(cfg, &widths, &min_widths, count_rows, count_columns);
201
202    for (pos, width) in points {
203        let text = records.get_text(pos);
204        let wrapped = wrap_text(text, width, w.keep_words);
205        records.set(pos, wrapped);
206    }
207
208    widths
209}
210
211fn wrap_text(text: &str, width: usize, keep_words: bool) -> String {
212    if width == 0 {
213        return String::new();
214    }
215
216    #[cfg(not(feature = "ansi"))]
217    {
218        if keep_words {
219            wrap_text_keeping_words_noansi(text, width)
220        } else {
221            wrap_text_basic(text, width)
222        }
223    }
224
225    #[cfg(feature = "ansi")]
226    {
227        use crate::util::string::{build_link, strip_osc};
228
229        let (text, url) = strip_osc(text);
230        let (prefix, suffix) = build_link(url);
231
232        if keep_words {
233            wrap_text_keeping_words(&text, width, &prefix, &suffix)
234        } else {
235            wrap_text_basic(&text, width, &prefix, &suffix)
236        }
237    }
238}
239
240#[cfg(not(feature = "ansi"))]
241fn wrap_text_basic(s: &str, width: usize) -> String {
242    const REPLACEMENT: char = '\u{FFFD}';
243
244    if width == 0 {
245        return String::new();
246    }
247
248    let mut buf = String::new();
249    let mut current_width = 0;
250    for c in s.chars() {
251        if c == '\n' {
252            buf.push('\n');
253            current_width = 0;
254            continue;
255        }
256
257        if current_width == width {
258            buf.push('\n');
259            current_width = 0;
260        }
261
262        let char_width = std::cmp::max(1, get_char_width(c));
263        let has_line_space = current_width + char_width <= width;
264        if !has_line_space {
265            let is_char_small = char_width <= width;
266            if !is_char_small {
267                let count_unknowns = width - current_width;
268                buf.extend(std::iter::repeat_n(REPLACEMENT, count_unknowns));
269                current_width += count_unknowns;
270            } else {
271                buf.push('\n');
272                buf.push(c);
273                current_width = char_width;
274            }
275        } else {
276            buf.push(c);
277            current_width += char_width;
278        }
279    }
280
281    buf
282}
283
284#[cfg(feature = "ansi")]
285fn wrap_text_basic(text: &str, width: usize, line_prefix: &str, line_suffix: &str) -> String {
286    use std::fmt::Write;
287
288    const REPLACEMENT: char = '\u{FFFD}';
289
290    if width == 0 || text.is_empty() {
291        return String::new();
292    }
293
294    let mut buf = String::with_capacity(width);
295    let mut line_width = 0;
296
297    buf.push_str(line_prefix);
298
299    for block in ansi_str::get_blocks(text) {
300        let style = block.style();
301        let text = block.text();
302        if text.is_empty() {
303            continue;
304        }
305
306        let available = width - line_width;
307        if available == 0 {
308            buf.push('\n');
309            line_width = 0;
310        }
311
312        let _ = write!(&mut buf, "{}", style.start());
313
314        for c in text.chars() {
315            if c == '\n' {
316                let _ = write!(&mut buf, "{}", style.end());
317                buf.push_str(line_suffix);
318                buf.push('\n');
319                line_width = 0;
320                buf.push_str(line_prefix);
321                let _ = write!(&mut buf, "{}", style.start());
322                continue;
323            }
324
325            let char_width = std::cmp::max(1, get_char_width(c));
326            let line_has_space = line_width + char_width <= width;
327            if line_has_space {
328                buf.push(c);
329                line_width += char_width;
330                continue;
331            }
332
333            let is_char_small = char_width <= width;
334            if is_char_small {
335                let _ = write!(&mut buf, "{}", style.end());
336                buf.push_str(line_suffix);
337                buf.push('\n');
338                line_width = 0;
339                buf.push_str(line_prefix);
340                let _ = write!(&mut buf, "{}", style.start());
341
342                buf.push(c);
343                line_width += char_width;
344                continue;
345            }
346
347            if line_width == width {
348                let _ = write!(&mut buf, "{}", style.end());
349                buf.push_str(line_suffix);
350                buf.push('\n');
351                line_width = 0;
352                buf.push_str(line_prefix);
353                let _ = write!(&mut buf, "{}", style.start());
354            }
355
356            let count_unknowns = width - line_width;
357            buf.extend(std::iter::repeat_n(REPLACEMENT, count_unknowns));
358            line_width += count_unknowns;
359        }
360
361        if line_width > 0 {
362            let _ = write!(&mut buf, "{}", style.end());
363        }
364    }
365
366    if line_width > 0 {
367        buf.push_str(line_suffix);
368    }
369
370    buf
371}
372
373fn wrap_text_keeping_words_noansi(text: &str, width: usize) -> String {
374    const REPLACEMENT: char = '\u{FFFD}';
375
376    if width == 0 || text.is_empty() {
377        return String::new();
378    }
379
380    let mut buf = String::with_capacity(width);
381    let mut line_width = 0;
382
383    for (i, word) in text.split(' ').enumerate() {
384        // restore space char
385        let line_has_space = line_width < width;
386        if i > 0 {
387            if line_has_space {
388                buf.push(' ');
389                line_width += 1;
390            } else {
391                buf.push('\n');
392                line_width = 0;
393
394                buf.push(' ');
395                line_width += 1;
396            }
397        }
398
399        let word_width = get_string_width(word);
400
401        let line_has_space = line_width + word_width <= width;
402        if line_has_space {
403            buf.push_str(word);
404            line_width += word_width;
405            continue;
406        }
407
408        let is_small_word = word_width <= width;
409        if is_small_word {
410            buf.push('\n');
411            line_width = 0;
412
413            buf.push_str(word);
414            line_width += word_width;
415            continue;
416        }
417
418        for c in word.chars() {
419            // take 1 char by 1 and just push it
420
421            let char_width = std::cmp::max(1, get_char_width(c));
422            let line_has_space = line_width + char_width <= width;
423            if !line_has_space {
424                let is_char_small = char_width <= width;
425                if !is_char_small {
426                    if line_width == width {
427                        buf.push('\n');
428                        line_width = 0;
429                    }
430
431                    // NOTE:
432                    // Practically it only can happen if we wrap some late UTF8 symbol.
433                    // For example:
434                    // Emojie with width 2 but and wrap width 1
435                    let available = width - line_width;
436                    buf.extend(std::iter::repeat_n(REPLACEMENT, available));
437                    line_width = width;
438                    continue;
439                }
440
441                buf.push('\n');
442                line_width = 0;
443            }
444
445            buf.push(c);
446            line_width += char_width;
447        }
448    }
449
450    buf
451}
452
453#[cfg(feature = "ansi")]
454fn wrap_text_keeping_words(text: &str, width: usize, prefix: &str, suffix: &str) -> String {
455    const REPLACEMENT: char = '\u{FFFD}';
456
457    struct Blocks<'a> {
458        iter: ansi_str::AnsiBlockIter<'a>,
459        last_block: Option<BlockSlice<'a>>,
460    }
461
462    struct BlockSlice<'a> {
463        block: ansi_str::AnsiBlock<'a>,
464        pos: usize,
465    }
466
467    impl<'a> Blocks<'a> {
468        fn new(text: &'a str) -> Self {
469            Self {
470                iter: ansi_str::get_blocks(text),
471                last_block: None,
472            }
473        }
474
475        fn read(&mut self, buf: &mut String, nbytes: usize) {
476            use std::fmt::Write;
477
478            debug_assert_ne!(nbytes, 0);
479
480            let mut size: usize = nbytes;
481
482            if let Some(mut b) = self.last_block.take() {
483                let text = b.block.text();
484                let slice = &text[b.pos..];
485                let slice_size = slice.len();
486
487                match slice_size.cmp(&size) {
488                    std::cmp::Ordering::Less => {
489                        buf.push_str(slice);
490                        let _ = write!(buf, "{}", b.block.style().end());
491                        size -= slice_size;
492                    }
493                    std::cmp::Ordering::Equal => {
494                        buf.push_str(slice);
495                        let _ = write!(buf, "{}", b.block.style().end());
496                        return;
497                    }
498                    std::cmp::Ordering::Greater => {
499                        let truncated = &slice[..size];
500                        buf.push_str(truncated);
501                        b.pos += size;
502                        self.last_block = Some(b);
503                        return;
504                    }
505                }
506            }
507
508            for block in self.iter.by_ref() {
509                let text = block.text();
510                if text.is_empty() {
511                    continue;
512                }
513
514                let text_size = text.len();
515
516                match text_size.cmp(&size) {
517                    std::cmp::Ordering::Less => {
518                        let _ = write!(buf, "{}", block.style().start());
519                        buf.push_str(text);
520                        let _ = write!(buf, "{}", block.style().end());
521                        size -= text_size;
522                    }
523                    std::cmp::Ordering::Equal => {
524                        let _ = write!(buf, "{}", block.style().start());
525                        buf.push_str(text);
526                        let _ = write!(buf, "{}", block.style().end());
527                        return;
528                    }
529                    std::cmp::Ordering::Greater => {
530                        let _ = write!(buf, "{}", block.style().start());
531                        let slice = &text[..size];
532                        buf.push_str(slice);
533                        self.last_block = Some(BlockSlice { block, pos: size });
534
535                        return;
536                    }
537                }
538            }
539        }
540
541        fn read_char(&mut self, buf: &mut String) {
542            use std::fmt::Write;
543
544            if let Some(mut b) = self.last_block.take() {
545                let text = b.block.text();
546                let slice = &text[b.pos..];
547
548                let mut chars = slice.chars();
549                let ch = chars.next().expect("ok");
550                let ch_size = ch.len_utf8();
551
552                buf.push(ch);
553
554                if chars.next().is_none() {
555                    let _ = write!(buf, "{}", b.block.style().end());
556                    return;
557                } else {
558                    debug_assert_ne!(ch_size, 0);
559
560                    b.pos += ch_size;
561                    self.last_block = Some(b);
562
563                    return;
564                }
565            }
566
567            for block in self.iter.by_ref() {
568                let text = block.text();
569                if text.is_empty() {
570                    continue;
571                }
572
573                let mut chars = text.chars();
574                let ch = chars.next().expect("ok");
575                let ch_size = ch.len_utf8();
576
577                let _ = write!(buf, "{}", block.style().start());
578                buf.push(ch);
579
580                if chars.next().is_none() {
581                    let _ = write!(buf, "{}", block.style().end());
582                    return;
583                } else {
584                    debug_assert_ne!(ch_size, 0);
585
586                    self.last_block = Some(BlockSlice {
587                        block,
588                        pos: ch_size,
589                    });
590                    return;
591                }
592            }
593        }
594
595        fn skip(&mut self, buf: &mut String, nbytes: usize) {
596            use std::fmt::Write;
597
598            debug_assert_ne!(nbytes, 0);
599
600            let mut size = nbytes;
601
602            if let Some(mut b) = self.last_block.take() {
603                let text = b.block.text();
604                let slice = &text[b.pos..];
605                let slice_size = slice.len();
606
607                match slice_size.cmp(&size) {
608                    std::cmp::Ordering::Less => {
609                        let _ = write!(buf, "{}", b.block.style().end());
610                        size -= slice_size;
611                    }
612                    std::cmp::Ordering::Equal => {
613                        let _ = write!(buf, "{}", b.block.style().end());
614                        return;
615                    }
616                    std::cmp::Ordering::Greater => {
617                        b.pos += size;
618                        self.last_block = Some(b);
619                        return;
620                    }
621                }
622            }
623
624            for block in self.iter.by_ref() {
625                let text = block.text();
626                let text_size = text.len();
627
628                if text.is_empty() {
629                    continue;
630                }
631
632                match text_size.cmp(&size) {
633                    std::cmp::Ordering::Less => {
634                        size -= text_size;
635                    }
636                    std::cmp::Ordering::Equal => {
637                        return;
638                    }
639                    std::cmp::Ordering::Greater => {
640                        let _ = write!(buf, "{}", block.style().start());
641                        self.last_block = Some(BlockSlice { block, pos: size });
642                        return;
643                    }
644                }
645            }
646        }
647
648        fn finish(&mut self, buf: &mut String) {
649            use std::fmt::Write;
650
651            if let Some(b) = &mut self.last_block {
652                let _ = write!(buf, "{}", b.block.style().end());
653            }
654        }
655
656        fn start(&mut self, buf: &mut String) {
657            use std::fmt::Write;
658
659            if let Some(b) = &mut self.last_block {
660                let _ = write!(buf, "{}", b.block.style().start());
661            }
662        }
663    }
664
665    if width == 0 || text.is_empty() {
666        return String::new();
667    }
668
669    let stripped = ansi_str::AnsiStr::ansi_strip(text);
670    let is_simple_text = stripped.len() == text.len() && prefix.is_empty() && suffix.is_empty();
671    if is_simple_text {
672        return wrap_text_keeping_words_noansi(text, width);
673    }
674
675    let mut buf = String::with_capacity(width + prefix.len() + suffix.len());
676    let mut line_width = 0;
677    let mut blocks = Blocks::new(text);
678
679    buf.push_str(prefix);
680
681    for (i, word) in stripped.split(' ').enumerate() {
682        let word_width = get_string_width(word);
683        let word_size = word.len();
684
685        // restore space char if we can
686        if i > 0 {
687            let line_has_space = line_width < width;
688            if line_has_space {
689                blocks.read(&mut buf, 1);
690                line_width += 1;
691            } else {
692                blocks.finish(&mut buf);
693                buf.push_str(suffix);
694                buf.push('\n');
695                buf.push_str(prefix);
696                blocks.start(&mut buf);
697                blocks.read(&mut buf, 1);
698                line_width = 1;
699            }
700        }
701
702        if word_width == 0 {
703            continue;
704        }
705
706        let line_has_space = line_width + word_width <= width;
707        if line_has_space {
708            blocks.read(&mut buf, word_size);
709            line_width += word_width;
710            continue;
711        }
712
713        let is_small_word = word_width <= width;
714        if is_small_word {
715            blocks.finish(&mut buf);
716            buf.push_str(suffix);
717            buf.push('\n');
718            buf.push_str(prefix);
719            blocks.start(&mut buf);
720            blocks.read(&mut buf, word_size);
721            line_width = word_width;
722            continue;
723        }
724
725        // take 1 char by 1 and just push it
726        for c in word.chars() {
727            let char_width = std::cmp::max(1, get_char_width(c));
728            let char_size = c.len_utf8();
729
730            let line_has_space = line_width + char_width <= width;
731            if line_has_space {
732                blocks.read_char(&mut buf);
733                line_width += char_width;
734                continue;
735            }
736
737            let is_char_small = char_width <= width;
738            if is_char_small {
739                blocks.finish(&mut buf);
740                buf.push_str(suffix);
741                buf.push('\n');
742                buf.push_str(prefix);
743                blocks.start(&mut buf);
744                blocks.read_char(&mut buf);
745                line_width = char_width;
746                continue;
747            }
748
749            if line_width == width {
750                blocks.finish(&mut buf);
751                buf.push_str(suffix);
752                buf.push('\n');
753                buf.push_str(prefix);
754                blocks.start(&mut buf);
755                line_width = 0;
756            }
757
758            // NOTE:
759            // Practically it only can happen if we wrap some late UTF8 symbol.
760            // For example:
761            // Emojie with width 2 but and wrap width 1
762            let available = width - line_width;
763            buf.extend(std::iter::repeat_n(REPLACEMENT, available));
764            line_width = width;
765            blocks.skip(&mut buf, char_size);
766        }
767    }
768
769    buf.push_str(suffix);
770
771    buf
772}
773
774fn decrease_widths<F>(
775    widths: &mut [usize],
776    min_widths: &[usize],
777    total_width: usize,
778    mut width: usize,
779    mut peeaker: F,
780) where
781    F: Peaker,
782{
783    let mut empty_list = 0;
784    for col in 0..widths.len() {
785        if widths[col] == 0 || widths[col] <= min_widths[col] {
786            empty_list += 1;
787        }
788    }
789
790    while width != total_width {
791        if empty_list == widths.len() {
792            break;
793        }
794
795        let col = match peeaker.peak(min_widths, widths) {
796            Some(col) => col,
797            None => break,
798        };
799
800        if widths[col] == 0 || widths[col] <= min_widths[col] {
801            continue;
802        }
803
804        widths[col] -= 1;
805
806        if widths[col] == 0 || widths[col] <= min_widths[col] {
807            empty_list += 1;
808        }
809
810        width += 1;
811    }
812}
813
814fn get_decrease_cell_list(
815    cfg: &SpannedConfig,
816    widths: &[usize],
817    min_widths: &[usize],
818    count_rows: usize,
819    count_columns: usize,
820) -> Vec<(Position, usize)> {
821    let mut points = Vec::new();
822    for col in 0..count_columns {
823        for row in 0..count_rows {
824            let pos = Position::new(row, col);
825            if !cfg.is_cell_visible(pos) {
826                continue;
827            }
828
829            let (width, width_min) = match cfg.get_column_span(pos) {
830                Some(span) => {
831                    let width = (col..col + span).map(|i| widths[i]).sum::<usize>();
832                    let min_width = (col..col + span).map(|i| min_widths[i]).sum::<usize>();
833                    let count_borders = count_borders(cfg, col, col + span, count_columns);
834                    (width + count_borders, min_width + count_borders)
835                }
836                None => (widths[col], min_widths[col]),
837            };
838
839            if width >= width_min {
840                let padding = cfg.get_padding(pos);
841                let width = width.saturating_sub(padding.left.size + padding.right.size);
842
843                points.push((pos, width));
844            }
845        }
846    }
847
848    points
849}
850
851fn count_borders(cfg: &SpannedConfig, start: usize, end: usize, count_columns: usize) -> usize {
852    (start..end)
853        .skip(1)
854        .filter(|&i| cfg.has_vertical(i, count_columns))
855        .count()
856}
857
858#[cfg(test)]
859mod tests {
860    use super::*;
861
862    #[test]
863    fn split_test() {
864        #[cfg(not(feature = "ansi"))]
865        let split = |text, width| wrap_text_basic(text, width);
866
867        #[cfg(feature = "ansi")]
868        let split = |text, width| wrap_text_basic(text, width, "", "");
869
870        assert_eq!(split("123456", 0), "");
871
872        assert_eq!(split("123456", 1), "1\n2\n3\n4\n5\n6");
873        assert_eq!(split("123456", 2), "12\n34\n56");
874        assert_eq!(split("12345", 2), "12\n34\n5");
875        assert_eq!(split("123456", 6), "123456");
876        assert_eq!(split("123456", 10), "123456");
877
878        assert_eq!(split("😳😳😳😳😳", 1), "�\n�\n�\n�\n�");
879        assert_eq!(split("😳😳😳😳😳", 2), "😳\n😳\n😳\n😳\n😳");
880        assert_eq!(split("😳😳😳😳😳", 3), "😳\n😳\n😳\n😳\n😳");
881        assert_eq!(split("😳😳😳😳😳", 6), "😳😳😳\n😳😳");
882        assert_eq!(split("😳😳😳😳😳", 20), "😳😳😳😳😳");
883
884        assert_eq!(split("😳123😳", 1), "�\n1\n2\n3\n�");
885        assert_eq!(split("😳12😳3", 1), "�\n1\n2\n�\n3");
886    }
887
888    #[test]
889    fn chunks_test() {
890        #[allow(clippy::redundant_closure)]
891        #[cfg(not(feature = "ansi"))]
892        let chunks = |text, width| wrap_text_basic(text, width);
893
894        #[cfg(feature = "ansi")]
895        let chunks = |text, width| wrap_text_basic(text, width, "", "");
896
897        assert_eq!(chunks("123456", 0), "");
898
899        assert_eq!(
900            chunks("123456", 1),
901            ["1", "2", "3", "4", "5", "6"].join("\n")
902        );
903        assert_eq!(chunks("123456", 2), ["12", "34", "56"].join("\n"));
904        assert_eq!(chunks("12345", 2), ["12", "34", "5"].join("\n"));
905
906        assert_eq!(
907            chunks("😳😳😳😳😳", 1),
908            ["�", "�", "�", "�", "�"].join("\n")
909        );
910        assert_eq!(
911            chunks("😳😳😳😳😳", 2),
912            ["😳", "😳", "😳", "😳", "😳"].join("\n")
913        );
914        assert_eq!(
915            chunks("😳😳😳😳😳", 3),
916            ["😳", "😳", "😳", "😳", "😳"].join("\n")
917        );
918    }
919
920    #[test]
921    fn split_by_line_keeping_words_test() {
922        #[cfg(not(feature = "ansi"))]
923        let split_keeping_words = |text, width| wrap_text_keeping_words_noansi(text, width);
924        #[cfg(feature = "ansi")]
925        let split_keeping_words = |text, width| wrap_text_keeping_words(text, width, "", "");
926
927        assert_eq!(split_keeping_words("123456", 1), "1\n2\n3\n4\n5\n6");
928        assert_eq!(split_keeping_words("123456", 2), "12\n34\n56");
929        assert_eq!(split_keeping_words("12345", 2), "12\n34\n5");
930
931        assert_eq!(split_keeping_words("😳😳😳😳😳", 1), "�\n�\n�\n�\n�");
932
933        assert_eq!(split_keeping_words("111 234 1", 4), "111 \n234 \n1");
934    }
935
936    #[cfg(feature = "ansi")]
937    #[test]
938    fn split_by_line_keeping_words_color_test() {
939        let split_keeping_words = |text, width| wrap_text_keeping_words(text, width, "", "");
940
941        let text = "\u{1b}[36mJapanese “vacancy” button\u{1b}[0m";
942        assert_eq!(split_keeping_words(text, 2), "\u{1b}[36mJa\u{1b}[39m\n\u{1b}[36mpa\u{1b}[39m\n\u{1b}[36mne\u{1b}[39m\n\u{1b}[36mse\u{1b}[39m\n\u{1b}[36m “\u{1b}[39m\n\u{1b}[36mva\u{1b}[39m\n\u{1b}[36mca\u{1b}[39m\n\u{1b}[36mnc\u{1b}[39m\n\u{1b}[36my”\u{1b}[39m\n\u{1b}[36m b\u{1b}[39m\n\u{1b}[36mut\u{1b}[39m\n\u{1b}[36mto\u{1b}[39m\n\u{1b}[36mn\u{1b}[39m");
943        assert_eq!(split_keeping_words(text, 1), "\u{1b}[36mJ\u{1b}[39m\n\u{1b}[36ma\u{1b}[39m\n\u{1b}[36mp\u{1b}[39m\n\u{1b}[36ma\u{1b}[39m\n\u{1b}[36mn\u{1b}[39m\n\u{1b}[36me\u{1b}[39m\n\u{1b}[36ms\u{1b}[39m\n\u{1b}[36me\u{1b}[39m\n\u{1b}[36m \u{1b}[39m\n\u{1b}[36m“\u{1b}[39m\n\u{1b}[36mv\u{1b}[39m\n\u{1b}[36ma\u{1b}[39m\n\u{1b}[36mc\u{1b}[39m\n\u{1b}[36ma\u{1b}[39m\n\u{1b}[36mn\u{1b}[39m\n\u{1b}[36mc\u{1b}[39m\n\u{1b}[36my\u{1b}[39m\n\u{1b}[36m”\u{1b}[39m\n\u{1b}[36m \u{1b}[39m\n\u{1b}[36mb\u{1b}[39m\n\u{1b}[36mu\u{1b}[39m\n\u{1b}[36mt\u{1b}[39m\n\u{1b}[36mt\u{1b}[39m\n\u{1b}[36mo\u{1b}[39m\n\u{1b}[36mn\u{1b}[39m");
944    }
945
946    #[cfg(feature = "ansi")]
947    #[test]
948    fn split_by_line_keeping_words_color_2_test() {
949        use ansi_str::AnsiStr;
950
951        let split_keeping_words = |text, width| wrap_text_keeping_words(text, width, "", "");
952
953        let text = "\u{1b}[37mTigre Ecuador   OMYA Andina     3824909999      Calcium carbonate       Colombia\u{1b}[0m";
954
955        assert_eq!(
956            split_keeping_words(text, 2)
957                .ansi_split("\n")
958                .collect::<Vec<_>>(),
959            [
960                "\u{1b}[37mTi\u{1b}[39m",
961                "\u{1b}[37mgr\u{1b}[39m",
962                "\u{1b}[37me \u{1b}[39m",
963                "\u{1b}[37mEc\u{1b}[39m",
964                "\u{1b}[37mua\u{1b}[39m",
965                "\u{1b}[37mdo\u{1b}[39m",
966                "\u{1b}[37mr \u{1b}[39m",
967                "\u{1b}[37m  \u{1b}[39m",
968                "\u{1b}[37mOM\u{1b}[39m",
969                "\u{1b}[37mYA\u{1b}[39m",
970                "\u{1b}[37m A\u{1b}[39m",
971                "\u{1b}[37mnd\u{1b}[39m",
972                "\u{1b}[37min\u{1b}[39m",
973                "\u{1b}[37ma \u{1b}[39m",
974                "\u{1b}[37m  \u{1b}[39m",
975                "\u{1b}[37m  \u{1b}[39m",
976                "\u{1b}[37m38\u{1b}[39m",
977                "\u{1b}[37m24\u{1b}[39m",
978                "\u{1b}[37m90\u{1b}[39m",
979                "\u{1b}[37m99\u{1b}[39m",
980                "\u{1b}[37m99\u{1b}[39m",
981                "\u{1b}[37m  \u{1b}[39m",
982                "\u{1b}[37m  \u{1b}[39m",
983                "\u{1b}[37m  \u{1b}[39m",
984                "\u{1b}[37mCa\u{1b}[39m",
985                "\u{1b}[37mlc\u{1b}[39m",
986                "\u{1b}[37miu\u{1b}[39m",
987                "\u{1b}[37mm \u{1b}[39m",
988                "\u{1b}[37mca\u{1b}[39m",
989                "\u{1b}[37mrb\u{1b}[39m",
990                "\u{1b}[37mon\u{1b}[39m",
991                "\u{1b}[37mat\u{1b}[39m",
992                "\u{1b}[37me \u{1b}[39m",
993                "\u{1b}[37m  \u{1b}[39m",
994                "\u{1b}[37m  \u{1b}[39m",
995                "\u{1b}[37m  \u{1b}[39m",
996                "\u{1b}[37mCo\u{1b}[39m",
997                "\u{1b}[37mlo\u{1b}[39m",
998                "\u{1b}[37mmb\u{1b}[39m",
999                "\u{1b}[37mia\u{1b}[39m"
1000            ]
1001        );
1002
1003        assert_eq!(
1004            split_keeping_words(text, 1)
1005                .ansi_split("\n")
1006                .collect::<Vec<_>>(),
1007            [
1008                "\u{1b}[37mT\u{1b}[39m",
1009                "\u{1b}[37mi\u{1b}[39m",
1010                "\u{1b}[37mg\u{1b}[39m",
1011                "\u{1b}[37mr\u{1b}[39m",
1012                "\u{1b}[37me\u{1b}[39m",
1013                "\u{1b}[37m \u{1b}[39m",
1014                "\u{1b}[37mE\u{1b}[39m",
1015                "\u{1b}[37mc\u{1b}[39m",
1016                "\u{1b}[37mu\u{1b}[39m",
1017                "\u{1b}[37ma\u{1b}[39m",
1018                "\u{1b}[37md\u{1b}[39m",
1019                "\u{1b}[37mo\u{1b}[39m",
1020                "\u{1b}[37mr\u{1b}[39m",
1021                "\u{1b}[37m \u{1b}[39m",
1022                "\u{1b}[37m \u{1b}[39m",
1023                "\u{1b}[37m \u{1b}[39m",
1024                "\u{1b}[37mO\u{1b}[39m",
1025                "\u{1b}[37mM\u{1b}[39m",
1026                "\u{1b}[37mY\u{1b}[39m",
1027                "\u{1b}[37mA\u{1b}[39m",
1028                "\u{1b}[37m \u{1b}[39m",
1029                "\u{1b}[37mA\u{1b}[39m",
1030                "\u{1b}[37mn\u{1b}[39m",
1031                "\u{1b}[37md\u{1b}[39m",
1032                "\u{1b}[37mi\u{1b}[39m",
1033                "\u{1b}[37mn\u{1b}[39m",
1034                "\u{1b}[37ma\u{1b}[39m",
1035                "\u{1b}[37m \u{1b}[39m",
1036                "\u{1b}[37m \u{1b}[39m",
1037                "\u{1b}[37m \u{1b}[39m",
1038                "\u{1b}[37m \u{1b}[39m",
1039                "\u{1b}[37m \u{1b}[39m",
1040                "\u{1b}[37m3\u{1b}[39m",
1041                "\u{1b}[37m8\u{1b}[39m",
1042                "\u{1b}[37m2\u{1b}[39m",
1043                "\u{1b}[37m4\u{1b}[39m",
1044                "\u{1b}[37m9\u{1b}[39m",
1045                "\u{1b}[37m0\u{1b}[39m",
1046                "\u{1b}[37m9\u{1b}[39m",
1047                "\u{1b}[37m9\u{1b}[39m",
1048                "\u{1b}[37m9\u{1b}[39m",
1049                "\u{1b}[37m9\u{1b}[39m",
1050                "\u{1b}[37m \u{1b}[39m",
1051                "\u{1b}[37m \u{1b}[39m",
1052                "\u{1b}[37m \u{1b}[39m",
1053                "\u{1b}[37m \u{1b}[39m",
1054                "\u{1b}[37m \u{1b}[39m",
1055                "\u{1b}[37m \u{1b}[39m",
1056                "\u{1b}[37mC\u{1b}[39m",
1057                "\u{1b}[37ma\u{1b}[39m",
1058                "\u{1b}[37ml\u{1b}[39m",
1059                "\u{1b}[37mc\u{1b}[39m",
1060                "\u{1b}[37mi\u{1b}[39m",
1061                "\u{1b}[37mu\u{1b}[39m",
1062                "\u{1b}[37mm\u{1b}[39m",
1063                "\u{1b}[37m \u{1b}[39m",
1064                "\u{1b}[37mc\u{1b}[39m",
1065                "\u{1b}[37ma\u{1b}[39m",
1066                "\u{1b}[37mr\u{1b}[39m",
1067                "\u{1b}[37mb\u{1b}[39m",
1068                "\u{1b}[37mo\u{1b}[39m",
1069                "\u{1b}[37mn\u{1b}[39m",
1070                "\u{1b}[37ma\u{1b}[39m",
1071                "\u{1b}[37mt\u{1b}[39m",
1072                "\u{1b}[37me\u{1b}[39m",
1073                "\u{1b}[37m \u{1b}[39m",
1074                "\u{1b}[37m \u{1b}[39m",
1075                "\u{1b}[37m \u{1b}[39m",
1076                "\u{1b}[37m \u{1b}[39m",
1077                "\u{1b}[37m \u{1b}[39m",
1078                "\u{1b}[37m \u{1b}[39m",
1079                "\u{1b}[37m \u{1b}[39m",
1080                "\u{1b}[37mC\u{1b}[39m",
1081                "\u{1b}[37mo\u{1b}[39m",
1082                "\u{1b}[37ml\u{1b}[39m",
1083                "\u{1b}[37mo\u{1b}[39m",
1084                "\u{1b}[37mm\u{1b}[39m",
1085                "\u{1b}[37mb\u{1b}[39m",
1086                "\u{1b}[37mi\u{1b}[39m",
1087                "\u{1b}[37ma\u{1b}[39m"
1088            ]
1089        )
1090    }
1091
1092    #[cfg(feature = "ansi")]
1093    #[test]
1094    fn split_by_line_keeping_words_color_3_test() {
1095        let split = |text, width| wrap_text_keeping_words(text, width, "", "");
1096        assert_eq!(
1097            split(
1098                "\u{1b}[37m🚵🏻🚵🏻🚵🏻🚵🏻🚵🏻🚵🏻🚵🏻🚵🏻🚵🏻🚵🏻\u{1b}[0m",
1099                3,
1100            ),
1101            "\u{1b}[37m🚵\u{1b}[39m\n\u{1b}[37m🏻\u{1b}[39m\n\u{1b}[37m🚵\u{1b}[39m\n\u{1b}[37m🏻\u{1b}[39m\n\u{1b}[37m🚵\u{1b}[39m\n\u{1b}[37m🏻\u{1b}[39m\n\u{1b}[37m🚵\u{1b}[39m\n\u{1b}[37m🏻\u{1b}[39m\n\u{1b}[37m🚵\u{1b}[39m\n\u{1b}[37m🏻\u{1b}[39m\n\u{1b}[37m🚵\u{1b}[39m\n\u{1b}[37m🏻\u{1b}[39m\n\u{1b}[37m🚵\u{1b}[39m\n\u{1b}[37m🏻\u{1b}[39m\n\u{1b}[37m🚵\u{1b}[39m\n\u{1b}[37m🏻\u{1b}[39m\n\u{1b}[37m🚵\u{1b}[39m\n\u{1b}[37m🏻\u{1b}[39m\n\u{1b}[37m🚵\u{1b}[39m\n\u{1b}[37m🏻\u{1b}[39m",
1102        );
1103        assert_eq!(
1104            split("\u{1b}[37mthis is a long sentence\u{1b}[0m", 7),
1105            "\u{1b}[37mthis is\u{1b}[39m\n\u{1b}[37m a long\u{1b}[39m\n\u{1b}[37m senten\u{1b}[39m\n\u{1b}[37mce\u{1b}[39m"
1106        );
1107        assert_eq!(
1108            split("\u{1b}[37mHello World\u{1b}[0m", 7),
1109            "\u{1b}[37mHello \u{1b}[39m\n\u{1b}[37mWorld\u{1b}[39m"
1110        );
1111        assert_eq!(
1112            split("\u{1b}[37mHello Wo\u{1b}[37mrld\u{1b}[0m", 7),
1113            "\u{1b}[37mHello \u{1b}[39m\n\u{1b}[37mWo\u{1b}[39m\u{1b}[37mrld\u{1b}[39m"
1114        );
1115        assert_eq!(
1116            split("\u{1b}[37mHello Wo\u{1b}[37mrld\u{1b}[0m", 8),
1117            "\u{1b}[37mHello \u{1b}[39m\n\u{1b}[37mWo\u{1b}[39m\u{1b}[37mrld\u{1b}[39m"
1118        );
1119    }
1120
1121    #[test]
1122    fn split_keeping_words_4_test() {
1123        #[cfg(feature = "ansi")]
1124        let split_keeping_words = |text, width| wrap_text_keeping_words(text, width, "", "");
1125        #[cfg(not(feature = "ansi"))]
1126        let split_keeping_words = |text, width| wrap_text_keeping_words_noansi(text, width);
1127
1128        assert_eq!(split_keeping_words("12345678", 3,), "123\n456\n78");
1129        assert_eq!(split_keeping_words("12345678", 2,), "12\n34\n56\n78");
1130    }
1131
1132    #[cfg(feature = "ansi")]
1133    #[test]
1134    fn chunks_test_with_prefix_and_suffix() {
1135        assert_eq!(wrap_text_basic("123456", 0, "^", "$"), ["^$"; 0].join("\n"));
1136
1137        assert_eq!(
1138            wrap_text_basic("123456", 1, "^", "$"),
1139            ["^1$", "^2$", "^3$", "^4$", "^5$", "^6$"].join("\n")
1140        );
1141        assert_eq!(
1142            wrap_text_basic("123456", 2, "^", "$"),
1143            ["^12$", "^34$", "^56$"].join("\n")
1144        );
1145        assert_eq!(
1146            wrap_text_basic("12345", 2, "^", "$"),
1147            ["^12$", "^34$", "^5$"].join("\n")
1148        );
1149
1150        assert_eq!(
1151            wrap_text_basic("😳😳😳😳😳", 1, "^", "$"),
1152            ["^�$", "^�$", "^�$", "^�$", "^�$"].join("\n")
1153        );
1154        assert_eq!(
1155            wrap_text_basic("😳😳😳😳😳", 2, "^", "$"),
1156            ["^😳$", "^😳$", "^😳$", "^😳$", "^😳$"].join("\n")
1157        );
1158        assert_eq!(
1159            wrap_text_basic("😳😳😳😳😳", 3, "^", "$"),
1160            "^😳$\n^😳$\n^😳$\n^😳$\n^😳$"
1161        );
1162    }
1163
1164    #[cfg(feature = "ansi")]
1165    #[test]
1166    fn split_by_line_keeping_words_test_with_prefix_and_suffix() {
1167        assert_eq!(
1168            wrap_text_keeping_words("123456", 1, "^", "$"),
1169            "^1$\n^2$\n^3$\n^4$\n^5$\n^6$"
1170        );
1171        assert_eq!(
1172            wrap_text_keeping_words("123456", 2, "^", "$"),
1173            "^12$\n^34$\n^56$"
1174        );
1175        assert_eq!(
1176            wrap_text_keeping_words("12345", 2, "^", "$"),
1177            "^12$\n^34$\n^5$"
1178        );
1179
1180        assert_eq!(
1181            wrap_text_keeping_words("😳😳😳😳😳", 1, "^", "$"),
1182            "^�$\n^�$\n^�$\n^�$\n^�$"
1183        );
1184    }
1185
1186    #[cfg(feature = "ansi")]
1187    #[test]
1188    fn split_by_line_keeping_words_color_2_test_with_prefix_and_suffix() {
1189        use ansi_str::AnsiStr;
1190
1191        let text = "\u{1b}[37mTigre Ecuador   OMYA Andina     3824909999      Calcium carbonate       Colombia\u{1b}[0m";
1192
1193        assert_eq!(
1194            wrap_text_keeping_words(text, 2, "^", "$")
1195                .ansi_split("\n")
1196                .collect::<Vec<_>>(),
1197            [
1198                "^\u{1b}[37mTi\u{1b}[39m$",
1199                "^\u{1b}[37mgr\u{1b}[39m$",
1200                "^\u{1b}[37me \u{1b}[39m$",
1201                "^\u{1b}[37mEc\u{1b}[39m$",
1202                "^\u{1b}[37mua\u{1b}[39m$",
1203                "^\u{1b}[37mdo\u{1b}[39m$",
1204                "^\u{1b}[37mr \u{1b}[39m$",
1205                "^\u{1b}[37m  \u{1b}[39m$",
1206                "^\u{1b}[37mOM\u{1b}[39m$",
1207                "^\u{1b}[37mYA\u{1b}[39m$",
1208                "^\u{1b}[37m A\u{1b}[39m$",
1209                "^\u{1b}[37mnd\u{1b}[39m$",
1210                "^\u{1b}[37min\u{1b}[39m$",
1211                "^\u{1b}[37ma \u{1b}[39m$",
1212                "^\u{1b}[37m  \u{1b}[39m$",
1213                "^\u{1b}[37m  \u{1b}[39m$",
1214                "^\u{1b}[37m38\u{1b}[39m$",
1215                "^\u{1b}[37m24\u{1b}[39m$",
1216                "^\u{1b}[37m90\u{1b}[39m$",
1217                "^\u{1b}[37m99\u{1b}[39m$",
1218                "^\u{1b}[37m99\u{1b}[39m$",
1219                "^\u{1b}[37m  \u{1b}[39m$",
1220                "^\u{1b}[37m  \u{1b}[39m$",
1221                "^\u{1b}[37m  \u{1b}[39m$",
1222                "^\u{1b}[37mCa\u{1b}[39m$",
1223                "^\u{1b}[37mlc\u{1b}[39m$",
1224                "^\u{1b}[37miu\u{1b}[39m$",
1225                "^\u{1b}[37mm \u{1b}[39m$",
1226                "^\u{1b}[37mca\u{1b}[39m$",
1227                "^\u{1b}[37mrb\u{1b}[39m$",
1228                "^\u{1b}[37mon\u{1b}[39m$",
1229                "^\u{1b}[37mat\u{1b}[39m$",
1230                "^\u{1b}[37me \u{1b}[39m$",
1231                "^\u{1b}[37m  \u{1b}[39m$",
1232                "^\u{1b}[37m  \u{1b}[39m$",
1233                "^\u{1b}[37m  \u{1b}[39m$",
1234                "^\u{1b}[37mCo\u{1b}[39m$",
1235                "^\u{1b}[37mlo\u{1b}[39m$",
1236                "^\u{1b}[37mmb\u{1b}[39m$",
1237                "^\u{1b}[37mia\u{1b}[39m$"
1238            ]
1239        );
1240
1241        assert_eq!(
1242            wrap_text_keeping_words(text, 1, "^", "$")
1243                .ansi_split("\n")
1244                .collect::<Vec<_>>(),
1245            [
1246                "^\u{1b}[37mT\u{1b}[39m$",
1247                "^\u{1b}[37mi\u{1b}[39m$",
1248                "^\u{1b}[37mg\u{1b}[39m$",
1249                "^\u{1b}[37mr\u{1b}[39m$",
1250                "^\u{1b}[37me\u{1b}[39m$",
1251                "^\u{1b}[37m \u{1b}[39m$",
1252                "^\u{1b}[37mE\u{1b}[39m$",
1253                "^\u{1b}[37mc\u{1b}[39m$",
1254                "^\u{1b}[37mu\u{1b}[39m$",
1255                "^\u{1b}[37ma\u{1b}[39m$",
1256                "^\u{1b}[37md\u{1b}[39m$",
1257                "^\u{1b}[37mo\u{1b}[39m$",
1258                "^\u{1b}[37mr\u{1b}[39m$",
1259                "^\u{1b}[37m \u{1b}[39m$",
1260                "^\u{1b}[37m \u{1b}[39m$",
1261                "^\u{1b}[37m \u{1b}[39m$",
1262                "^\u{1b}[37mO\u{1b}[39m$",
1263                "^\u{1b}[37mM\u{1b}[39m$",
1264                "^\u{1b}[37mY\u{1b}[39m$",
1265                "^\u{1b}[37mA\u{1b}[39m$",
1266                "^\u{1b}[37m \u{1b}[39m$",
1267                "^\u{1b}[37mA\u{1b}[39m$",
1268                "^\u{1b}[37mn\u{1b}[39m$",
1269                "^\u{1b}[37md\u{1b}[39m$",
1270                "^\u{1b}[37mi\u{1b}[39m$",
1271                "^\u{1b}[37mn\u{1b}[39m$",
1272                "^\u{1b}[37ma\u{1b}[39m$",
1273                "^\u{1b}[37m \u{1b}[39m$",
1274                "^\u{1b}[37m \u{1b}[39m$",
1275                "^\u{1b}[37m \u{1b}[39m$",
1276                "^\u{1b}[37m \u{1b}[39m$",
1277                "^\u{1b}[37m \u{1b}[39m$",
1278                "^\u{1b}[37m3\u{1b}[39m$",
1279                "^\u{1b}[37m8\u{1b}[39m$",
1280                "^\u{1b}[37m2\u{1b}[39m$",
1281                "^\u{1b}[37m4\u{1b}[39m$",
1282                "^\u{1b}[37m9\u{1b}[39m$",
1283                "^\u{1b}[37m0\u{1b}[39m$",
1284                "^\u{1b}[37m9\u{1b}[39m$",
1285                "^\u{1b}[37m9\u{1b}[39m$",
1286                "^\u{1b}[37m9\u{1b}[39m$",
1287                "^\u{1b}[37m9\u{1b}[39m$",
1288                "^\u{1b}[37m \u{1b}[39m$",
1289                "^\u{1b}[37m \u{1b}[39m$",
1290                "^\u{1b}[37m \u{1b}[39m$",
1291                "^\u{1b}[37m \u{1b}[39m$",
1292                "^\u{1b}[37m \u{1b}[39m$",
1293                "^\u{1b}[37m \u{1b}[39m$",
1294                "^\u{1b}[37mC\u{1b}[39m$",
1295                "^\u{1b}[37ma\u{1b}[39m$",
1296                "^\u{1b}[37ml\u{1b}[39m$",
1297                "^\u{1b}[37mc\u{1b}[39m$",
1298                "^\u{1b}[37mi\u{1b}[39m$",
1299                "^\u{1b}[37mu\u{1b}[39m$",
1300                "^\u{1b}[37mm\u{1b}[39m$",
1301                "^\u{1b}[37m \u{1b}[39m$",
1302                "^\u{1b}[37mc\u{1b}[39m$",
1303                "^\u{1b}[37ma\u{1b}[39m$",
1304                "^\u{1b}[37mr\u{1b}[39m$",
1305                "^\u{1b}[37mb\u{1b}[39m$",
1306                "^\u{1b}[37mo\u{1b}[39m$",
1307                "^\u{1b}[37mn\u{1b}[39m$",
1308                "^\u{1b}[37ma\u{1b}[39m$",
1309                "^\u{1b}[37mt\u{1b}[39m$",
1310                "^\u{1b}[37me\u{1b}[39m$",
1311                "^\u{1b}[37m \u{1b}[39m$",
1312                "^\u{1b}[37m \u{1b}[39m$",
1313                "^\u{1b}[37m \u{1b}[39m$",
1314                "^\u{1b}[37m \u{1b}[39m$",
1315                "^\u{1b}[37m \u{1b}[39m$",
1316                "^\u{1b}[37m \u{1b}[39m$",
1317                "^\u{1b}[37m \u{1b}[39m$",
1318                "^\u{1b}[37mC\u{1b}[39m$",
1319                "^\u{1b}[37mo\u{1b}[39m$",
1320                "^\u{1b}[37ml\u{1b}[39m$",
1321                "^\u{1b}[37mo\u{1b}[39m$",
1322                "^\u{1b}[37mm\u{1b}[39m$",
1323                "^\u{1b}[37mb\u{1b}[39m$",
1324                "^\u{1b}[37mi\u{1b}[39m$",
1325                "^\u{1b}[37ma\u{1b}[39m$"
1326            ]
1327        )
1328    }
1329
1330    #[cfg(feature = "ansi")]
1331    #[test]
1332    fn chunks_wrap_2() {
1333        let text = "\u{1b}[30mDebian\u{1b}[0m\u{1b}[31mDebian\u{1b}[0m\u{1b}[32mDebian\u{1b}[0m\u{1b}[33mDebian\u{1b}[0m\u{1b}[34mDebian\u{1b}[0m\u{1b}[35mDebian\u{1b}[0m\u{1b}[36mDebian\u{1b}[0m\u{1b}[37mDebian\u{1b}[0m\u{1b}[40mDebian\u{1b}[0m\u{1b}[41mDebian\u{1b}[0m\u{1b}[42mDebian\u{1b}[0m\u{1b}[43mDebian\u{1b}[0m\u{1b}[44mDebian\u{1b}[0m";
1334        assert_eq!(
1335            wrap_text_basic(text, 30, "", ""),
1336            [
1337                "\u{1b}[30mDebian\u{1b}[39m\u{1b}[31mDebian\u{1b}[39m\u{1b}[32mDebian\u{1b}[39m\u{1b}[33mDebian\u{1b}[39m\u{1b}[34mDebian\u{1b}[39m",
1338                "\u{1b}[35mDebian\u{1b}[39m\u{1b}[36mDebian\u{1b}[39m\u{1b}[37mDebian\u{1b}[39m\u{1b}[40mDebian\u{1b}[49m\u{1b}[41mDebian\u{1b}[49m",
1339                "\u{1b}[42mDebian\u{1b}[49m\u{1b}[43mDebian\u{1b}[49m\u{1b}[44mDebian\u{1b}[49m",
1340            ].join("\n")
1341        );
1342    }
1343
1344    #[cfg(feature = "ansi")]
1345    #[test]
1346    fn chunks_wrap_3() {
1347        let text = "\u{1b}[37mCreate bytes from the \u{1b}[0m\u{1b}[7;34marg\u{1b}[0m\u{1b}[37muments.\u{1b}[0m";
1348
1349        assert_eq!(
1350            wrap_text_basic(text, 22, "", ""),
1351            [
1352                "\u{1b}[37mCreate bytes from the \u{1b}[39m",
1353                "\u{1b}[7m\u{1b}[34marg\u{1b}[27m\u{1b}[39m\u{1b}[37muments.\u{1b}[39m"
1354            ]
1355            .join("\n")
1356        );
1357    }
1358
1359    #[cfg(feature = "ansi")]
1360    #[test]
1361    fn chunks_wrap_3_keeping_words() {
1362        let text = "\u{1b}[37mCreate bytes from the \u{1b}[0m\u{1b}[7;34marg\u{1b}[0m\u{1b}[37muments.\u{1b}[0m";
1363
1364        assert_eq!(
1365            wrap_text_keeping_words(text, 22, "", ""),
1366            "\u{1b}[37mCreate bytes from the \u{1b}[39m\n\u{1b}[7m\u{1b}[34marg\u{1b}[27m\u{1b}[39m\u{1b}[37muments.\u{1b}[39m"
1367        );
1368    }
1369
1370    #[cfg(feature = "ansi")]
1371    #[test]
1372    fn chunks_wrap_4() {
1373        let text = "\u{1b}[37mReturns the floor of a number (l\u{1b}[0m\u{1b}[41;37marg\u{1b}[0m\u{1b}[37mest integer less than or equal to that number).\u{1b}[0m";
1374
1375        assert_eq!(
1376            wrap_text_basic(text, 10, "", ""),
1377            [
1378                "\u{1b}[37mReturns th\u{1b}[39m",
1379                "\u{1b}[37me floor of\u{1b}[39m",
1380                "\u{1b}[37m a number \u{1b}[39m",
1381                "\u{1b}[37m(l\u{1b}[39m\u{1b}[37m\u{1b}[41marg\u{1b}[39m\u{1b}[49m\u{1b}[37mest i\u{1b}[39m",
1382                "\u{1b}[37mnteger les\u{1b}[39m",
1383                "\u{1b}[37ms than or \u{1b}[39m",
1384                "\u{1b}[37mequal to t\u{1b}[39m",
1385                "\u{1b}[37mhat number\u{1b}[39m",
1386                "\u{1b}[37m).\u{1b}[39m",
1387            ].join("\n")
1388        );
1389    }
1390
1391    #[cfg(feature = "ansi")]
1392    #[test]
1393    fn chunks_wrap_4_keeping_words() {
1394        let text = "\u{1b}[37mReturns the floor of a number (l\u{1b}[0m\u{1b}[41;37marg\u{1b}[0m\u{1b}[37mest integer less than or equal to that number).\u{1b}[0m";
1395        assert_eq!(
1396            wrap_text_keeping_words(text, 10, "", ""),
1397            concat!(
1398                "\u{1b}[37mReturns \u{1b}[39m\n",
1399                "\u{1b}[37mthe floor \u{1b}[39m\n",
1400                "\u{1b}[37mof a \u{1b}[39m\n",
1401                "\u{1b}[37mnumber \u{1b}[39m\n",
1402                "\u{1b}[37m(l\u{1b}[39m\u{1b}[37m\u{1b}[41marg\u{1b}[39m\u{1b}[49m\u{1b}[37mest \u{1b}[39m\n",
1403                "\u{1b}[37minteger \u{1b}[39m\n",
1404                "\u{1b}[37mless than \u{1b}[39m\n",
1405                "\u{1b}[37mor equal \u{1b}[39m\n",
1406                "\u{1b}[37mto that \u{1b}[39m\n",
1407                "\u{1b}[37mnumber).\u{1b}[39m",
1408            )
1409        );
1410    }
1411
1412    #[test]
1413    fn chunks_wrap_5_keeping_words() {
1414        #[cfg(feature = "ansi")]
1415        let split_keeping_words = |text, width| wrap_text_keeping_words(text, width, "", "");
1416        #[cfg(not(feature = "ansi"))]
1417        let split_keeping_words = |text, width| wrap_text_keeping_words_noansi(text, width);
1418
1419        let text = "修复 zlib 软件包中 CMake 配置不一致的问题,该问题先前导致部分软件无法正常构建";
1420        assert_eq!(
1421            split_keeping_words(text, 44),
1422            "修复 zlib 软件包中 CMake 配置不一致的问题,\n该问题先前导致部分软件无法正常构建"
1423        );
1424    }
1425
1426    #[test]
1427    fn chunks_chinese_0() {
1428        #[cfg(feature = "ansi")]
1429        let split_keeping_words = |text, width| wrap_text_keeping_words(text, width, "", "");
1430        #[cfg(not(feature = "ansi"))]
1431        let split_keeping_words = |text, width| wrap_text_keeping_words_noansi(text, width);
1432
1433        let text = "(公司{ 名称:\"腾讯科技(深圳)有限公司\",成立时间:\"1998年11月\"}";
1434        assert_eq!(
1435            split_keeping_words(text, 40),
1436            concat!(
1437                "(公司{ 名称:\"腾讯科技(深圳)有限公司\",\n",
1438                "成立时间:\"1998年11月\"}",
1439            ),
1440        );
1441    }
1442
1443    #[test]
1444    fn chunks_keeping_chinese_0() {
1445        #[cfg(feature = "ansi")]
1446        let split_keeping_words = |text, width| wrap_text_keeping_words(text, width, "", "");
1447        #[cfg(not(feature = "ansi"))]
1448        let split_keeping_words = |text, width| wrap_text_keeping_words_noansi(text, width);
1449
1450        let text = "(公司{ 名称:\"腾讯科技(深圳)有限公司\",成立时间:\"1998年11月\"}";
1451        assert_eq!(
1452            split_keeping_words(text, 40),
1453            concat!(
1454                "(公司{ 名称:\"腾讯科技(深圳)有限公司\",\n",
1455                "成立时间:\"1998年11月\"}",
1456            ),
1457        );
1458    }
1459}