mirror of
https://gitlab.com/arnekeller/ascii-table.git
synced 2024-12-04 13:39:06 +00:00
Merge branch 'smart_string'
This commit is contained in:
commit
a30489cd5b
216
src/lib.rs
216
src/lib.rs
@ -162,23 +162,20 @@ impl AsciiTable {
|
||||
self.format_inner(self.stringify(data))
|
||||
}
|
||||
|
||||
fn format_inner(&self, data: Vec<Vec<String>>) -> String {
|
||||
fn format_inner(&self, data: Vec<Vec<SmartString>>) -> String {
|
||||
let num_cols = data.iter().map(|row| row.len()).max().unwrap_or(0);
|
||||
if !self.valid(&data, num_cols) {
|
||||
return self.format_empty()
|
||||
}
|
||||
|
||||
let header = self.stringify_header(num_cols);
|
||||
let data = self.square_data(data, num_cols);
|
||||
let has_header = self.columns.iter().any(|(_, col)| !col.header.is_empty());
|
||||
let widths = self.column_widths(&data, num_cols);
|
||||
let has_header = header.iter().any(|text| !text.is_empty());
|
||||
let widths = self.column_widths(&header, &data, num_cols);
|
||||
|
||||
let mut result = String::new();
|
||||
result.push_str(&self.format_first(&widths));
|
||||
if has_header {
|
||||
let default_conf = &DEFAULT_COLUMN;
|
||||
let header: Vec<_> = (0..num_cols).map(|a|
|
||||
self.columns.get(&a).unwrap_or(default_conf).header.as_str()
|
||||
).collect();
|
||||
result.push_str(&self.format_header_row(&header, &widths));
|
||||
result.push_str(&self.format_middle(&widths));
|
||||
}
|
||||
@ -189,7 +186,7 @@ impl AsciiTable {
|
||||
result
|
||||
}
|
||||
|
||||
fn valid(&self, data: &Vec<Vec<String>>, num_cols: usize) -> bool {
|
||||
fn valid(&self, data: &Vec<Vec<SmartString>>, num_cols: usize) -> bool {
|
||||
if data.len() == 0 {
|
||||
false
|
||||
} else if num_cols == 0 {
|
||||
@ -205,55 +202,40 @@ impl AsciiTable {
|
||||
((num_cols - 1) * 3) + 4
|
||||
}
|
||||
|
||||
fn stringify<L1, L2, T>(&self, data: L1) -> Vec<Vec<String>>
|
||||
fn stringify<L1, L2, T>(&self, data: L1) -> Vec<Vec<SmartString>>
|
||||
where L1: IntoIterator<Item = L2>,
|
||||
L2: IntoIterator<Item = T>,
|
||||
T: Display {
|
||||
data.into_iter().map(|row| row.into_iter().map(|cell| cell.to_string()).collect()).collect()
|
||||
data.into_iter().map(|row| row.into_iter().map(|cell| SmartString::from(cell)).collect()).collect()
|
||||
}
|
||||
|
||||
fn square_data(&self, mut data: Vec<Vec<String>>, num_cols: usize) -> Vec<Vec<String>> {
|
||||
fn stringify_header(&self, num_cols: usize) -> Vec<SmartString> {
|
||||
let default_conf = &DEFAULT_COLUMN;
|
||||
(0..num_cols).map(|a|
|
||||
SmartString::from(&self.columns.get(&a).unwrap_or(default_conf).header)
|
||||
).collect()
|
||||
}
|
||||
|
||||
fn square_data(&self, mut data: Vec<Vec<SmartString>>, num_cols: usize) -> Vec<Vec<SmartString>> {
|
||||
for row in data.iter_mut() {
|
||||
while row.len() < num_cols {
|
||||
row.push(String::new())
|
||||
row.push(SmartString::new())
|
||||
}
|
||||
}
|
||||
data
|
||||
}
|
||||
|
||||
fn column_widths(&self, data: &[Vec<String>], num_cols: usize) -> Vec<usize> {
|
||||
fn column_widths(&self, header: &[SmartString], data: &[Vec<SmartString>], num_cols: usize) -> Vec<usize> {
|
||||
let result: Vec<_> = (0..num_cols).map(|a| {
|
||||
let default_conf = &DEFAULT_COLUMN;
|
||||
let conf = self.columns.get(&a).unwrap_or(default_conf);
|
||||
let column_width = data.iter().map(|row| self.count_characters(&row[a])).max().unwrap();
|
||||
let header_width = conf.header.chars().count();
|
||||
let column_width = data.iter().map(|row| row[a].char_len()).max().unwrap();
|
||||
let header_width = header[a].char_len();
|
||||
column_width.max(header_width).min(conf.max_width)
|
||||
}).collect();
|
||||
self.truncate_widths(result)
|
||||
}
|
||||
|
||||
fn count_characters(&self, cell: &str) -> usize {
|
||||
// let mut count = 0;
|
||||
// let mut block = false;
|
||||
// let mut iter = cell.chars().peekable();
|
||||
// while let Some(ch) = iter.next() {
|
||||
// if block {
|
||||
// if ch != '\u{1b}' && ch != '[' && ch != ';' && ch != 'm' && !('0'..'9').contains(&ch) {
|
||||
// block = false;
|
||||
// count += 1;
|
||||
// }
|
||||
// } else {
|
||||
// if ch == '\u{1b}' && Some(&'[') == iter.peek() {
|
||||
// block = true;
|
||||
// } else {
|
||||
// count += 1;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// count
|
||||
cell.chars().count()
|
||||
}
|
||||
|
||||
fn truncate_widths(&self, mut widths: Vec<usize>) -> Vec<usize> {
|
||||
let max_width = self.max_width;
|
||||
let table_padding = Self::smallest_width(widths.len());
|
||||
@ -266,7 +248,7 @@ impl AsciiTable {
|
||||
widths
|
||||
}
|
||||
|
||||
fn format_line(&self, row: &[String], head: &str, delim: &str, tail: &str) -> String {
|
||||
fn format_line(&self, row: &[SmartString], head: &str, delim: &str, tail: &str) -> String {
|
||||
let mut result = String::new();
|
||||
result.push_str(head);
|
||||
for cell in row {
|
||||
@ -282,21 +264,21 @@ impl AsciiTable {
|
||||
|
||||
fn format_empty(&self) -> String {
|
||||
self.format_first(&vec![0])
|
||||
+ &self.format_line(&vec![String::new()], &format!("{}{}", NS, ' '), &format!("{}{}{}", ' ', NS, ' '), &format!("{}{}", ' ', NS))
|
||||
+ &self.format_last(&vec![0])
|
||||
+ &self.format_line(&[SmartString::new()], &format!("{}{}", NS, ' '), &format!("{}{}{}", ' ', NS, ' '), &format!("{}{}", ' ', NS))
|
||||
+ &self.format_last(&[0])
|
||||
}
|
||||
|
||||
fn format_first(&self, widths: &[usize]) -> String {
|
||||
let row: Vec<String> = widths.iter().map(|&x| EW.repeat(x)).collect();
|
||||
let row: Vec<_> = widths.iter().map(|&x| SmartString::from_visible(EW.repeat(x))).collect();
|
||||
self.format_line(&row, &format!("{}{}", SE, EW), &format!("{}{}{}", EW, EWS, EW), &format!("{}{}", EW, SW))
|
||||
}
|
||||
|
||||
fn format_middle(&self, widths: &[usize]) -> String {
|
||||
let row: Vec<String> = widths.iter().map(|&x| EW.repeat(x)).collect();
|
||||
let row: Vec<_> = widths.iter().map(|&x| SmartString::from_visible(EW.repeat(x))).collect();
|
||||
self.format_line(&row, &format!("{}{}", NES, EW), &format!("{}{}{}", EW, NEWS, EW), &format!("{}{}", EW, NWS))
|
||||
}
|
||||
|
||||
fn format_row(&self, row: &[String], widths: &[usize]) -> String {
|
||||
fn format_row(&self, row: &[SmartString], widths: &[usize]) -> String {
|
||||
let row: Vec<_> = (0..widths.len()).map(|a| {
|
||||
let cell = &row[a];
|
||||
let width = widths[a];
|
||||
@ -307,38 +289,41 @@ impl AsciiTable {
|
||||
self.format_line(&row, &format!("{}{}", NS, ' '), &format!("{}{}{}", ' ', NS, ' '), &format!("{}{}", ' ', NS))
|
||||
}
|
||||
|
||||
fn format_header_row(&self, row: &[&str], widths: &[usize]) -> String {
|
||||
let row: Vec<String> = row.iter().zip(widths.iter()).map(|(cell, &width)|
|
||||
fn format_header_row(&self, row: &[SmartString], widths: &[usize]) -> String {
|
||||
let row: Vec<_> = row.iter().zip(widths.iter()).map(|(cell, &width)|
|
||||
self.format_cell(cell, width, ' ', Align::Left)
|
||||
).collect();
|
||||
self.format_line(&row, &format!("{}{}", NS, ' '), &format!("{}{}{}", ' ', NS, ' '), &format!("{}{}", ' ', NS))
|
||||
}
|
||||
|
||||
fn format_last(&self, widths: &[usize]) -> String {
|
||||
let row: Vec<String> = widths.iter().map(|&x| EW.repeat(x)).collect();
|
||||
let row: Vec<_> = widths.iter().map(|&x| SmartString::from_visible(EW.repeat(x))).collect();
|
||||
self.format_line(&row, &format!("{}{}", NE, EW), &format!("{}{}{}", EW, NEW, EW), &format!("{}{}", EW, NW))
|
||||
}
|
||||
|
||||
fn format_cell(&self, text: &str, len: usize, pad: char, align: Align) -> String {
|
||||
if text.chars().count() > len {
|
||||
let mut result: String = text.chars().take(len).collect();
|
||||
fn format_cell(&self, text: &SmartString, len: usize, pad: char, align: Align) -> SmartString {
|
||||
if text.char_len() > len {
|
||||
let mut result = text.clone();
|
||||
while result.char_len() > len {
|
||||
result.pop();
|
||||
}
|
||||
if result.pop().is_some() {
|
||||
result.push('+')
|
||||
result.push_visible('+')
|
||||
}
|
||||
result
|
||||
} else {
|
||||
let mut result = text.to_string();
|
||||
let mut result = text.clone();
|
||||
match align {
|
||||
Align::Left => while result.chars().count() < len {
|
||||
result.push(pad)
|
||||
Align::Left => while result.char_len() < len {
|
||||
result.push_visible(pad)
|
||||
}
|
||||
Align::Right => while result.chars().count() < len {
|
||||
result.insert(0, pad)
|
||||
Align::Right => while result.char_len() < len {
|
||||
result.lpush_visible(pad)
|
||||
}
|
||||
Align::Center => while result.chars().count() < len {
|
||||
result.push(pad);
|
||||
if result.chars().count() < len {
|
||||
result.insert(0, pad)
|
||||
Align::Center => while result.char_len() < len {
|
||||
result.push_visible(pad);
|
||||
if result.char_len() < len {
|
||||
result.lpush_visible(pad)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -346,3 +331,116 @@ impl AsciiTable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct SmartString {
|
||||
fragments: Vec<(bool, String)>
|
||||
}
|
||||
|
||||
impl SmartString {
|
||||
|
||||
fn new() -> Self {
|
||||
Self { fragments: Vec::new() }
|
||||
}
|
||||
|
||||
fn from<T>(string: T) -> Self
|
||||
where T: Display {
|
||||
let string = string.to_string();
|
||||
let mut fragments = Vec::new();
|
||||
let mut visible = true;
|
||||
let mut buf = String::new();
|
||||
let mut iter = string.chars().peekable();
|
||||
|
||||
while let Some(ch) = iter.next() {
|
||||
if visible {
|
||||
if ch == '\u{1b}' && Some(&'[') == iter.peek() {
|
||||
if !buf.is_empty() {
|
||||
fragments.push((visible, buf));
|
||||
}
|
||||
visible = !visible;
|
||||
buf = String::new();
|
||||
}
|
||||
buf.push(ch);
|
||||
} else {
|
||||
if ch == 'm' {
|
||||
buf.push(ch);
|
||||
if !buf.is_empty() {
|
||||
fragments.push((visible, buf));
|
||||
}
|
||||
visible = !visible;
|
||||
buf = String::new();
|
||||
} else if ch != '[' && ch != ';' && !('0'..='9').contains(&ch) {
|
||||
if !buf.is_empty() {
|
||||
fragments.push((visible, buf));
|
||||
}
|
||||
visible = !visible;
|
||||
buf = String::new();
|
||||
buf.push(ch);
|
||||
} else {
|
||||
buf.push(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
if !buf.is_empty() {
|
||||
fragments.push((visible, buf));
|
||||
}
|
||||
|
||||
Self { fragments }
|
||||
}
|
||||
|
||||
fn from_visible(string: String) -> Self {
|
||||
Self { fragments: vec![(true, string)] }
|
||||
}
|
||||
|
||||
fn char_len(&self) -> usize {
|
||||
self.fragments.iter()
|
||||
.filter(|(visible, _)| *visible)
|
||||
.map(|(_, string)| string.chars().count())
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.fragments.iter()
|
||||
.filter(|(visible, _)| *visible)
|
||||
.all(|(_, string)| string.is_empty())
|
||||
}
|
||||
|
||||
fn pop(&mut self) -> Option<char> {
|
||||
self.fragments.iter_mut()
|
||||
.filter(|(visible, string)| *visible && !string.is_empty())
|
||||
.last()
|
||||
.and_then(|(_, string)| string.pop())
|
||||
}
|
||||
|
||||
fn push_visible(&mut self, ch: char) {
|
||||
let last_fragment = self.fragments.iter_mut()
|
||||
.filter(|(visible, _)| *visible)
|
||||
.map(|(_, string)| string)
|
||||
.last();
|
||||
if let Some(fragment) = last_fragment {
|
||||
fragment.push(ch);
|
||||
} else {
|
||||
self.fragments.push((true, ch.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
fn lpush_visible(&mut self, ch: char) {
|
||||
let first_fragment = self.fragments.iter_mut()
|
||||
.filter(|(visible, _)| *visible)
|
||||
.map(|(_, string)| string)
|
||||
.next();
|
||||
if let Some(fragment) = first_fragment {
|
||||
fragment.insert(0, ch);
|
||||
} else {
|
||||
self.fragments.insert(0, (true, ch.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for SmartString {
|
||||
|
||||
fn fmt(&self, fmt: &mut ::std::fmt::Formatter) -> Result<(), ::std::fmt::Error> {
|
||||
let concat: String = self.fragments.iter().map(|(_, string)| string.as_str()).collect();
|
||||
concat.fmt(fmt)
|
||||
}
|
||||
}
|
||||
|
145
src/test.rs
145
src/test.rs
@ -523,13 +523,107 @@ fn mixed_types() {
|
||||
}
|
||||
|
||||
#[ignore]
|
||||
#[test]
|
||||
fn color_codes_zero() {
|
||||
let config = AsciiTable::default();
|
||||
let input = vec![vec![
|
||||
"\u{1b}[0mHello\u{1b}[0m"
|
||||
]];
|
||||
let expected = "┌───────┐\n\
|
||||
│ \u{1b}[0mHello\u{1b}[0m │\n\
|
||||
└───────┘\n";
|
||||
|
||||
assert_eq!(expected, config.format(input));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_codes_zero_inbetween() {
|
||||
let config = AsciiTable::default();
|
||||
let input = vec![vec![
|
||||
"He\u{1b}[0ml\u{1b}[0mlo"
|
||||
]];
|
||||
let expected = "┌───────┐\n\
|
||||
│ He\u{1b}[0ml\u{1b}[0mlo │\n\
|
||||
└───────┘\n";
|
||||
|
||||
assert_eq!(expected, config.format(input));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_codes_m5() {
|
||||
let config = AsciiTable::default();
|
||||
let input = vec![
|
||||
vec!["mmmmm".color(Color::Blue).bg_color(Color::Yellow).bold()]
|
||||
];
|
||||
let expected = "┌───────┐\n\
|
||||
│ \u{1b}[38;5;4m\u{1b}[48;5;3;1mmmmmm\u{1b}[0m │\n\
|
||||
└───────┘\n";
|
||||
|
||||
assert_eq!(expected, config.format(input));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_codes_b5() {
|
||||
let config = AsciiTable::default();
|
||||
let input = vec![
|
||||
vec!["[[[[[".color(Color::Blue).bg_color(Color::Yellow).bold()]
|
||||
];
|
||||
let expected = "┌───────┐\n\
|
||||
│ \u{1b}[38;5;4m\u{1b}[48;5;3;1m[[[[[\u{1b}[0m │\n\
|
||||
└───────┘\n";
|
||||
|
||||
assert_eq!(expected, config.format(input));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_codes_s5() {
|
||||
let config = AsciiTable::default();
|
||||
let input = vec![
|
||||
vec![";;;;;".color(Color::Blue).bg_color(Color::Yellow).bold()]
|
||||
];
|
||||
let expected = "┌───────┐\n\
|
||||
│ \u{1b}[38;5;4m\u{1b}[48;5;3;1m;;;;;\u{1b}[0m │\n\
|
||||
└───────┘\n";
|
||||
|
||||
assert_eq!(expected, config.format(input));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_codes_n5() {
|
||||
let config = AsciiTable::default();
|
||||
let input = vec![
|
||||
vec!["00000".color(Color::Blue).bg_color(Color::Yellow).bold()]
|
||||
];
|
||||
let expected = "┌───────┐\n\
|
||||
│ \u{1b}[38;5;4m\u{1b}[48;5;3;1m00000\u{1b}[0m │\n\
|
||||
└───────┘\n";
|
||||
|
||||
assert_eq!(expected, config.format(input));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_codes_missing_m() {
|
||||
let config = AsciiTable::default();
|
||||
let input = vec![vec![
|
||||
"\u{1b}[0Hello\u{1b}[0"
|
||||
]];
|
||||
let expected = "┌───────┐\n\
|
||||
│ \u{1b}[0Hello\u{1b}[0 │\n\
|
||||
└───────┘\n";
|
||||
|
||||
assert_eq!(expected, config.format(input));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_codes() {
|
||||
let config = AsciiTable::default();
|
||||
let text = "Hello".color(Color::Blue).bg_color(Color::Yellow).bold();
|
||||
let input = vec![vec![text]];
|
||||
let input = vec![
|
||||
vec!["Hello".color(Color::Blue).bg_color(Color::Yellow).bold()],
|
||||
vec!["Hello".gradient(Color::Red)]
|
||||
];
|
||||
let expected = "┌───────┐\n\
|
||||
│ \u{1b}[38;5;4m\u{1b}[48;5;3;1mHello\u{1b}[0m │\n\
|
||||
│ \u{1b}[38;2;255;0;0mH\u{1b}[38;2;255;6;0me\u{1b}[38;2;255;13;0ml\u{1b}[38;2;255;19;0ml\u{1b}[38;2;255;26;0mo\u{1b}[0m │\n\
|
||||
└───────┘\n";
|
||||
|
||||
assert_eq!(expected, config.format(input));
|
||||
@ -550,3 +644,50 @@ fn color_codes_in_header() {
|
||||
|
||||
assert_eq!(expected, config.format(input));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_codes_pad_right() {
|
||||
let config = AsciiTable::default();
|
||||
let input = vec![
|
||||
vec!["Hello".color(Color::Blue).bg_color(Color::Yellow).bold()],
|
||||
vec!["H".color(Color::Blue).bg_color(Color::Yellow).bold()]
|
||||
];
|
||||
let expected = "┌───────┐\n\
|
||||
│ \u{1b}[38;5;4m\u{1b}[48;5;3;1mHello\u{1b}[0m │\n\
|
||||
│ \u{1b}[38;5;4m\u{1b}[48;5;3;1mH \u{1b}[0m │\n\
|
||||
└───────┘\n";
|
||||
|
||||
assert_eq!(expected, config.format(input));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_codes_pad_left() {
|
||||
let mut config = AsciiTable::default();
|
||||
config.columns.insert(0, Column {header: String::new(), align: Right, ..Column::default()});
|
||||
let input = vec![
|
||||
vec!["Hello".color(Color::Blue).bg_color(Color::Yellow).bold()],
|
||||
vec!["H".color(Color::Blue).bg_color(Color::Yellow).bold()]
|
||||
];
|
||||
let expected = "┌───────┐\n\
|
||||
│ \u{1b}[38;5;4m\u{1b}[48;5;3;1mHello\u{1b}[0m │\n\
|
||||
│ \u{1b}[38;5;4m\u{1b}[48;5;3;1m H\u{1b}[0m │\n\
|
||||
└───────┘\n";
|
||||
|
||||
assert_eq!(expected, config.format(input));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_codes_trunc() {
|
||||
let mut config = AsciiTable::default();
|
||||
config.columns.insert(0, Column {header: String::new(), max_width: 2, ..Column::default()});
|
||||
let input = vec![
|
||||
vec!["Hello".color(Color::Blue).bg_color(Color::Yellow).bold()],
|
||||
vec!["H".color(Color::Blue).bg_color(Color::Yellow).bold()]
|
||||
];
|
||||
let expected = "┌────┐\n\
|
||||
│ \u{1b}[38;5;4m\u{1b}[48;5;3;1mH+\u{1b}[0m │\n\
|
||||
│ \u{1b}[38;5;4m\u{1b}[48;5;3;1mH \u{1b}[0m │\n\
|
||||
└────┘\n";
|
||||
|
||||
assert_eq!(expected, config.format(input));
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user