fn is_control(c: char) -> bool {
c < '\x20' || ('\x7f'..='\u{009f}').contains(&c)
}
fn is_blocked_unicode(c: char) -> bool {
matches!(
c,
'\u{202A}'..='\u{202E}' | '\u{2066}'..='\u{2069}'
| '\u{2028}' | '\u{2029}'
| '\u{200E}' | '\u{200F}' | '\u{061C}'
| '\u{200B}' | '\u{2060}' | '\u{FEFF}'
)
}
pub(crate) fn strip_ansi(s: &str) -> String {
let bytes = strip_ansi_escapes::strip(s);
let text = String::from_utf8_lossy(&bytes);
let mut out = String::with_capacity(text.len());
let mut in_control_run = false;
for c in text.chars() {
if is_blocked_unicode(c) {
continue;
}
if is_control(c) {
if !in_control_run {
out.push(' ');
in_control_run = true;
}
continue;
}
out.push(c);
in_control_run = false;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strip_ansi_removes_escape_sequences() {
assert_eq!(strip_ansi("\x1b[31mhello\x1b[0m"), "hello");
assert_eq!(strip_ansi("\x1b[1;32mgreen bold\x1b[0m"), "green bold");
}
#[test]
fn strip_ansi_removes_bidi_overrides() {
assert_eq!(strip_ansi("pay \u{202E}oot"), "pay oot");
assert_eq!(strip_ansi("\u{202A}\u{202B}\u{202C}\u{202D}\u{202E}"), "");
assert_eq!(strip_ansi("\u{2066}\u{2067}\u{2068}\u{2069}"), "");
}
#[test]
fn strip_ansi_removes_line_and_para_separators() {
assert_eq!(strip_ansi("line\u{2028}break"), "linebreak");
assert_eq!(strip_ansi("para\u{2029}sep"), "parasep");
}
#[test]
fn strip_ansi_preserves_printable_ascii() {
let s = "hello world 123!";
assert_eq!(strip_ansi(s), s);
}
#[test]
fn strip_ansi_preserves_printable_unicode() {
assert_eq!(strip_ansi("hello\u{00e9}"), "hello\u{00e9}");
assert_eq!(strip_ansi("caf\u{00e9}"), "caf\u{00e9}");
}
#[test]
fn strip_ansi_removes_c0_controls() {
assert_eq!(strip_ansi("a\x00b"), "ab");
assert_eq!(strip_ansi("a\x01b"), "ab");
assert_eq!(strip_ansi("a\x1fb"), "ab");
assert_eq!(strip_ansi("a b"), "a b");
}
#[test]
fn strip_ansi_collapses_newline_runs_to_one_space_not_joining_words() {
assert_eq!(
strip_ansi("make build\nmake install"),
"make build make install"
);
assert_eq!(strip_ansi("line one\r\nline two"), "line one line two");
assert_eq!(strip_ansi("a\n\n\nb"), "a b");
assert_eq!(strip_ansi("\ntail"), " tail");
assert_eq!(strip_ansi("head\n"), "head ");
}
#[test]
fn strip_ansi_blocked_unicode_still_vanishes_without_a_space() {
assert_eq!(strip_ansi("pay\u{202E}oot"), "payoot");
assert_eq!(strip_ansi("wo\u{200B}rd"), "word");
assert_eq!(strip_ansi("line\u{2028}break"), "linebreak");
}
#[test]
fn strip_ansi_removes_directional_marks_and_zero_width() {
assert_eq!(strip_ansi("a\u{200E}b"), "ab", "LRM");
assert_eq!(strip_ansi("a\u{200F}b"), "ab", "RLM");
assert_eq!(strip_ansi("a\u{061C}b"), "ab", "ALM");
assert_eq!(strip_ansi("a\u{200B}b"), "ab", "zero-width space");
assert_eq!(strip_ansi("a\u{2060}b"), "ab", "word joiner");
assert_eq!(
strip_ansi("a\u{FEFF}b"),
"ab",
"BOM / zero-width no-break space"
);
}
#[test]
fn strip_ansi_empty_string() {
assert_eq!(strip_ansi(""), "");
}
#[test]
fn strip_ansi_tab_and_cr_are_consumed_by_first_pass_leaving_no_space() {
assert_eq!(
strip_ansi("a\tb"),
"ab",
"TAB must not survive to become a space"
);
assert_eq!(
strip_ansi("a\rb"),
"ab",
"lone CR must not survive to become a space"
);
assert_eq!(strip_ansi("a\x0bb"), "ab", "VT must not survive");
assert_eq!(strip_ansi("a\x0cb"), "ab", "FF must not survive");
assert_eq!(
strip_ansi("a\x1b[0mb"),
"ab",
"an ANSI reset leaves no space"
);
assert_eq!(
strip_ansi("ab\x1b"),
"ab",
"a trailing bare ESC leaves no space"
);
assert_eq!(
strip_ansi("a\nb"),
"a b",
"newline is the sole survivor and becomes a space"
);
}
}