use compact_str::{CompactString, format_compact};
use crate::text::ansi_tokenize::consts::{
C1_ST, LINK_CODE_PREFIX, LINK_END_CODE, LINK_END_CODE_C1ST, LINK_END_CODE_ST,
};
use super::types::AnsiToken;
pub(crate) const RESET_CODE: &str = "\x1B[0m";
fn end_code_for_num(n: u32) -> Option<u32> {
Some(match n {
0 => 0,
1 | 2 => 22,
3 => 23,
4 => 24,
53 => 55,
7 => 27,
8 => 28,
9 => 29,
30..=37 | 90..=97 => 39,
40..=47 | 100..=107 => 49,
_ => return None,
})
}
fn is_end_code_num(n: u32) -> bool {
matches!(n, 0 | 22 | 23 | 24 | 27 | 28 | 29 | 39 | 49 | 55)
}
fn parse_sgr_first_param(code: &str) -> Option<u32> {
let inner = code.strip_prefix("\x1B[")?;
let inner = inner.strip_suffix('m')?;
let first = inner.split(';').next()?;
first.parse().ok()
}
pub(crate) fn get_end_code(code: &str) -> CompactString {
if let Some(n) = parse_sgr_first_param(code) {
if is_canonical_sgr(code, n) {
if is_end_code_num(n) {
return code.into();
}
if let Some(end) = end_code_for_num(n) {
return format_compact!("\x1B[{end}m");
}
}
}
if code.starts_with(LINK_CODE_PREFIX) {
if code.ends_with("\x1B\\") {
return LINK_END_CODE_ST.into();
}
if code.ends_with(C1_ST) {
return LINK_END_CODE_C1ST.into();
}
return LINK_END_CODE.into();
}
let rest: &str = code
.char_indices()
.nth(2)
.map(|(off, _)| &code[off..])
.unwrap_or("");
if rest.starts_with("38") {
return "\x1B[39m".into();
}
if rest.starts_with("48") {
return "\x1B[49m".into();
}
if let Some(end) = leading_int(rest).and_then(end_code_for_num) {
return format_compact!("\x1B[{end}m");
}
RESET_CODE.into()
}
fn is_canonical_sgr(code: &str, n: u32) -> bool {
format_compact!("\x1B[{n}m") == code
}
fn leading_int(s: &str) -> Option<u32> {
let end = s
.as_bytes()
.iter()
.position(|b| !b.is_ascii_digit())
.unwrap_or(s.len());
s[..end].parse().ok()
}
pub(crate) fn is_end_code(code: &str) -> bool {
if let Some(n) = parse_sgr_first_param(code) {
return is_canonical_sgr(code, n) && is_end_code_num(n);
}
false
}
pub(crate) fn is_intensity_code(token: &AnsiToken) -> bool {
token.code == "\x1B[1m" || token.code == "\x1B[2m"
}
pub(crate) fn ansi_codes_to_string(codes: &[AnsiToken]) -> String {
let mut seen: Vec<&str> = Vec::with_capacity(codes.len());
let mut result = String::new();
for code in codes {
let s = code.code.as_str();
if !seen.contains(&s) {
seen.push(s);
result.push_str(s);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn get_end_code_parity_table() {
let cases = [
("\x1B[0m", "\x1B[0m"),
("\x1B[39m", "\x1B[39m"),
("\x1B[49m", "\x1B[49m"),
("\x1B[55m", "\x1B[55m"),
("\x1B[25m", "\x1B[0m"),
("\x1B[5m", "\x1B[0m"),
("\x1B[6m", "\x1B[0m"),
("\x1B[21m", "\x1B[0m"),
("\x1B[99m", "\x1B[0m"),
("\x1B[53m", "\x1B[55m"),
("\x1B[1m", "\x1B[22m"),
("\x1B[2m", "\x1B[22m"),
("\x1B[31m", "\x1B[39m"),
("\x1B[38;5;200m", "\x1B[39m"),
("\x1B[48;2;1;2;3m", "\x1B[49m"),
("[31m", "\x1B[22m"),
("[38;5;1m", "\x1B[28m"),
("[1m", "\x1B[0m"),
];
for (input, expected) in cases {
assert_eq!(
get_end_code(input),
expected,
"get_end_code({input:?}) mismatch"
);
}
}
#[test]
fn get_end_code_link_suffix_variants() {
assert_eq!(get_end_code("\x1B]8;;http://x\x07"), "\x1B]8;;\x07");
assert_eq!(get_end_code("\x1B]8;;http://x\x1B\\"), "\x1B]8;;\x1B\\");
assert_eq!(get_end_code("\x1B]8;;http://x\u{9C}"), "\x1B]8;;\u{9C}");
}
#[test]
fn is_end_code_matches_set() {
for c in [
"\x1B[0m", "\x1B[22m", "\x1B[23m", "\x1B[24m", "\x1B[27m", "\x1B[28m", "\x1B[29m",
"\x1B[39m", "\x1B[49m", "\x1B[55m",
] {
assert!(is_end_code(c), "{c:?} should be an end code");
}
for c in [
"\x1B[25m", "\x1B[54m", "\x1B[59m", "\x1B[1m", "\x1B[31m", "[39m",
] {
assert!(!is_end_code(c), "{c:?} should NOT be an end code");
}
}
#[test]
fn ansi_codes_to_string_dedup() {
let red = AnsiToken {
code: "\x1B[31m".into(),
end_code: "\x1B[39m".into(),
};
let out = ansi_codes_to_string(&[red.clone(), red]);
assert_eq!(out, "\x1B[31m");
}
}