Skip to main content

uncurses/ansi/
strip.rs

1//! ANSI escape stripping built on the byte tokenizer.
2//!
3//! ## Category
4//!
5//! [`strip`] removes ANSI escape/string/control sequences while preserving
6//! printable text and non-ESC control bytes such as newlines and tabs.
7//!
8//! ## Parser conventions
9//!
10//! Tokenization recognizes CSI, OSC, DCS, SOS, PM, APC, and two-byte ESC
11//! sequences in both 7-bit and 8-bit forms. Escape tokens contribute no output.
12//!
13//! ## Mode interaction
14//!
15//! Stripping does not emulate terminal modes. It is a byte-stream transformation
16//! suitable for display-width and plain-text extraction paths.
17
18use super::text::{Token, WidthMode, tokenize};
19
20/// Return `s` with ANSI escape sequences removed.
21///
22/// CSI, OSC, DCS, SOS, PM, APC, and short ESC sequences are dropped. Printable
23/// UTF-8 text and non-ESC control bytes such as `\n`, `\r`, and `\t` are
24/// preserved.
25pub fn strip(s: &str) -> String {
26    let mut out = String::with_capacity(s.len());
27    for tok in tokenize(s.as_bytes(), WidthMode::default(), false) {
28        match tok {
29            Token::Text { text, .. } => out.push_str(bs(text)),
30            Token::Control(b) => out.push(b as char),
31            Token::Escape(_) => {}
32        }
33    }
34    out
35}
36
37#[inline]
38fn bs(b: &[u8]) -> &str {
39    // SAFETY: Tokens emitted for `&str` input are always whole grapheme
40    // clusters or escape sequences that fall on valid UTF-8 boundaries.
41    unsafe { std::str::from_utf8_unchecked(b) }
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47
48    #[test]
49    fn strip_sgr() {
50        assert_eq!(strip("\x1b[31mhello\x1b[0m"), "hello");
51    }
52
53    #[test]
54    fn strip_osc() {
55        assert_eq!(strip("\x1b]0;title\x07hello"), "hello");
56        assert_eq!(strip("\x1b]0;title\x1b\\hello"), "hello");
57    }
58
59    #[test]
60    fn strip_preserves_newlines() {
61        assert_eq!(strip("a\nb\tc"), "a\nb\tc");
62    }
63
64    #[test]
65    fn strip_unicode() {
66        assert_eq!(strip("\x1b[1m中文\x1b[m"), "中文");
67    }
68
69    #[test]
70    fn strip_empty() {
71        assert_eq!(strip(""), "");
72    }
73
74    #[test]
75    fn strip_only_escapes() {
76        assert_eq!(strip("\x1b[31m\x1b[m"), "");
77    }
78
79    #[test]
80    fn strip_nested_csi() {
81        assert_eq!(strip("a\x1b[1;2;3;4mb\x1b[0;1mc"), "abc");
82    }
83
84    #[test]
85    fn strip_two_byte_esc() {
86        // ESC = (DECKPAM)
87        assert_eq!(strip("a\x1b=b"), "ab");
88    }
89}