use std::borrow::Cow;
use std::fmt::{self, Write};
use std::iter;
const DEFAULT_COLUMN_SEPARATOR: &str = " ";
#[derive(Debug)]
struct TableBlueprint<'a> {
headers: Vec<&'a str>,
alignments: Vec<fmt::Alignment>,
data: Vec<Vec<&'a str>>,
columns_width: Vec<usize>,
column_separator: &'a str,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Table<'a> {
headers: Option<Vec<&'a str>>,
alignments: Option<&'a [fmt::Alignment]>,
data: Option<Vec<Vec<&'a str>>>,
max_rows: Option<usize>,
column_separator: Option<&'a str>,
}
impl Default for Table<'_> {
fn default() -> Self {
Self::new()
}
}
impl<'a> Table<'a> {
#[must_use]
pub fn new() -> Self {
Self {
headers: None,
alignments: None,
data: None,
max_rows: None,
column_separator: None,
}
}
pub fn headers(&mut self, headers: &'a [impl AsRef<str>]) -> &mut Self {
let headers: Vec<&str> = headers.iter().map(AsRef::as_ref).collect();
self.headers = Some(headers);
self
}
pub fn alignments(&mut self, alignments: &'a [fmt::Alignment]) -> &mut Self {
self.alignments = Some(alignments);
self
}
pub fn data(&mut self, data: &'a [Vec<impl AsRef<str>>]) -> &mut Self {
let data: Vec<Vec<&str>> = data
.iter()
.map(|row| row.iter().map(AsRef::as_ref).collect())
.collect();
self.data = Some(data);
self
}
pub fn max_rows(&mut self, max_rows: usize) -> &mut Self {
self.max_rows = Some(max_rows);
self
}
pub fn column_separator(&mut self, separator: &'a str) -> &mut Self {
self.column_separator = Some(separator);
self
}
#[allow(clippy::missing_errors_doc)]
pub fn render(&self, output: &mut fmt::Formatter) -> fmt::Result {
let table = self.make_table_blueprint();
if table.data.is_empty() {
return writeln!(output, "{}", table.headers.join(table.column_separator));
}
let mut buffer = String::with_capacity(15 * table.headers.len());
let mut render_row = |row: &Vec<&str>| {
buffer.clear();
for (i, cell) in row.iter().enumerate() {
let width = table.columns_width[i];
let alignment = table.alignments[i];
let is_last_column = i == table.headers.len() - 1;
_ = match alignment {
fmt::Alignment::Left if is_last_column => write!(buffer, "{cell}"),
fmt::Alignment::Left => write!(buffer, "{}", Self::align_left(cell, width)),
fmt::Alignment::Right => write!(buffer, "{}", Self::align_right(cell, width)),
fmt::Alignment::Center => write!(buffer, "{}", Self::align_center(cell, width)),
};
if !is_last_column {
_ = write!(buffer, "{}", table.column_separator);
}
}
_ = writeln!(output, "{}", buffer.trim_end());
};
if !table.headers.iter().all(|header| header.is_empty()) {
render_row(&table.headers);
}
for row in table.data {
render_row(&row);
}
Ok(())
}
fn align_left(string: &str, width: usize) -> Cow<'_, str> {
let string_len_without_colors = Self::strip_ansi_colors(string).chars().count();
let padding_len = width.saturating_sub(string_len_without_colors);
if padding_len == 0 {
return Cow::Borrowed(string);
}
Cow::Owned(format!("{string}{}", " ".repeat(padding_len)))
}
fn align_right(string: &str, width: usize) -> Cow<'_, str> {
let string_len_without_colors = Self::strip_ansi_colors(string).chars().count();
let padding_len = width.saturating_sub(string_len_without_colors);
if padding_len == 0 {
return Cow::Borrowed(string);
}
Cow::Owned(format!("{}{string}", " ".repeat(padding_len)))
}
fn align_center(string: &str, width: usize) -> Cow<'_, str> {
let string_len_without_colors = Self::strip_ansi_colors(string).chars().count();
let padding_len = width.saturating_sub(string_len_without_colors);
if padding_len == 0 {
return Cow::Borrowed(string);
}
let padding_left = padding_len / 2;
let padding_right = padding_len - padding_left;
Cow::Owned(format!(
"{}{string}{}",
" ".repeat(padding_left),
" ".repeat(padding_right)
))
}
fn strip_ansi_colors(string: &str) -> Cow<'_, str> {
enum State {
NotInSequence,
InSequence,
}
let mut state = State::NotInSequence;
let mut chars = string.chars().enumerate().peekable();
let mut output_matches_input = true;
let mut out = String::new();
#[cfg(not(tarpaulin_include))] while let Some((i, char)) = chars.next() {
match (char, &state) {
('\x1b', State::NotInSequence) => {
if let Some((_, char)) = chars.peek()
&& *char == '['
{
state = State::InSequence;
if output_matches_input {
output_matches_input = false;
out.reserve_exact(string.len() - 4);
out = string.chars().take(i).collect();
}
continue;
}
}
('m', State::InSequence) => {
state = State::NotInSequence;
continue;
}
(_, State::NotInSequence) => {}
(_, State::InSequence) => continue,
}
if output_matches_input {
continue;
}
out.push(char);
}
if output_matches_input {
Cow::Borrowed(string)
} else {
Cow::Owned(out)
}
}
fn make_table_blueprint(&self) -> TableBlueprint<'_> {
let nb_cols = self.determine_nb_columns();
let headers = self.get_headers_or_default(nb_cols);
let alignments = self.get_alignments_or_default(nb_cols);
let mut data = self.get_data_or_default();
Self::ensure_data_consistency(&headers, &alignments, &data);
if let Some(max_rows) = self.max_rows {
#[cfg(not(tarpaulin_include))] {
data = Self::apply_max_rows(data, max_rows, nb_cols);
}
}
let columns_width = Self::determine_columns_width(&headers, &data);
let column_separator = self.column_separator.unwrap_or(DEFAULT_COLUMN_SEPARATOR);
TableBlueprint {
headers,
alignments,
data,
columns_width,
column_separator,
}
}
#[cfg(not(tarpaulin_include))] fn determine_nb_columns(&self) -> usize {
if let Some(headers) = self.headers.as_ref() {
return headers.len();
}
if let Some(data) = self.data.as_ref()
&& !data.is_empty()
{
return data[0].len();
}
panic!("headers and data cannot both be empty");
}
fn get_headers_or_default(&self, nb_cols: usize) -> Vec<&str> {
match self.headers.as_ref() {
Some(headers) => headers.to_owned(),
None => [""].repeat(nb_cols),
}
}
fn get_alignments_or_default(&self, nb_cols: usize) -> Vec<fmt::Alignment> {
match self.alignments {
Some(alignments) => alignments.to_vec(),
None => [fmt::Alignment::Left].repeat(nb_cols),
}
}
fn get_data_or_default(&self) -> Vec<Vec<&str>> {
match self.data.as_ref() {
Some(data) => data.to_owned(),
None => Vec::new(),
}
}
fn ensure_data_consistency(
headers: &[&str],
alignments: &[fmt::Alignment],
data: &[Vec<&str>],
) {
assert_eq!(
headers.len(),
alignments.len(),
"number of headers must match alignments"
);
assert!(
data.iter().all(|row| row.len() == headers.len()),
"number of headers must match columns in data"
);
}
fn apply_max_rows(mut data: Vec<Vec<&str>>, max_rows: usize, nb_cols: usize) -> Vec<Vec<&str>> {
if data.len() <= max_rows {
return data; }
if max_rows == 0 {
return vec![["..."].repeat(nb_cols)];
}
if max_rows == 1 {
data.truncate(1);
return data
.into_iter()
.chain(iter::once(["..."].repeat(nb_cols)))
.collect();
}
let nb_head = max_rows / 2;
let nb_tail = max_rows - nb_head;
let tail = data.split_off(data.len() - nb_tail);
data.truncate(nb_head);
let head = data;
head.into_iter()
.chain(iter::once(["..."].repeat(nb_cols)))
.chain(tail)
.collect()
}
fn determine_columns_width(headers: &[&str], data: &[Vec<&str>]) -> Vec<usize> {
let mut cols_width = vec![0; headers.len()];
for i in 0..headers.len() {
let column_values: Vec<&str> = data.iter().map(|x| x[i]).collect();
let max_width = Self::width_of_longest_value_in_column(headers[i], &column_values);
cols_width[i] = max_width;
}
cols_width
}
fn width_of_longest_value_in_column(header: &str, column_values: &[&str]) -> usize {
let header = iter::once(&header);
let column_values = column_values.iter();
header
.chain(column_values)
.map(|x| Self::strip_ansi_colors(x).chars().count())
.max()
.expect("iterator cannot be empty because header is required")
}
}
impl fmt::Display for Table<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.render(f)
}
}
#[cfg(test)]
mod tests {
use std::fmt::Alignment::{Center, Left, Right};
use super::*;
#[test]
fn table_default_builder() {
assert_eq!(Table::new(), Table::default());
}
#[test]
fn table_regular() {
let table = Table::new()
.headers(&["SHORT", "WITH SPACE", "LAST COLUMN"])
.alignments(&[Left, Left, Left])
.data(&[
vec![
"Value larger than header",
"Column name has space",
"No trailing whitespace",
],
vec!["---", "---", "---"],
])
.to_string();
println!("{table}");
assert_eq!(
table,
"\
SHORT WITH SPACE LAST COLUMN
Value larger than header Column name has space No trailing whitespace
--- --- ---
"
);
}
#[test]
fn table_single_column() {
let table = Table::new()
.headers(&["foo"])
.data(&[vec!["bar"], vec!["baz"]])
.column_separator("|")
.to_string();
println!("{table}");
assert_eq!(
table,
"\
foo
bar
baz
"
);
}
#[test]
fn table_without_data() {
let table = Table::new()
.headers(&["SHORT", "WITH SPACE", "LAST COLUMN"])
.alignments(&[Left, Left, Left])
.to_string();
println!("{table}");
assert_eq!(
table,
"\
SHORT WITH SPACE LAST COLUMN
"
);
}
#[test]
fn table_without_data_with_custom_column_separator() {
let table = Table::new()
.headers(&["A", "B", "C"])
.column_separator("|")
.to_string();
println!("{table}");
assert_eq!(table, "A|B|C\n");
}
#[test]
fn table_all_empty_headers_not_rendered() {
let table = Table::new()
.headers(&["", ""])
.data(&[vec!["---", "----------------"]])
.to_string();
println!("{table}");
assert_eq!(
table,
"\
--- ----------------
"
);
}
#[test]
fn table_some_empty_headers_all_rendered() {
let table = Table::new()
.headers(&["", "-"])
.data(&[vec!["---", "----------------"]])
.to_string();
println!("{table}");
assert_eq!(
table,
r" -
--- ----------------
"
);
}
#[test]
fn table_default_headers() {
let table = Table::new()
.data(&[vec!["---", "----------------"]])
.to_string();
println!("{table}");
assert_eq!(
table,
"\
--- ----------------
"
);
}
#[test]
fn table_empty_rows_dont_contain_whitespace() {
let table = Table::new()
.headers(&["FOO", "BAR", "BAZ"])
.data(&[
vec!["----------", "", "----------"],
vec!["", "", ""],
vec!["", "----------", ""],
])
.to_string();
println!("{table}");
assert_eq!(
table,
"\
FOO BAR BAZ
---------- ----------
----------
"
);
}
#[test]
fn table_rows_dont_end_with_trailing_whitespace() {
let table = Table::new()
.headers(&["FOO", "BAR", "BAZ "])
.data(&[
vec!["----------", "", "---------- "],
vec![" ", " ", " "],
vec!["", "----------", ""],
])
.to_string();
println!("{table}");
assert_eq!(
table,
"\
FOO BAR BAZ
---------- ----------
----------
"
);
}
#[test]
fn table_headers_alignment() {
let table = Table::new()
.headers(&["ALIGN-LEFT", "ALIGN-CENTER", "ALIGN-RIGHT"])
.alignments(&[Left, Center, Right])
.data(&[
vec![
"Header is aligned Left",
"Header is aligned Center",
"Header is aligned Right",
],
vec!["---", "---", "---"],
])
.to_string();
println!("{table}");
assert_eq!(
table,
"\
ALIGN-LEFT ALIGN-CENTER ALIGN-RIGHT
Header is aligned Left Header is aligned Center Header is aligned Right
--- --- ---
"
);
}
#[test]
fn table_values_alignment() {
let table = Table::new()
.headers(&["ALIGN-LEFT", "ALIGN-CENTER", "ALIGN-RIGHT"])
.alignments(&[Left, Center, Right])
.data(&[vec!["Left", "Center", "Right"], vec!["---", "---", "---"]])
.to_string();
println!("{table}");
assert_eq!(
table,
"\
ALIGN-LEFT ALIGN-CENTER ALIGN-RIGHT
Left Center Right
--- --- ---
"
);
}
#[test]
fn table_default_alignments() {
let table = Table::new()
.headers(&["VALUE LEFT", "COLUMN LEFT"])
.data(&[vec!["---", "----------------"]])
.to_string();
println!("{table}");
assert_eq!(
table,
"\
VALUE LEFT COLUMN LEFT
--- ----------------
"
);
}
#[test]
fn table_default_headers_and_alignments() {
let table = Table::new()
.data(&[
vec!["---", "----------------"],
vec!["----------------", "---"],
])
.to_string();
println!("{table}");
assert_eq!(
table,
"\
--- ----------------
---------------- ---
"
);
}
#[test]
fn table_with_empty_data() {
let table = Table::new()
.headers(&["SHORT", "WITH SPACE", "LAST COLUMN"])
.alignments(&[Left, Left, Left])
.data(&[] as &[Vec<&str>; 0])
.to_string();
println!("{table}");
assert_eq!(
table,
"\
SHORT WITH SPACE LAST COLUMN
"
);
}
#[test]
fn table_completely_empty() {
let table = Table::new()
.headers(&[] as &[&str; 0])
.alignments(&[])
.data(&[] as &[Vec<&str>; 0])
.to_string();
println!("{table}");
assert_eq!(table, "\n");
}
#[test]
#[should_panic(expected = "headers and data cannot both be empty")]
fn table_error_completely_empty_with_default_headers() {
let table = Table::new()
.alignments(&[])
.data(&[] as &[Vec<&str>; 0])
.to_string();
println!("{table}");
assert_eq!(table, "\n");
}
#[test]
fn table_completely_empty_with_default_alignments() {
let table = Table::new()
.headers(&[] as &[&str; 0])
.data(&[] as &[Vec<&str>; 0])
.to_string();
println!("{table}");
assert_eq!(table, "\n");
}
#[test]
#[should_panic(expected = "headers and data cannot both be empty")]
fn table_error_completely_empty_with_default_headers_and_alignments() {
let table = Table::new().data(&[] as &[Vec<&str>; 0]).to_string();
println!("{table}");
assert_eq!(table, "\n");
}
#[test]
#[should_panic(expected = "number of headers must match alignments")]
fn table_error_nb_headers_neq_nb_alignments() {
Table::new()
.headers(&["COLUMN 1", "COLUMN 2"])
.alignments(&[Left, Left, Left])
.data(&[vec!["---", "---"]])
.to_string();
}
#[test]
#[should_panic(expected = "number of headers must match columns in data")]
fn table_error_nb_headers_neq_nb_columns_in_data() {
Table::new()
.headers(&["COLUMN 1", "COLUMN 2"])
.alignments(&[Left, Left])
.data(&[
vec!["---", "---"],
vec!["---", "---", "---"],
vec!["---", "---"],
])
.to_string();
}
#[test]
fn table_max_rows_regular() {
let table = Table::new()
.max_rows(5)
.headers(&["#", "COLUMN 1", "COLUMN 2"])
.alignments(&[Left, Left, Right])
.data(&[
vec!["1.", "---", "---"],
vec!["2.", "---", "---"],
vec!["3.", "------------", "------------"],
vec!["4.", "------------", "------------"],
vec!["5.", "---", "---"],
vec!["6.", "---", "---"],
vec!["7.", "---", "---"],
])
.to_string();
println!("{table}");
assert_eq!(
table,
"\
# COLUMN 1 COLUMN 2
1. --- ---
2. --- ---
... ... ...
5. --- ---
6. --- ---
7. --- ---
"
);
}
#[test]
fn table_max_rows_smallest_regular_case() {
let table = Table::new()
.max_rows(2)
.headers(&["#", "COLUMN 1", "COLUMN 2"])
.alignments(&[Left, Left, Right])
.data(&[
vec!["1.", "---", "---"],
vec!["2.", "---", "---"],
vec!["3.", "---", "---"],
])
.to_string();
println!("{table}");
assert_eq!(
table,
"\
# COLUMN 1 COLUMN 2
1. --- ---
... ... ...
3. --- ---
"
);
}
#[test]
fn table_max_rows_elided_rows_do_not_impact_column_width() {
let table = Table::new()
.max_rows(1)
.headers(&["#", "COLUMN 1", "COLUMN 2"])
.alignments(&[Left, Left, Right])
.data(&[
vec!["1.", "---", "---"],
vec!["2.", "------------", "------------"],
])
.to_string();
println!("{table}");
assert_eq!(
table,
"\
# COLUMN 1 COLUMN 2
1. --- ---
... ... ...
"
);
}
#[test]
fn table_max_rows_gt_nb_rows() {
let table = Table::new()
.max_rows(8)
.headers(&["#", "COLUMN 1", "COLUMN 2"])
.alignments(&[Left, Left, Right])
.data(&[
vec!["1.", "---", "---"],
vec!["2.", "---", "---"],
vec!["3.", "------------", "------------"],
vec!["4.", "------------", "------------"],
vec!["5.", "---", "---"],
vec!["6.", "---", "---"],
vec!["7.", "---", "---"],
])
.to_string();
println!("{table}");
assert_eq!(
table,
"\
# COLUMN 1 COLUMN 2
1. --- ---
2. --- ---
3. ------------ ------------
4. ------------ ------------
5. --- ---
6. --- ---
7. --- ---
"
);
}
#[test]
fn table_max_rows_eq_nb_rows() {
let table = Table::new()
.max_rows(7)
.headers(&["#", "COLUMN 1", "COLUMN 2"])
.alignments(&[Left, Left, Right])
.data(&[
vec!["1.", "---", "---"],
vec!["2.", "---", "---"],
vec!["3.", "------------", "------------"],
vec!["4.", "------------", "------------"],
vec!["5.", "---", "---"],
vec!["6.", "---", "---"],
vec!["7.", "---", "---"],
])
.to_string();
println!("{table}");
assert_eq!(
table,
"\
# COLUMN 1 COLUMN 2
1. --- ---
2. --- ---
3. ------------ ------------
4. ------------ ------------
5. --- ---
6. --- ---
7. --- ---
"
);
}
#[test]
fn table_max_rows_max_zero() {
let table = Table::new()
.max_rows(0)
.headers(&["#", "COLUMN 1", "COLUMN 2"])
.alignments(&[Left, Left, Right])
.data(&[
vec!["1.", "---", "---"],
vec!["2.", "------------", "------------"],
])
.to_string();
println!("{table}");
assert_eq!(
table,
"\
# COLUMN 1 COLUMN 2
... ... ...
"
);
}
#[test]
fn table_max_rows_max_zero_with_empty_data() {
let table = Table::new()
.max_rows(0)
.headers(&["#", "COLUMN 1", "COLUMN 2"])
.alignments(&[Left, Left, Right])
.data(&[] as &[Vec<&str>; 0])
.to_string();
println!("{table}");
assert_eq!(
table,
"\
# COLUMN 1 COLUMN 2
"
);
}
#[test]
fn table_max_rows_max_zero_without_header() {
let table = Table::new()
.max_rows(0)
.data(&[vec!["---", "----------------"]])
.to_string();
println!("{table}");
assert_eq!(table, "... ...\n");
}
#[test]
fn table_max_rows_max_one() {
let table = Table::new()
.max_rows(1)
.headers(&["#", "COLUMN 1", "COLUMN 2"])
.alignments(&[Left, Left, Right])
.data(&[
vec!["1.", "---", "---"],
vec!["2.", "------------", "------------"],
vec!["3.", "---", "---"],
])
.to_string();
println!("{table}");
assert_eq!(
table,
"\
# COLUMN 1 COLUMN 2
1. --- ---
... ... ...
"
);
}
#[test]
fn table_column_separator() {
let table = Table::new()
.headers(&["1", "2", "3"])
.alignments(&[Right, Center, Left])
.data(&[
vec!["---", "---", "---"],
vec!["------", "------", "------"],
vec!["---", "---", "---"],
])
.column_separator("|")
.to_string();
println!("{table}");
assert_eq!(
table,
" 1| 2 |3
---| --- |---
------|------|------
---| --- |---
"
);
}
#[test]
fn table_render_multiple_times() {
let data = [vec!["---"]];
let table = Table::new()
.headers(&["HEADER"])
.alignments(&[Left])
.data(&data)
.to_owned();
let render_1 = table.to_string();
let render_2 = table.to_string();
println!("{table}");
assert_eq!(render_1, "HEADER\n---\n");
assert_eq!(render_1, render_2);
}
#[test]
fn table_accented_characters_count_as_one_aligned_left() {
let table = Table::new()
.headers(&["", ""])
.alignments(&[Left, Left])
.data(&[
vec!["éà ô", "abc"], vec!["123", "abc"], vec!["-----", "-----"], ])
.column_separator("|")
.to_string();
println!("{table}");
assert_eq!(
table,
"\
éà ô |abc
123 |abc
-----|-----
"
);
}
#[test]
fn table_accented_characters_count_as_one_aligned_right() {
let table = Table::new()
.headers(&["", ""])
.alignments(&[Right, Right])
.data(&[
vec!["éà ", "abc"], vec!["123", "abc"], vec!["-----", "-----"], ])
.column_separator("|")
.to_string();
println!("{table}");
assert_eq!(
table,
"\
\x20 éà | abc
123| abc
-----|-----
"
);
}
#[test]
fn table_accented_characters_count_as_one_aligned_center() {
let table = Table::new()
.headers(&["", ""])
.alignments(&[Center, Center])
.data(&[
vec!["éà ô", "abc"], vec!["123", "abc"], vec!["-----", "-----"], ])
.column_separator("|")
.to_string();
println!("{table}");
assert_eq!(
table,
"\
\x20éà ô | abc
123 | abc
-----|-----
"
);
}
#[test]
fn table_width_of_longest_value_in_column() {
assert_eq!(
Table::width_of_longest_value_in_column("hello", &["foo", "héllo!"]),
6
);
}
#[test]
fn table_ansi_colors_not_counted_in_column_width_align_left() {
let table = Table::new()
.headers(&["", "", ""])
.alignments(&[Left, Left, Left])
.data(&[
vec!["-", "\x1b[92mfoo\x1b[0m", "-"], vec!["-", "foo", "-"], vec!["-", "barbaz", "-"], ])
.column_separator("|")
.to_string();
println!("{table}");
assert_eq!(
table,
"\
-|\x1b[92mfoo\x1b[0m |-
-|foo |-
-|barbaz|-
"
);
}
#[test]
fn table_ansi_colors_not_counted_in_column_width_align_right() {
let table = Table::new()
.headers(&["", "", ""])
.alignments(&[Right, Right, Right])
.data(&[
vec!["-", "\x1b[92mfoo\x1b[0m", "-"], vec!["-", "foo", "-"], vec!["-", "barbaz", "-"], ])
.column_separator("|")
.to_string();
println!("{table}");
assert_eq!(
table,
"\
-| \x1b[92mfoo\x1b[0m|-
-| foo|-
-|barbaz|-
"
);
}
#[test]
fn table_ansi_colors_not_counted_in_column_width_align_center() {
let table = Table::new()
.headers(&["", "", ""])
.alignments(&[Center, Center, Center])
.data(&[
vec!["-", "\x1b[92mfoo\x1b[0m", "-"], vec!["-", "foo", "-"], vec!["-", "barbaz", "-"], ])
.column_separator("|")
.to_string();
println!("{table}");
assert_eq!(
table,
"\
-| \x1b[92mfoo\x1b[0m |-
-| foo |-
-|barbaz|-
"
);
}
#[test]
fn table_ansi_colors_in_headers() {
let table = Table::new()
.headers(&["-", "\x1b[92mhi\x1b[0m", "-"]) .alignments(&[Center, Center, Center])
.data(&[
vec!["-", "hi", "-"], vec!["-", "barbaz", "-"], ])
.column_separator("|")
.to_string();
println!("{table}");
assert_eq!(
table,
"\
-| \x1b[92mhi\x1b[0m |-
-| hi |-
-|barbaz|-
"
);
}
#[test]
fn strip_ansi_colors() {
let strip = Table::strip_ansi_colors;
assert_eq!(strip("\x1b[0;90mhello\x1b[0m"), "hello");
assert_eq!(strip("\u{1b}[0;91mhello\u{1b}[0m"), "hello");
assert_eq!(strip("\x1b[38;5;82mHello\x1b[0m"), "Hello");
assert_eq!(strip("hello \x1b[31mworld\x1b[0m!"), "hello world!");
assert_eq!(strip("hello world"), "hello world");
assert_eq!(strip(""), "");
assert_eq!(strip("\x1b[0;90m\x1b[0m"), "");
assert_eq!(strip("\x1b0;92mhello\x1b0m"), "\x1b0;92mhello\x1b0m");
assert_eq!(strip("\x1b[31hello"), ""); assert_eq!(strip("text with \x1b[no escape\x1b[0m"), "text with ");
assert_eq!(strip("\x1b[31mHello"), "Hello");
assert_eq!(strip("text\x1b"), "text\x1b");
assert_eq!(strip("text\x1b["), "text");
assert_eq!(strip("\x1b[0;90m\x1b[1;92mhello\x1b[0m"), "hello");
assert_eq!(strip("\x1b[31m\x1b[32mtext\x1b[0m"), "text");
assert_eq!(strip("\x1b[0;90mfoo\x1b[0m").len(), 3);
}
}