ansi_cut/
lib.rs

1//! # Ansi-cut
2//!
3//! A library for cutting a string while preserving colors.
4//!
5//! ## Example
6//!
7//! ```
8//! use ansi_cut::AnsiCut;
9//! use owo_colors::{colors::*, OwoColorize};
10//!
11//! let colored_text = "When the night has come"
12//!     .fg::<Black>()
13//!     .bg::<White>()
14//!     .to_string();
15//!
16//! let cutted_text = colored_text.cut(5..);
17//!
18//! println!("{}", cutted_text);
19//! ```
20
21use ansi_parser::AnsiSequence;
22use ansi_parser::{AnsiParser, Output};
23use std::ops::{Bound, RangeBounds};
24
25/// AnsiCut a trait to cut a string while keeping information
26/// about its color defined as ANSI control sequences.
27pub trait AnsiCut {
28    /// Cut string from the beginning of the range to the end.
29    /// Preserving its colors.
30    ///
31    /// Range is defined in terms of `byte`s of the string not containing ANSI
32    /// control sequences.
33    ///
34    /// Exceeding an upper bound does not panic.
35    ///
36    /// # Panics
37    ///
38    /// Panics if a start or end indexes are not on a UTF-8 code point boundary.
39    ///
40    /// # Examples
41    ///
42    /// ```rust
43    /// use owo_colors::{OwoColorize, colors::*};
44    /// use ansi_cut::AnsiCut;
45    /// let colored_text = format!("{} {} {}", "A".fg::<Black>(), "Colored".fg::<Red>(), "Text".fg::<Blue>()).bg::<Yellow>().to_string();
46    /// let cut_text = colored_text.cut(5..);
47    /// println!("{}", cut_text);
48    /// ```
49    fn cut<R>(&self, range: R) -> String
50    where
51        R: RangeBounds<usize>;
52}
53
54impl AnsiCut for &str {
55    fn cut<R>(&self, range: R) -> String
56    where
57        R: RangeBounds<usize>,
58    {
59        crate::cut(&self, range)
60    }
61}
62
63impl AnsiCut for String {
64    fn cut<R>(&self, range: R) -> String
65    where
66        R: RangeBounds<usize>,
67    {
68        crate::cut(&self, range)
69    }
70}
71
72/// Returns an Vec over chunk_size elements of string, starting at the beginning of the slice.
73/// It uses chars but not bytes!
74///
75/// The chunks are vectors and do not overlap.
76/// If chunk_size does not divide the length of the slice, then the last chunk will not have length chunk_size.
77///
78/// # Panics
79///
80/// Panics if chunk_size is 0.
81///
82/// # Examples
83///
84/// ```rust
85/// use owo_colors::{OwoColorize, colors::*};
86/// let colored_text = format!("{} {} {}", "A".fg::<Black>(), "Colored".fg::<Red>(), "Text".fg::<Blue>()).bg::<Yellow>().to_string();
87/// let chunks = ansi_cut::chunks(&colored_text, 3);
88/// for chunk in &chunks {
89///     println!("{}", chunk);
90/// }
91/// ```
92pub fn chunks(s: &str, chunk_size: usize) -> Vec<String> {
93    assert!(chunk_size > 0);
94
95    let stripped = srip_ansi_sequences(s);
96    let count_chars = stripped.chars().count();
97    let mut chunks = Vec::new();
98    let mut start_pos = 0;
99
100    while start_pos < count_chars {
101        let start = stripped
102            .chars()
103            .map(|c| c.len_utf8())
104            .take(start_pos)
105            .sum::<usize>();
106        let end_pos = std::cmp::min(start_pos + chunk_size, count_chars);
107        let end = stripped
108            .chars()
109            .map(|c| c.len_utf8())
110            .take(end_pos)
111            .sum::<usize>();
112        let part = s.cut(start..end);
113        start_pos = end_pos;
114
115        if part.is_empty() {
116            break;
117        }
118
119        chunks.push(part);
120    }
121
122    chunks
123}
124
125// Bounds are byte index
126// It's not safe to go over grapheme boundres.
127fn cut<S, R>(string: S, bounds: R) -> String
128where
129    S: AsRef<str>,
130    R: RangeBounds<usize>,
131{
132    let string = string.as_ref();
133    let (start, end) = bounds_to_usize(bounds.start_bound(), bounds.end_bound());
134
135    cut_str(string, start, end)
136}
137
138fn cut_str(string: &str, lower_bound: usize, upper_bound: Option<usize>) -> String {
139    let mut asci_state = AnsiState::default();
140    let tokens = string.ansi_parse();
141    let mut buf = String::new();
142    let mut index = 0;
143
144    '_tokens_loop: for token in tokens {
145        match token {
146            Output::TextBlock(text) => {
147                let block_end_index = index + text.len();
148                if lower_bound > block_end_index {
149                    index += text.len();
150                    continue;
151                };
152
153                let mut start = 0;
154                if lower_bound > index {
155                    start = lower_bound - index;
156                }
157
158                let mut end = text.len();
159                let mut done = false;
160                if let Some(upper_bound) = upper_bound {
161                    if upper_bound > index && upper_bound < block_end_index {
162                        end = upper_bound - index;
163                        done = true;
164                    }
165                }
166
167                index += text.len();
168
169                match text.get(start..end) {
170                    Some(text) => {
171                        buf.push_str(text);
172                        if done {
173                            break '_tokens_loop;
174                        }
175                    }
176                    None => {
177                        panic!("One of indexes are not on a UTF-8 code point boundary");
178                    }
179                }
180            }
181            Output::Escape(seq) => {
182                let seq_str = seq.to_string();
183                buf.push_str(&seq_str);
184                if let AnsiSequence::SetGraphicsMode(v) = seq {
185                    update_ansi_state(&mut asci_state, v.as_ref());
186                }
187            }
188        }
189    }
190
191    complete_ansi_sequences(&asci_state, &mut buf);
192
193    buf
194}
195
196#[derive(Debug, Clone, Default)]
197struct AnsiState {
198    fg_color: Option<AnsiColor>,
199    bg_color: Option<AnsiColor>,
200    undr_color: Option<AnsiColor>,
201    bold: bool,
202    faint: bool,
203    italic: bool,
204    underline: bool,
205    double_underline: bool,
206    slow_blink: bool,
207    rapid_blink: bool,
208    inverse: bool,
209    hide: bool,
210    crossedout: bool,
211    reset: bool,
212    framed: bool,
213    encircled: bool,
214    font: Option<u8>,
215    fraktur: bool,
216    proportional_spacing: bool,
217    overlined: bool,
218    igrm_underline: bool,
219    igrm_double_underline: bool,
220    igrm_overline: bool,
221    igrm_double_overline: bool,
222    igrm_stress_marking: bool,
223    superscript: bool,
224    subscript: bool,
225    unknown: bool,
226}
227
228#[derive(Debug, Clone, PartialEq, Eq)]
229enum AnsiColor {
230    Bit4 { index: u8 },
231    Bit8 { index: u8 },
232    Bit24 { r: u8, g: u8, b: u8 },
233}
234
235fn update_ansi_state(state: &mut AnsiState, mode: &[u8]) {
236    let mut ptr = mode;
237    loop {
238        if ptr.is_empty() {
239            break;
240        }
241
242        let tag = ptr[0];
243
244        match tag {
245            0 => {
246                *state = AnsiState::default();
247                state.reset = true;
248            }
249            1 => state.bold = true,
250            2 => state.faint = true,
251            3 => state.italic = true,
252            4 => state.underline = true,
253            5 => state.slow_blink = true,
254            6 => state.rapid_blink = true,
255            7 => state.inverse = true,
256            8 => state.hide = true,
257            9 => state.crossedout = true,
258            10 => state.font = None,
259            n @ 11..=19 => state.font = Some(n),
260            20 => state.fraktur = true,
261            21 => state.double_underline = true,
262            22 => {
263                state.faint = false;
264                state.bold = false;
265            }
266            23 => {
267                state.italic = false;
268            }
269            24 => {
270                state.underline = false;
271                state.double_underline = false;
272            }
273            25 => {
274                state.slow_blink = false;
275                state.rapid_blink = false;
276            }
277            26 => {
278                state.proportional_spacing = true;
279            }
280            28 => {
281                state.inverse = false;
282            }
283            29 => {
284                state.crossedout = false;
285            }
286            n @ 30..=37 | n @ 90..=97 => {
287                state.fg_color = Some(AnsiColor::Bit4 { index: n });
288            }
289            38 => {
290                if let Some((color, n)) = parse_ansi_color(ptr) {
291                    state.fg_color = Some(color);
292                    ptr = &ptr[n..];
293                }
294            }
295            39 => {
296                state.fg_color = None;
297            }
298            n @ 40..=47 | n @ 100..=107 => {
299                state.bg_color = Some(AnsiColor::Bit4 { index: n });
300            }
301            48 => {
302                if let Some((color, n)) = parse_ansi_color(ptr) {
303                    state.bg_color = Some(color);
304                    ptr = &ptr[n..];
305                }
306            }
307            49 => {
308                state.bg_color = None;
309            }
310            50 => {
311                state.proportional_spacing = false;
312            }
313            51 => {
314                state.framed = true;
315            }
316            52 => {
317                state.encircled = true;
318            }
319            53 => {
320                state.overlined = true;
321            }
322            54 => {
323                state.encircled = false;
324                state.framed = false;
325            }
326            55 => {
327                state.overlined = false;
328            }
329            58 => {
330                if let Some((color, n)) = parse_ansi_color(ptr) {
331                    state.undr_color = Some(color);
332                    ptr = &ptr[n..];
333                }
334            }
335            59 => {
336                state.undr_color = None;
337            }
338            60 => {
339                state.igrm_underline = true;
340            }
341            61 => {
342                state.igrm_double_underline = true;
343            }
344            62 => {
345                state.igrm_overline = true;
346            }
347            63 => {
348                state.igrm_double_overline = true;
349            }
350            64 => {
351                state.igrm_stress_marking = true;
352            }
353            65 => {
354                state.igrm_underline = false;
355                state.igrm_double_underline = false;
356                state.igrm_overline = false;
357                state.igrm_double_overline = false;
358                state.igrm_stress_marking = false;
359            }
360            73 => {
361                state.superscript = true;
362            }
363            74 => {
364                state.subscript = true;
365            }
366            75 => {
367                state.subscript = false;
368                state.superscript = false;
369            }
370            _ => {
371                state.unknown = true;
372            }
373        }
374
375        ptr = &ptr[1..];
376    }
377}
378
379fn parse_ansi_color(buf: &[u8]) -> Option<(AnsiColor, usize)> {
380    match buf {
381        [b'2', b';', index, ..] => Some((AnsiColor::Bit8 { index: *index }, 3)),
382        [b'5', b';', r, b';', g, b';', b, ..] => Some((
383            AnsiColor::Bit24 {
384                r: *r,
385                g: *g,
386                b: *b,
387            },
388            7,
389        )),
390        _ => None,
391    }
392}
393
394fn complete_ansi_sequences(state: &AnsiState, buf: &mut String) {
395    macro_rules! emit_static {
396        ($s:expr) => {
397            buf.push_str(concat!("\u{1b}[", $s, "m"))
398        };
399    }
400
401    if state.unknown && state.reset {
402        emit_static!("0");
403    }
404
405    if state.font.is_some() {
406        emit_static!("10");
407    }
408
409    if state.bold || state.faint {
410        emit_static!("22");
411    }
412
413    if state.italic {
414        emit_static!("23");
415    }
416
417    if state.underline || state.double_underline {
418        emit_static!("24");
419    }
420
421    if state.slow_blink || state.rapid_blink {
422        emit_static!("25");
423    }
424
425    if state.inverse {
426        emit_static!("28");
427    }
428
429    if state.crossedout {
430        emit_static!("29");
431    }
432
433    if state.fg_color.is_some() {
434        emit_static!("39");
435    }
436
437    if state.bg_color.is_some() {
438        emit_static!("49");
439    }
440
441    if state.proportional_spacing {
442        emit_static!("50");
443    }
444
445    if state.encircled || state.framed {
446        emit_static!("54");
447    }
448
449    if state.overlined {
450        emit_static!("55");
451    }
452
453    if state.igrm_underline
454        || state.igrm_double_underline
455        || state.igrm_overline
456        || state.igrm_double_overline
457        || state.igrm_stress_marking
458    {
459        emit_static!("65");
460    }
461
462    if state.undr_color.is_some() {
463        emit_static!("59");
464    }
465
466    if state.subscript || state.superscript {
467        emit_static!("75");
468    }
469
470    if state.unknown {
471        emit_static!("0");
472    }
473}
474
475fn bounds_to_usize(left: Bound<&usize>, right: Bound<&usize>) -> (usize, Option<usize>) {
476    match (left, right) {
477        (Bound::Included(x), Bound::Included(y)) => (*x, Some(y + 1)),
478        (Bound::Included(x), Bound::Excluded(y)) => (*x, Some(*y)),
479        (Bound::Included(x), Bound::Unbounded) => (*x, None),
480        (Bound::Unbounded, Bound::Unbounded) => (0, None),
481        (Bound::Unbounded, Bound::Included(y)) => (0, Some(y + 1)),
482        (Bound::Unbounded, Bound::Excluded(y)) => (0, Some(*y)),
483        (Bound::Excluded(_), Bound::Unbounded)
484        | (Bound::Excluded(_), Bound::Included(_))
485        | (Bound::Excluded(_), Bound::Excluded(_)) => {
486            unreachable!("A start bound can't be excluded")
487        }
488    }
489}
490
491fn srip_ansi_sequences(string: &str) -> String {
492    let tokens = string.ansi_parse();
493    let mut buf = String::new();
494    for token in tokens {
495        match token {
496            Output::TextBlock(text) => {
497                buf.push_str(text);
498            }
499            Output::Escape(_) => {}
500        }
501    }
502
503    buf
504}
505
506#[cfg(test)]
507mod tests {
508    use super::*;
509
510    #[test]
511    fn parse_ansi_color_test() {
512        let tests: Vec<(&[u8], _)> = vec![
513            (&[b'2', b';', 200], Some(AnsiColor::Bit8 { index: 200 })),
514            (
515                &[b'2', b';', 100, b';', 123, b';', 39],
516                Some(AnsiColor::Bit8 { index: 100 }),
517            ),
518            (
519                &[b'2', b';', 100, 1, 2, 3],
520                Some(AnsiColor::Bit8 { index: 100 }),
521            ),
522            (&[b'2', b';'], None),
523            (&[b'2', 1, 2, 3], None),
524            (&[b'2'], None),
525            (
526                &[b'5', b';', 100, b';', 123, b';', 39],
527                Some(AnsiColor::Bit24 {
528                    r: 100,
529                    g: 123,
530                    b: 39,
531                }),
532            ),
533            (
534                &[b'5', b';', 100, b';', 123, b';', 39, 1, 2, 3],
535                Some(AnsiColor::Bit24 {
536                    r: 100,
537                    g: 123,
538                    b: 39,
539                }),
540            ),
541            (
542                &[b'5', b';', 100, b';', 123, b';', 39, 1, 2, 3],
543                Some(AnsiColor::Bit24 {
544                    r: 100,
545                    g: 123,
546                    b: 39,
547                }),
548            ),
549            (&[b'5', b';', 100, b';', 123, b';'], None),
550            (&[b'5', b';', 100, b';', 123], None),
551            (&[b'5', b';', 100, b';'], None),
552            (&[b'5', b';', 100], None),
553            (&[b'5', b';'], None),
554            (&[b'5'], None),
555            (&[], None),
556        ];
557
558        for (i, (bytes, expected)) in tests.into_iter().enumerate() {
559            assert_eq!(parse_ansi_color(bytes).map(|a| a.0), expected, "test={}", i);
560        }
561    }
562
563    #[test]
564    fn cut_colored_fg_test() {
565        let colored_s = "\u{1b}[30mTEXT\u{1b}[39m";
566        assert_eq!(colored_s, colored_s.cut(..));
567        assert_eq!(colored_s, colored_s.cut(0..4));
568        assert_eq!("\u{1b}[30mEXT\u{1b}[39m", colored_s.cut(1..));
569        assert_eq!("\u{1b}[30mTEX\u{1b}[39m", colored_s.cut(..3));
570        assert_eq!("\u{1b}[30mEX\u{1b}[39m", colored_s.cut(1..3));
571
572        assert_eq!("TEXT", srip_ansi_sequences(&colored_s.cut(..)));
573        assert_eq!("TEX", srip_ansi_sequences(&colored_s.cut(..3)));
574        assert_eq!("EX", srip_ansi_sequences(&colored_s.cut(1..3)));
575
576        let colored_s = "\u{1b}[30mTEXT\u{1b}[39m \u{1b}[31mTEXT\u{1b}[39m";
577        assert_eq!(colored_s, colored_s.cut(..));
578        assert_eq!(colored_s, colored_s.cut(0..9));
579        assert_eq!(
580            "\u{1b}[30mXT\u{1b}[39m \u{1b}[31mTEXT\u{1b}[39m",
581            colored_s.cut(2..)
582        );
583        assert_eq!(
584            "\u{1b}[30mTEXT\u{1b}[39m \u{1b}[31mT\u{1b}[39m",
585            colored_s.cut(..6)
586        );
587        assert_eq!(
588            "\u{1b}[30mXT\u{1b}[39m \u{1b}[31mT\u{1b}[39m",
589            colored_s.cut(2..6)
590        );
591
592        assert_eq!("TEXT TEXT", srip_ansi_sequences(&colored_s.cut(..)));
593        assert_eq!("TEXT T", srip_ansi_sequences(&colored_s.cut(..6)));
594        assert_eq!("XT T", srip_ansi_sequences(&colored_s.cut(2..6)));
595
596        assert_eq!("\u{1b}[30m\u{1b}[39m", cut("\u{1b}[30m\u{1b}[39m", ..));
597    }
598
599    #[test]
600    fn cut_colored_bg_test() {
601        let colored_s = "\u{1b}[40mTEXT\u{1b}[49m";
602        assert_eq!(colored_s, colored_s.cut(..));
603        assert_eq!(colored_s, colored_s.cut(0..4));
604        assert_eq!("\u{1b}[40mEXT\u{1b}[49m", colored_s.cut(1..));
605        assert_eq!("\u{1b}[40mTEX\u{1b}[49m", colored_s.cut(..3));
606        assert_eq!("\u{1b}[40mEX\u{1b}[49m", colored_s.cut(1..3));
607
608        // todo: determine if this is the right behaviour
609        assert_eq!("\u{1b}[40m\u{1b}[49m", colored_s.cut(3..3));
610
611        assert_eq!("TEXT", srip_ansi_sequences(&colored_s.cut(..)));
612        assert_eq!("TEX", srip_ansi_sequences(&colored_s.cut(..3)));
613        assert_eq!("EX", srip_ansi_sequences(&colored_s.cut(1..3)));
614
615        let colored_s = "\u{1b}[40mTEXT\u{1b}[49m \u{1b}[41mTEXT\u{1b}[49m";
616        assert_eq!(colored_s, colored_s.cut(..));
617        assert_eq!(colored_s, colored_s.cut(0..9));
618        assert_eq!(
619            "\u{1b}[40mXT\u{1b}[49m \u{1b}[41mTEXT\u{1b}[49m",
620            colored_s.cut(2..)
621        );
622        assert_eq!(
623            "\u{1b}[40mTEXT\u{1b}[49m \u{1b}[41mT\u{1b}[49m",
624            colored_s.cut(..6)
625        );
626        assert_eq!(
627            "\u{1b}[40mXT\u{1b}[49m \u{1b}[41mT\u{1b}[49m",
628            colored_s.cut(2..6)
629        );
630
631        assert_eq!("TEXT TEXT", srip_ansi_sequences(&colored_s.cut(..)));
632        assert_eq!("TEXT T", srip_ansi_sequences(&colored_s.cut(..6)));
633        assert_eq!("XT T", srip_ansi_sequences(&colored_s.cut(2..6)));
634
635        assert_eq!("\u{1b}[40m\u{1b}[49m", cut("\u{1b}[40m\u{1b}[49m", ..));
636    }
637
638    #[test]
639    fn cut_colored_bg_fg_test() {
640        let colored_s = "\u{1b}[31;40mTEXT\u{1b}[0m";
641        assert_eq!(colored_s, colored_s.cut(..));
642        assert_eq!(colored_s, colored_s.cut(0..4));
643        assert_eq!("\u{1b}[31;40mEXT\u{1b}[0m", colored_s.cut(1..));
644        assert_eq!("\u{1b}[31;40mTEX\u{1b}[39m\u{1b}[49m", colored_s.cut(..3));
645        assert_eq!("\u{1b}[31;40mEX\u{1b}[39m\u{1b}[49m", colored_s.cut(1..3));
646
647        assert_eq!("TEXT", srip_ansi_sequences(&colored_s.cut(..)));
648        assert_eq!("TEX", srip_ansi_sequences(&colored_s.cut(..3)));
649        assert_eq!("EX", srip_ansi_sequences(&colored_s.cut(1..3)));
650
651        let colored_s = "\u{1b}[31;40mTEXT\u{1b}[0m \u{1b}[34;42mTEXT\u{1b}[0m";
652        assert_eq!(colored_s, colored_s.cut(..));
653        assert_eq!(colored_s, colored_s.cut(0..9));
654        assert_eq!(
655            "\u{1b}[31;40mXT\u{1b}[0m \u{1b}[34;42mTEXT\u{1b}[0m",
656            colored_s.cut(2..)
657        );
658        assert_eq!(
659            "\u{1b}[31;40mTEXT\u{1b}[0m \u{1b}[34;42mT\u{1b}[39m\u{1b}[49m",
660            colored_s.cut(..6)
661        );
662        assert_eq!(
663            "\u{1b}[31;40mXT\u{1b}[0m \u{1b}[34;42mT\u{1b}[39m\u{1b}[49m",
664            colored_s.cut(2..6)
665        );
666
667        assert_eq!("TEXT TEXT", srip_ansi_sequences(&colored_s.cut(..)));
668        assert_eq!("TEXT T", srip_ansi_sequences(&colored_s.cut(..6)));
669        assert_eq!("XT T", srip_ansi_sequences(&colored_s.cut(2..6)));
670
671        assert_eq!("\u{1b}[40m\u{1b}[49m", cut("\u{1b}[40m\u{1b}[49m", ..));
672    }
673
674    #[test]
675    fn cut_colored_test() {}
676
677    #[test]
678    fn cut_no_colored_str() {
679        assert_eq!("something", cut("something", ..));
680        assert_eq!("som", cut("something", ..3));
681        assert_eq!("some", cut("something", ..=3));
682        assert_eq!("et", cut("something", 3..5));
683        assert_eq!("eth", cut("something", 3..=5));
684        assert_eq!("ething", cut("something", 3..));
685        assert_eq!("something", cut("something", ..));
686        assert_eq!("", cut("", ..));
687    }
688
689    #[test]
690    fn dont_panic_on_exceeding_upper_bound() {
691        assert_eq!("TEXT", cut("TEXT", ..50));
692        assert_eq!("EXT", cut("TEXT", 1..50));
693        assert_eq!(
694            "\u{1b}[31;40mTEXT\u{1b}[0m",
695            cut("\u{1b}[31;40mTEXT\u{1b}[0m", ..50)
696        );
697        assert_eq!(
698            "\u{1b}[31;40mEXT\u{1b}[0m",
699            cut("\u{1b}[31;40mTEXT\u{1b}[0m", 1..50)
700        );
701    }
702
703    #[test]
704    fn dont_panic_on_exceeding_lower_bound() {
705        assert_eq!("", cut("TEXT", 10..));
706        assert_eq!("", cut("TEXT", 10..50));
707    }
708
709    #[test]
710    #[should_panic = "One of indexes are not on a UTF-8 code point boundary"]
711    fn cut_a_mid_of_emojie_2_test() {
712        cut("😀", 1..2);
713    }
714
715    #[test]
716    #[should_panic = "One of indexes are not on a UTF-8 code point boundary"]
717    fn cut_a_mid_of_emojie_1_test() {
718        cut("😀", 1..);
719    }
720
721    #[test]
722    #[should_panic = "One of indexes are not on a UTF-8 code point boundary"]
723    fn cut_a_mid_of_emojie_0_test() {
724        cut("😀", ..1);
725    }
726
727    #[test]
728    fn cut_emojies_test() {
729        let emojes = "😀😃😄😁😆😅😂🤣🥲😊";
730        assert_eq!(emojes, emojes.cut(..));
731        assert_eq!("😀", emojes.cut(..4));
732        assert_eq!("😃😄", emojes.cut(4..12));
733        assert_eq!("🤣🥲😊", emojes.cut(emojes.find('🤣').unwrap()..));
734    }
735
736    #[test]
737    // todo: We probably need to fix it.
738    fn cut_colored_x_x_test() {
739        assert_ne!("", cut("\u{1b}[31;40mTEXT\u{1b}[0m", 3..3));
740        assert_ne!(
741            "",
742            cut(
743                "\u{1b}[31;40mTEXT\u{1b}[0m \u{1b}[34;42mTEXT\u{1b}[0m",
744                1..1
745            )
746        );
747        assert_ne!("", cut("\u{1b}[31;40mTEXT\u{1b}[0m", ..0));
748    }
749
750    #[test]
751    fn cut_partially_colored_str_test() {
752        let s = "zxc_\u{1b}[31;40mTEXT\u{1b}[0m_qwe";
753        assert_eq!("zxc", s.cut(..3));
754        assert_eq!("zxc_\u{1b}[31;40mT\u{1b}[39m\u{1b}[49m", s.cut(..5));
755        assert_eq!("\u{1b}[31;40mEXT\u{1b}[0m_q", s.cut(5..10));
756        assert_eq!("\u{1b}[31;40m\u{1b}[0m", s.cut(12..));
757    }
758
759    #[test]
760    fn chunks_not_colored_test() {
761        assert_eq!(
762            vec!["som".to_string(), "eth".to_string(), "ing".to_string()],
763            chunks("something", 3)
764        );
765        assert_eq!(
766            vec![
767                "so".to_string(),
768                "me".to_string(),
769                "th".to_string(),
770                "in".to_string(),
771                "g".to_string()
772            ],
773            chunks("something", 2)
774        );
775        assert_eq!(
776            vec!["a".to_string(), "b".to_string(), "c".to_string()],
777            chunks("abc", 1)
778        );
779        assert_eq!(vec!["something".to_string()], chunks("something", 99));
780    }
781
782    #[test]
783    #[should_panic]
784    fn chunks_panic_when_n_is_zero() {
785        chunks("something", 0);
786    }
787
788    #[test]
789    fn chunks_colored() {
790        let text = "\u{1b}[31;40mTEXT\u{1b}[0m";
791        assert_eq!(
792            vec![
793                "\u{1b}[31;40mT\u{1b}[39m\u{1b}[49m",
794                "\u{1b}[31;40mE\u{1b}[39m\u{1b}[49m",
795                "\u{1b}[31;40mX\u{1b}[39m\u{1b}[49m",
796                "\u{1b}[31;40mT\u{1b}[0m"
797            ],
798            chunks(text, 1)
799        );
800        assert_eq!(
801            vec![
802                "\u{1b}[31;40mTE\u{1b}[39m\u{1b}[49m",
803                "\u{1b}[31;40mXT\u{1b}[0m"
804            ],
805            chunks(text, 2)
806        );
807        assert_eq!(
808            vec![
809                "\u{1b}[31;40mTEX\u{1b}[39m\u{1b}[49m",
810                "\u{1b}[31;40mT\u{1b}[0m"
811            ],
812            chunks(text, 3)
813        );
814    }
815
816    #[test]
817    fn chunk_emojies_test() {
818        let emojes = "😀😃😄😁😆😅😂🤣🥲😊";
819        assert_eq!(
820            vec!["😀", "😃", "😄", "😁", "😆", "😅", "😂", "🤣", "🥲", "😊",],
821            chunks(emojes, 1)
822        );
823        assert_eq!(
824            vec!["😀😃", "😄😁", "😆😅", "😂🤣", "🥲😊",],
825            chunks(emojes, 2)
826        );
827        assert_eq!(vec!["😀😃😄", "😁😆😅", "😂🤣🥲", "😊",], chunks(emojes, 3));
828        assert_eq!(vec!["😀😃😄😁", "😆😅😂🤣", "🥲😊",], chunks(emojes, 4));
829        assert_eq!(vec!["😀😃😄😁😆", "😅😂🤣🥲😊",], chunks(emojes, 5));
830        assert_eq!(vec!["😀😃😄😁😆😅", "😂🤣🥲😊",], chunks(emojes, 6));
831        assert_eq!(vec!["😀😃😄😁😆😅😂", "🤣🥲😊",], chunks(emojes, 7));
832        assert_eq!(vec!["😀😃😄😁😆😅😂🤣", "🥲😊",], chunks(emojes, 8));
833        assert_eq!(vec!["😀😃😄😁😆😅😂🤣🥲", "😊",], chunks(emojes, 9));
834        assert_eq!(vec!["😀😃😄😁😆😅😂🤣🥲😊"], chunks(emojes, 10));
835        assert_eq!(vec!["😀😃😄😁😆😅😂🤣🥲😊"], chunks(emojes, 11));
836    }
837}