#[cfg(test)]
#[path = "terminal_text/tests.rs"]
mod tests;
pub(crate) fn sanitize_terminal_body(input: &str) -> String {
let characters = input.chars().collect::<Vec<_>>();
let mut output = String::with_capacity(input.len());
let mut previous_is_content = false;
let mut omitted_after_content = false;
let mut index = 0;
while index < characters.len() {
let character = characters[index];
let control = match character {
'\u{001B}' => Some(scan_escape_sequence(&characters, index)),
'\u{009B}' => Some(scan_csi(&characters, index + 1)),
'\u{009D}' => Some(scan_string_control(&characters, index + 1, true)),
'\u{0090}' | '\u{0098}' | '\u{009E}' | '\u{009F}' => {
Some(scan_string_control(&characters, index + 1, false))
}
_ => None,
};
if let Some((next, complete)) = control {
if complete {
index = next;
continue;
}
push_replacement(
&mut output,
&mut previous_is_content,
&mut omitted_after_content,
);
index = next;
continue;
}
if body_character_is_preserved(character) {
push_preserved(
&mut output,
character,
&mut previous_is_content,
&mut omitted_after_content,
);
} else if previous_is_content {
omitted_after_content = true;
}
index += 1;
}
output
}
fn body_character_is_preserved(character: char) -> bool {
matches!(character, '\n' | '\t' | ' ')
|| (!character.is_control()
&& !tau_proto::requires_visible_escape(character)
&& !is_unicode_format_character(character))
}
fn is_unicode_format_character(character: char) -> bool {
matches!(
character as u32,
0x00AD
| 0x0600..=0x0605
| 0x061C
| 0x06DD
| 0x070F
| 0x0890..=0x0891
| 0x08E2
| 0x180E
| 0x200B..=0x200F
| 0x202A..=0x202E
| 0x2060..=0x2064
| 0x2066..=0x206F
| 0xFEFF
| 0xFFF9..=0xFFFB
| 0x110BD
| 0x110CD
| 0x13430..=0x1343F
| 0x1BCA0..=0x1BCA3
| 0x1D173..=0x1D17A
| 0xE0001
| 0xE0020..=0xE007F
)
}
fn push_preserved(
output: &mut String,
character: char,
previous_is_content: &mut bool,
omitted_after_content: &mut bool,
) {
let current_is_content = !character.is_whitespace();
if *omitted_after_content && current_is_content {
output.push('\u{FFFD}');
}
output.push(character);
*previous_is_content = current_is_content;
*omitted_after_content = false;
}
fn push_replacement(
output: &mut String,
previous_is_content: &mut bool,
omitted_after_content: &mut bool,
) {
output.push('\u{FFFD}');
*previous_is_content = false;
*omitted_after_content = false;
}
fn scan_escape_sequence(characters: &[char], start: usize) -> (usize, bool) {
let Some(&introducer) = characters.get(start + 1) else {
return (start + 1, false);
};
match introducer {
'[' => scan_csi(characters, start + 2),
']' => scan_string_control(characters, start + 2, true),
'P' | 'X' | '^' | '_' => scan_string_control(characters, start + 2, false),
character if is_escape_intermediate(character) => scan_escape_final(characters, start + 2),
character if is_escape_final(character) => (start + 2, true),
_ => (start + 1, false),
}
}
fn scan_csi(characters: &[char], mut index: usize) -> (usize, bool) {
while let Some(&character) = characters.get(index) {
if is_csi_parameter(character) || is_escape_intermediate(character) {
index += 1;
continue;
}
return if is_csi_final(character) {
(index + 1, true)
} else {
(index, false)
};
}
(index, false)
}
fn scan_string_control(
characters: &[char],
mut index: usize,
bell_terminates: bool,
) -> (usize, bool) {
let payload_start = index;
while let Some(&character) = characters.get(index) {
if character == '\u{009C}' || (bell_terminates && character == '\u{0007}') {
return (index + 1, true);
}
if character == '\u{001B}' {
if characters.get(index + 1) == Some(&'\\') {
return (index + 2, true);
}
return (index, false);
}
if is_string_control_introducer(character) {
return (index, false);
}
index += 1;
}
(payload_start, false)
}
fn is_string_control_introducer(character: char) -> bool {
matches!(
character,
'\u{009D}' | '\u{0090}' | '\u{0098}' | '\u{009E}' | '\u{009F}'
)
}
fn scan_escape_final(characters: &[char], mut index: usize) -> (usize, bool) {
while characters
.get(index)
.is_some_and(|character| is_escape_intermediate(*character))
{
index += 1;
}
if characters
.get(index)
.is_some_and(|character| is_escape_final(*character))
{
(index + 1, true)
} else {
(index, false)
}
}
fn is_csi_parameter(character: char) -> bool {
matches!(character, '\u{0030}'..='\u{003F}')
}
fn is_escape_intermediate(character: char) -> bool {
matches!(character, '\u{0020}'..='\u{002F}')
}
fn is_escape_final(character: char) -> bool {
matches!(character, '\u{0030}'..='\u{007E}')
}
fn is_csi_final(character: char) -> bool {
matches!(character, '\u{0040}'..='\u{007E}')
}