fast_strip_ansi/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::borrow::Cow;
4
5use vt_push_parser::event::VTEvent;
6use vt_push_parser::{VT_PARSER_INTEREST_NONE, VTPushParser};
7
8/// Strip ANSI escape sequences from a string. If the input contains no ANSI
9/// escape sequences, the input is returned as-is.
10pub fn strip_ansi_string(s: &str) -> Cow<'_, str> {
11    let mut output = Cow::Borrowed(s);
12    let mut parser = VTPushParser::new_with_interest::<VT_PARSER_INTEREST_NONE>();
13    let mut has_text = false;
14    parser.feed_with(s.as_bytes(), |event: VTEvent| {
15        if let VTEvent::Raw(text) = event {
16            has_text = true;
17            if text.len() == s.len() {
18                return;
19            }
20            let output = match &mut output {
21                Cow::Borrowed(_) => {
22                    output = Cow::Owned(String::with_capacity(s.len()));
23                    let Cow::Owned(s) = &mut output else {
24                        unreachable!()
25                    };
26                    s
27                }
28                Cow::Owned(s) => s,
29            };
30            output.push_str(String::from_utf8_lossy(text).as_ref());
31        }
32    });
33    if has_text { output } else { Cow::Borrowed("") }
34}
35
36/// Strip ANSI escape sequences from a byte slice. If the input contains no
37/// ANSI escape sequences, the input is returned as-is.
38pub fn strip_ansi_bytes(s: &[u8]) -> Cow<'_, [u8]> {
39    let mut output = Cow::Borrowed(s);
40    let mut parser = VTPushParser::new_with_interest::<VT_PARSER_INTEREST_NONE>();
41    let mut has_text = false;
42    parser.feed_with(s, |event: VTEvent| {
43        if let VTEvent::Raw(text) = event {
44            has_text = true;
45            if text.len() == s.len() {
46                return;
47            }
48            let output = match &mut output {
49                Cow::Borrowed(_) => {
50                    output = Cow::Owned(Vec::with_capacity(s.len()));
51                    let Cow::Owned(s) = &mut output else {
52                        unreachable!()
53                    };
54                    s
55                }
56                Cow::Owned(s) => s,
57            };
58            output.extend_from_slice(text);
59        }
60    });
61    if has_text { output } else { Cow::Borrowed(b"") }
62}
63
64/// Strip ANSI escape sequences from a byte slice, calling a callback for each
65/// raw text chunk.
66pub fn strip_ansi_bytes_callback(s: &[u8], mut cb: impl FnMut(&[u8])) {
67    let mut parser = VTPushParser::new_with_interest::<VT_PARSER_INTEREST_NONE>();
68    parser.feed_with(s, |event: VTEvent| {
69        if let VTEvent::Raw(text) = event {
70            cb(text)
71        }
72    });
73}
74
75/// A streaming ANSI escape sequence stripper that can be fed chunks of data
76/// and yields text chunks to a callback.
77pub struct StreamingStripper {
78    parser: VTPushParser<VT_PARSER_INTEREST_NONE>,
79}
80
81impl Default for StreamingStripper {
82    fn default() -> Self {
83        Self::new()
84    }
85}
86
87impl StreamingStripper {
88    pub const fn new() -> Self {
89        Self {
90            parser: VTPushParser::new_with_interest::<VT_PARSER_INTEREST_NONE>(),
91        }
92    }
93
94    /// Feed a chunk of data to the stripper. The callback will be called for
95    /// each raw text chunk.
96    pub fn feed(&mut self, s: &[u8], cb: &mut impl FnMut(&[u8])) {
97        self.parser.feed_with(s, &mut |event: VTEvent| {
98            if let VTEvent::Raw(text) = event {
99                cb(text)
100            }
101        });
102    }
103}
104
105/// A writer that strips ANSI escape sequences from the data written to it and
106/// feeds the underlying writer with the raw text chunks.
107///
108/// Due to limitations of the [`std::io::Write`] interface, if the underlying
109/// writer returns an error, it may be uncertain how many bytes were written.
110pub struct Writer<W: std::io::Write> {
111    writer: W,
112    ansi_strip: StreamingStripper,
113}
114
115impl<W: std::io::Write> Writer<W> {
116    pub fn new(writer: W) -> Self {
117        Self {
118            writer,
119            ansi_strip: StreamingStripper::new(),
120        }
121    }
122
123    pub fn into_inner(self) -> W {
124        self.writer
125    }
126}
127
128impl<W: std::io::Write> std::io::Write for Writer<W> {
129    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
130        self.write_all(buf)?;
131        Ok(buf.len())
132    }
133
134    fn flush(&mut self) -> std::io::Result<()> {
135        self.writer.flush()
136    }
137
138    fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> {
139        let mut error = None;
140        self.ansi_strip.feed(buf, &mut |text| {
141            if error.is_none() {
142                _ = self.writer.write_all(text).map_err(|e| {
143                    error = Some(e);
144                });
145            }
146        });
147        if let Some(error) = error.take() {
148            Err(error)
149        } else {
150            Ok(())
151        }
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use std::io::Write;
158
159    use super::*;
160
161    #[test]
162    fn test_strip_ansi_string() {
163        let input = "Hello, world!\x1b[31mHello, world!\x1b[0m";
164        let output = strip_ansi_string(input);
165        assert_eq!(output, "Hello, world!Hello, world!");
166    }
167
168    #[test]
169    fn test_strip_ansi_as_is() {
170        let input = b"Hello, world!";
171        let output = strip_ansi_bytes(input);
172        assert_eq!(output, b"Hello, world!".as_slice());
173        assert!(matches!(output, Cow::Borrowed(_)));
174    }
175
176    #[test]
177    fn test_writer() {
178        let mut writer = Writer::new(Vec::new());
179        writer
180            .write_all(b"Hello, world!\x1b[31mHello, world!\x1b[0m")
181            .unwrap();
182        assert_eq!(writer.into_inner(), b"Hello, world!Hello, world!");
183    }
184
185    #[test]
186    fn test_only_ansi() {
187        let mut writer = Writer::new(Vec::new());
188        writer
189            .write_all("\x1b[31m\x1b[1m\x1b[0m".as_bytes())
190            .unwrap();
191        assert_eq!(writer.into_inner(), b"");
192
193        assert_eq!(strip_ansi_string("\x1b[31m\x1b[1m\x1b[0m"), "");
194        assert_eq!(
195            strip_ansi_bytes(b"\x1b[31m\x1b[1m\x1b[0m"),
196            Cow::Borrowed(b"")
197        );
198    }
199}