use regex::Regex;
pub const ANSI_REGEX: &'static str = r"[\x1b\x9b][\[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PRZcf-nqry=><]";
pub const ANSI_PAIR: [[&'static str; 2]; 24] = [
["\x1B[0m", "\x1B[0m"],
["\x1B[1m", "\x1B[22m"],
["\x1B[2m", "\x1B[22m"],
["\x1B[3m", "\x1B[23m"],
["\x1B[4m", "\x1B[24m"],
["\x1B[5m", "\x1B[25m"],
["\x1B[7m", "\x1B[27m"],
["\x1B[8m", "\x1B[28m"],
["\x1B[30m", "\x1B[39m"],
["\x1B[31m", "\x1B[39m"],
["\x1B[32m", "\x1B[39m"],
["\x1B[33m", "\x1B[39m"],
["\x1B[34m", "\x1B[39m"],
["\x1B[35m", "\x1B[39m"],
["\x1B[36m", "\x1B[39m"],
["\x1B[37m", "\x1B[39m"],
["\x1B[40m", "\x1B[49m"],
["\x1B[41m", "\x1B[49m"],
["\x1B[42m", "\x1B[49m"],
["\x1B[43m", "\x1B[49m"],
["\x1B[44m", "\x1B[49m"],
["\x1B[45m", "\x1B[49m"],
["\x1B[46m", "\x1B[49m"],
["\x1B[47m", "\x1B[49m"],
];
#[derive(Debug, Clone, PartialEq)]
pub enum Alignment {
Left,
Center,
Right,
}
pub fn ansi_pair<'a>(code: &'a str) -> Option<&[&str; 2]> {
ANSI_PAIR.iter().find(|&pair| pair.iter().any(|&v| v == code))
}
pub fn strip_codes(txt: &str) -> String {
let regex = Regex::new(ANSI_REGEX).unwrap();
let clean = String::from_utf8(regex.replace_all(txt, "").as_bytes().to_vec());
if clean.is_ok() {
clean.unwrap()
} else {
txt.to_string()
}
}
pub fn match_indices(txt: &str) -> Vec<String> {
let regex = Regex::new(ANSI_REGEX).unwrap();
let mut result = Vec::new();
let mut data: String = txt.to_string();
loop {
let mat = regex.find(data.as_str());
if mat.is_some() {
let mat = mat.unwrap();
let start = mat.start();
let end = mat.end();
result.push(data[0..start].to_string());
result.push(data[start..end].to_string());
let size = data.chars().count();
if size == 0 {
break;
} else {
data = data[end..].to_string();
}
} else {
result.push(data);
break;
}
}
result
}
pub fn slice_text(txt: &str, start: usize, end: usize) -> String {
let mut u_start = None;
let mut u_end = None;
let mut offset = 0;
let mut u_offset = 0;
for chunk in match_indices(txt).iter() {
let size = strip_codes(chunk).len();
if u_start.is_none() && offset + size >= start {
u_start = Some(u_offset + start - offset);
}
if u_end.is_none() && offset + size >= end {
u_end = Some(u_offset + end - offset);
break;
}
offset += size;
u_offset += chunk.len();
}
let u_start = match u_start {
Some(v) => v,
None => 0,
};
let u_end = match u_end {
Some(v) => v,
None => txt.len(),
};
txt[u_start..u_end].to_string()
}
pub fn size_text(txt: &str) -> usize {
unicode_width::UnicodeWidthStr::width(strip_codes(txt).as_str())
}
pub fn pad_text(
txt: &str,
width: usize,
align: &Alignment,
chr: char,
) -> String {
let size = size_text(txt);
if size >= width {
return txt.to_string();
}
let diff = width - size;
let (left_pad, right_pad) = match align {
Alignment::Left => (0, diff),
Alignment::Right => (diff, 0),
Alignment::Center => (diff / 2, diff - diff / 2),
};
let mut result = String::new();
for _ in 0..left_pad {
result.push(chr);
}
result.push_str(txt);
for _ in 0..right_pad {
result.push(chr);
}
result
}
pub fn trucate_text(
txt: &str,
width: usize,
align: &Alignment,
tail: &str
) -> String {
let size = size_text(txt);
if width >= size {
return txt.to_string();
}
let t_size = size_text(tail);
match align {
Alignment::Left => {
let text = slice_text(txt, 0, width - t_size).trim().to_string();
format!("{}{}", text, tail)
},
Alignment::Right => {
let text = slice_text(txt, size - width + t_size, size).trim().to_string();
format!("{}{}", tail, text)
},
Alignment::Center => {
let dim = (width - t_size) / 2;
let left = slice_text(txt, 0, dim).trim().to_string();
let right = slice_text(txt, size - width + t_size + dim, size).trim().to_string();
format!("{}{}{}", left, tail, right)
},
}
}
pub fn wrap_text(txt: &str, width: usize) -> Vec<String> {
let mut result: Vec<String> = Vec::new();
for line in txt.lines() {
let mut words: Vec<String> = Vec::new();
let mut length = 0;
for (wcount, word) in line.split(" ").enumerate() {
let word_size = size_text(word);
if length + word_size >= width && words.len() > 0 {
result.push(words.join(" "));
words = Vec::new();
length = 0;
}
length += word_size + if wcount > 0 { 1 } else { 0 };
words.push(word.to_string());
}
if words.len() > 0 {
result.push(words.join(" "));
}
}
result
}
pub fn repaire_text(lines: Vec<String>) -> Vec<String> {
let mut ansis: Vec<Vec<String>> = Vec::new();
lines.iter().map(|line| {
let parts = match_indices(line.as_str());
let mut result: Vec<String> = Vec::new();
let ansiiter = &ansis;
for ansi in ansiiter.into_iter() {
result.push(ansi[0].to_string());
}
for part in parts.into_iter() {
let pair = ansi_pair(part.as_str());
if pair.is_some() {
let pair = pair.unwrap();
let opentag = pair[0].to_string();
let closetag = pair[1].to_string();
if part == opentag {
ansis.push(vec![opentag, closetag]);
} else if part == closetag {
let index = ansis.iter().position(|a| a[1].to_string() == closetag);
if index.is_some() {
ansis.remove(index.unwrap());
}
}
}
result.push(part.to_string());
}
let ansiiter = &ansis;
for ansi in ansiiter.into_iter() {
result.push(ansi[1].to_string());
}
result.join("")
}).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn finds_ansi_pair() {
assert_eq!(ansi_pair(&ANSI_PAIR[0][1]), Some(&ANSI_PAIR[0]));
assert_eq!(ansi_pair("foo"), None);
}
#[test]
fn strips_ansi_codes() {
assert_eq!(strip_codes("aaa\x1B[0mbbb\x1B[0mccc"), "aaabbbccc");
}
#[test]
fn matches_ansi_indices() {
assert_eq!(match_indices("This is\x1B[39m long"), vec!["This is", "\x1B[39m", " long"]);
assert_eq!(match_indices("This is\x1B[39m long \x1B[46mtext for test"), vec!["This is", "\x1B[39m", " long ", "\x1B[46m", "text for test"]);
}
#[test]
fn slices_ansi_text() {
assert_eq!(slice_text("a\x1B[32maa\x1B[32mb\x1B[32mbb\x1B[32mcccdddeeefff", 5, 10), "b\x1B[32mcccd");
}
#[test]
fn sizes_ansi_text() {
assert_eq!(size_text("aaa\x1B[0mbbb\x1B[0mccc"), 9);
}
#[test]
fn pads_ansi_text() {
assert_eq!(pad_text("fo\x1B[39mobar", 10, &Alignment::Left, '+'), "fo\x1B[39mobar++++");
assert_eq!(pad_text("fo\x1B[39mobar", 10, &Alignment::Right, '+'), "++++fo\x1B[39mobar");
assert_eq!(pad_text("fo\x1B[39mobar", 10, &Alignment::Center, '+'), "++fo\x1B[39mobar++");
}
#[test]
fn truncates_ansi_text() {
assert_eq!(trucate_text("fo\x1B[39mobarbaz", 5, &Alignment::Left, "+"), "fo\x1B[39mob+");
assert_eq!(trucate_text("fo\x1B[39mobarbaz", 5, &Alignment::Right, "+++"), "+++az");
assert_eq!(trucate_text("fo\x1B[39mobarbaz", 5, &Alignment::Center, "+++"), "f+++z");
}
#[test]
fn wraps_ansi_text() {
assert_eq!(wrap_text("This is \x1B[39ma very long tekst for testing\x1B[39m only.", 10), vec![
"This is \x1B[39ma",
"very long",
"tekst for",
"testing\x1B[39m",
"only."
]);
}
#[test]
fn repairs_multiline_ansi_text() {
assert_eq!(repaire_text(vec![
"This is \x1B[31mlong".to_string(),
"string 利干 sample".to_string(),
"this is 利干 sample\x1B[39m long code".to_string(),
]), vec![
"This is \x1B[31mlong\x1B[39m",
"\x1B[31mstring 利干 sample\x1B[39m",
"\x1B[31mthis is 利干 sample\x1B[39m long code",
]);
}
}