use std::borrow::Cow;
const ESC: char = '\u{1b}';
const BEL: char = '\u{7}';
pub fn printable(text: &str) -> Cow<'_, str> {
if !text.chars().any(unwanted) {
return Cow::Borrowed(text);
}
let mut out = String::with_capacity(text.len());
let mut chars = text.chars().peekable();
while let Some(c) = chars.next() {
match c {
ESC => match chars.next() {
Some('[') => skip_csi(&mut chars),
Some(']' | 'P' | 'X' | '^' | '_') => skip_string(&mut chars),
_ => {}
},
'\u{9b}' => skip_csi(&mut chars),
'\u{9d}' | '\u{90}' | '\u{98}' | '\u{9e}' | '\u{9f}' => skip_string(&mut chars),
c if unwanted(c) => {}
c => out.push(c),
}
}
Cow::Owned(out)
}
fn unwanted(c: char) -> bool {
(c.is_control() && c != '\n' && c != '\t')
|| ('\u{202a}'..='\u{202e}').contains(&c)
|| ('\u{2066}'..='\u{2069}').contains(&c)
}
fn skip_csi(chars: &mut std::iter::Peekable<std::str::Chars>) {
while let Some(&c) = chars.peek() {
match c {
'\u{20}'..='\u{3f}' => {
chars.next();
}
'\u{40}'..='\u{7e}' => {
chars.next();
return;
}
_ => return,
}
}
}
fn skip_string(chars: &mut std::iter::Peekable<std::str::Chars>) {
while let Some(c) = chars.next() {
match c {
BEL | '\u{9c}' => return,
ESC => {
if chars.peek() == Some(&'\\') {
chars.next();
}
return;
}
_ => {}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plain_text_is_kept() {
for text in ["King James Version", "Ἰησοῦς\tבְּרֵאשִׁית\n", ""] {
assert!(matches!(printable(text), Cow::Borrowed(t) if t == text));
}
}
#[test]
fn controls_and_sequences_are_dropped() {
let cases = [
("\u{1b}[31mred\u{1b}[0m", "red"),
("\u{1b}[2J\u{1b}[1;1Hcleared", "cleared"),
("\u{1b}]0;title\u{7}name", "name"),
(
"\u{1b}]8;;https://x.example\u{1b}\\link\u{1b}]8;;\u{1b}\\",
"link",
),
("\u{9b}31mC1\u{9d}0;t\u{9c}", "C1"),
("a\rb\u{8}c\u{0}d\u{7f}e\u{85}f", "abcdef"),
("abc\u{202e}fed\u{202c} \u{2066}x\u{2069}", "abcfed x"),
("\u{1b}Pdcs\u{1b}\\ok \u{1b}", "ok "),
("line\n\tnext", "line\n\tnext"),
];
for (text, shown) in cases {
assert_eq!(printable(text), shown, "{text:?}");
}
}
}