use clap::builder::StyledStr;
pub(crate) fn plain(styled: &StyledStr) -> String {
strip(&styled.to_string())
}
fn strip(text: &str) -> String {
let mut plain = String::with_capacity(text.len());
let mut sequence: Option<Sequence> = None;
for c in text.chars() {
let Some(mut current) = sequence else {
match c {
'\u{1b}' => sequence = Some(Sequence::Escape),
'\t' | '\n' | '\u{c}' | '\r' => plain.push(c),
c if c.is_ascii_control() => {}
c => plain.push(c),
}
continue;
};
let mut buffer = [0; 4];
for &byte in c.encode_utf8(&mut buffer).as_bytes() {
match current.after(byte) {
Next::Stay => {}
Next::Enter(next) => current = next,
Next::Print => {
plain.push(c);
sequence = None;
break;
}
Next::Leave => {
sequence = None;
break;
}
}
sequence = Some(current);
}
}
plain
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Sequence {
Escape,
EscapeIntermediate,
Csi,
DcsEntry,
DcsParam,
DcsIntermediate,
DcsData,
Data,
Osc,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Next {
Stay,
Enter(Sequence),
Leave,
Print,
}
impl Sequence {
fn after(self, byte: u8) -> Next {
match byte {
0x18 | 0x1a => return Next::Leave,
0x1b => return Next::Enter(Self::Escape),
_ => {}
}
match self {
Self::Escape => match byte {
0x00..=0x1f => execute(byte),
0x20..=0x2f => Next::Enter(Self::EscapeIntermediate),
b'[' => Next::Enter(Self::Csi),
b']' => Next::Enter(Self::Osc),
b'P' => Next::Enter(Self::DcsEntry),
b'X' | b'^' | b'_' => Next::Enter(Self::Data),
0x30..=0x7e => Next::Leave,
_ => Next::Stay,
},
Self::EscapeIntermediate => match byte {
0x00..=0x1f => execute(byte),
0x30..=0x7e => Next::Leave,
_ => Next::Stay,
},
Self::Csi => match byte {
0x00..=0x1f => execute(byte),
0x40..=0x7e => Next::Leave,
_ => Next::Stay,
},
Self::DcsEntry => match byte {
0x20..=0x2f => Next::Enter(Self::DcsIntermediate),
0x30..=0x3f => Next::Enter(Self::DcsParam),
0x40..=0x7e => Next::Enter(Self::DcsData),
_ => Next::Stay,
},
Self::DcsParam => match byte {
0x20..=0x2f => Next::Enter(Self::DcsIntermediate),
0x3c..=0x7e => Next::Enter(Self::DcsData),
_ => Next::Stay,
},
Self::DcsIntermediate => match byte {
0x30..=0x7e => Next::Enter(Self::DcsData),
_ => Next::Stay,
},
Self::DcsData | Self::Data => match byte {
0x9c => Next::Leave,
_ => Next::Stay,
},
Self::Osc => match byte {
0x07 => Next::Leave,
_ => Next::Stay,
},
}
}
}
fn execute(byte: u8) -> Next {
match byte {
b'\t' | b'\n' | 0x0c | b'\r' => Next::Print,
_ => Next::Stay,
}
}
#[cfg(test)]
mod tests {
use super::strip;
const CASES: &[(&str, &str)] = &[
("a\u{1b}[1mb", "ab"),
("a\u{7f}b", "ab"),
("a\u{7}\u{b}\tb\r\n\u{c}", "a\tb\r\n\u{c}"),
("café \u{85}\u{2028}", "café \u{85}\u{2028}"),
("\u{1b}]8;;id\u{7}link\u{1b}]8;;\u{1b}\\", "link"),
("\u{1b}]é\u{7}x", "x"),
("\u{1b}[1\nm", "\nm"),
("\u{1b}[1\u{18}m", "m"),
("\u{1b}(Bx", "x"),
("\u{1b}Pq#0\u{1b}\\x", "x"),
("\u{1b}P\u{9c}x", ""),
("\u{1b}P1;2|\u{201c}x", "x"),
("\u{1b}_a\u{201c}b", "b"),
("\u{1b}_\u{2713}b", "b"),
("\u{1b}^\nb\u{1b}\\c", "c"),
("a\u{1b}", "a"),
];
#[test]
fn strips_what_anstream_strips() {
for (input, expected) in CASES {
assert_eq!(strip(input), *expected, "{input:?}");
}
}
#[test]
fn stripped_text_strips_to_itself() {
for (input, _) in CASES {
let once = strip(input);
assert_eq!(strip(&once), once, "{input:?}");
}
}
}