#[macro_export]
macro_rules! tabby {
($($rest:tt)+) => {{
$crate::_tabby_internal!(@start $($rest)+)
}};
}
pub mod tabled {
pub use ::tabled::*;
}
pub mod paste {
pub use ::paste::*;
}
mod internal {
#[macro_export]
#[doc(hidden)]
macro_rules! _tabby_internal {
(@start $($rest:tt)+) => {{
$crate::_tabby_internal!(@style $($rest)+)
}};
(@style {$($style:ident: $value:ident),+$(,)?} $($rest:tt)+) => {{
$crate::_tabby_internal!(@table {$($style: $value),+}; $($rest)+)
}};
(@style $($rest:tt)+) => {{
$crate::_tabby_internal!(@table {}; $($rest)+)
}};
(@table {$($style:ident: $value:ident),*}; $vec:expr) => {{
use $crate::tabled;
let mut rows = $vec;
let (max_columns, spans) = $crate::_process_rows!(rows);
let mut table = $crate::_table_convert!(max_columns, rows, 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20);
$crate::_apply_table_styling!({$($style: $value),*}, table, spans, max_columns)
}};
(@table {$($style:ident: $value:ident),*}; $([ $($cells:expr),* $(,)? ]),+ $(,)?) => {{
use $crate::tabled;
let mut rows = vec![
$({
vec![$( $cells.to_string() ),*]
}),+
];
let (max_columns, spans) = $crate::_process_rows!(rows);
let mut table = $crate::_table_convert!(max_columns, rows, 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20);
$crate::_apply_table_styling!({$($style: $value),*}, table, spans, max_columns)
}};
}
#[macro_export]
#[doc(hidden)]
macro_rules! _table_convert {
($max_columns:ident, $rows:ident, $($n:literal),+) => {
match $max_columns {
$(
$n => {
let data: Vec<[String; $n]> = $rows.drain(..)
.map(|mut r| {
r.resize($n, String::new());
r.try_into().unwrap_or_else(|v: Vec<String>| {
panic!("Expected vec of length {}, got {}", $n, v.len())
})
})
.collect();
tabled::Table::new(data)
}
)+,
_ => panic!("Table exceeds maximum supported columns (20)")
}
};
}
#[macro_export]
#[doc(hidden)]
macro_rules! _table_style {
(_default) => {
tabled::settings::Style::modern_rounded()
};
($style:ident) => {
tabled::settings::Style::$style()
};
}
#[macro_export]
#[doc(hidden)]
macro_rules! _table_border {
(_default) => {
tabled::settings::Color::FG_BRIGHT_BLACK
};
($color:ident) => {
$crate::paste::paste! {
tabled::settings::Color::[<FG_ $color:upper>]
}
};
}
#[macro_export]
#[doc(hidden)]
macro_rules! _table_labels {
(_default) => {
tabled::settings::Color::FG_WHITE
};
($color:ident) => {
$crate::paste::paste! {
tabled::settings::Color::[<FG_ $color:upper>]
}
};
}
#[macro_export]
#[doc(hidden)]
macro_rules! _table_header {
(_default) => {
tabled::settings::Color::FG_WHITE
};
($color:ident) => {
$crate::paste::paste! {
tabled::settings::Color::[<FG_ $color:upper>]
}
};
}
#[macro_export]
#[doc(hidden)]
macro_rules! _table_color {
(_default) => {
tabled::settings::Color::FG_WHITE
};
($color:ident) => {
$crate::paste::paste! {
tabled::settings::Color::[<FG_ $color:upper>]
}
};
}
#[macro_export]
#[doc(hidden)]
macro_rules! _table_bold {
(_default) => {
false
};
($bold:ident) => {
$bold
};
}
#[macro_export]
#[doc(hidden)]
macro_rules! _extract_field {
($macro:ident; $field:ident; $($style:ident : $value:ident),* $(,)?) => {
$crate::_extract_field_inner!($macro; $field; $($style : $value),*; _default)
};
($macro:ident; $field:ident;) => {
$crate::$macro!(_default)
};
}
#[macro_export]
#[doc(hidden)]
macro_rules! _extract_field_inner {
($macro:ident; $field:ident; $style:ident : $value:ident $(, $rest_style:ident : $rest_value:ident)*; $default:ident) => {{
macro_rules! _check_field {
($field : $v:ident) => {
$crate::$macro!($v)
};
($other:ident : $v:ident) => {
$crate::_extract_field_inner!($macro; $field; $($rest_style : $rest_value),*; $default)
};
}
_check_field!($style : $value)
}};
($macro:ident; $field:ident; ; $default:ident) => {
$crate::$macro!($default)
};
}
#[macro_export]
#[doc(hidden)]
macro_rules! _apply_table_styling {
({$($style:ident: $value:ident),*}, $table:expr, $spans:expr, $max_columns:expr) => {{
for (row, &width) in $spans.iter().enumerate() {
if width > 0 && width < $max_columns {
$table.modify(
(row, width - 1),
tabled::settings::Span::column(isize::MAX)
);
}
}
$table
.with(tabled::settings::Remove::row(tabled::settings::object::Rows::first()))
.with($crate::_extract_field!(_table_style; style; $($style: $value),*))
.with(tabled::settings::themes::BorderCorrection::span())
.modify(
tabled::settings::object::Columns::new(0..),
tabled::settings::style::BorderColor::filled($crate::_extract_field!(_table_border; border; $($style: $value),*))
)
.modify(
tabled::settings::object::Columns::new(0..),
$crate::_extract_field!(_table_color; color; $($style: $value),*)
)
.modify(
tabled::settings::object::Columns::first(),
$crate::_extract_field!(_table_labels; labels; $($style: $value),*)
)
.modify(
tabled::settings::object::Rows::first(),
if $crate::_extract_field!(_table_bold; bold; $($style: $value),*) {
$crate::_extract_field!(_table_header; header; $($style: $value),*) | tabled::settings::Color::BOLD
} else {
$crate::_extract_field!(_table_header; header; $($style: $value),*)
}
);
$table
}};
}
#[macro_export]
#[doc(hidden)]
macro_rules! _process_rows {
($rows:expr) => {{
let mut max_columns = 0;
let mut spans = Vec::new();
for row in $rows.iter() {
spans.push(row.len());
max_columns = std::cmp::max(max_columns, row.len());
}
(max_columns, spans)
}};
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_literal_syntax() {
let t = tabby![["a", "b", "c"], ["c", "d"], ["e", "f", "g"]];
let output = t.to_string();
assert!(output.contains("a"));
assert!(output.contains("b"));
assert!(output.contains("c"));
assert!(output.contains("d"));
assert!(output.contains("e"));
assert!(output.contains("f"));
assert!(output.contains("g"));
}
#[test]
fn test_vec_syntax() {
let data = vec![
vec!["Header1".to_string(), "Header2".to_string()],
vec!["Row1".to_string()],
vec!["Row2Col1".to_string(), "Row2Col2".to_string()],
];
let t = tabby!(data);
let output = t.to_string();
assert!(output.contains("Header1"));
assert!(output.contains("Header2"));
assert!(output.contains("Row1"));
assert!(output.contains("Row2Col1"));
assert!(output.contains("Row2Col2"));
}
#[test]
fn test_styling_options() {
let t = tabby![
{style: modern_rounded, header: green, labels: yellow, color: blue, border: red, bold: true}
["Header 1", "Header 2", "Header 3"],
["Short", "Row"],
["Full", "Width", "Row"]
];
let output = t.to_string();
assert!(output.contains("\x1b[31m")); assert!(output.contains("\x1b[32m")); assert!(output.contains("\x1b[33m")); assert!(output.contains("\x1b[34m")); assert!(output.contains("Header 1"));
assert!(output.contains("Short"));
}
#[test]
fn test_empty_rows() {
let t = tabby![["Header 1", "Header 2"], [], ["Row 2 Col 1"]];
let output = t.to_string();
assert!(output.contains("Header 1"));
assert!(output.contains("Header 2"));
assert!(output.contains("Row 2 Col 1"));
}
#[test]
fn test_single_column() {
let t = tabby![["Header"], ["Row 1"], ["Row 2"]];
let output = t.to_string();
assert!(output.contains("Header"));
assert!(output.contains("Row 1"));
assert!(output.contains("Row 2"));
}
#[test]
fn test_max_columns() {
let row1: Vec<String> = (0..20).map(|i| format!("Col{}", i)).collect();
let row2: Vec<String> = vec!["Row 1".to_string()];
let t = tabby![vec!(row1, row2)];
let output = t.to_string();
assert!(output.contains("Col0"));
assert!(output.contains("Col19"));
assert!(output.contains("Row 1"));
}
}