use std::fmt;
use termimad::crossterm::style::Color::*;
use termimad::*;
const SMALL_TERMINAL_THRESHOLD: u16 = 100;
#[derive(Clone, Copy)]
pub enum MarkdownRowAlignment {
Left,
Center,
}
#[derive(Clone, Copy)]
pub enum Padding {
Auto,
NoPad,
}
pub struct TableCell {
text: String,
secondary_text: Option<String>,
padding: Padding,
is_header: bool,
}
impl TableCell {
pub fn header(text: &str, secondary_text: Option<String>, padding: Padding) -> Self {
Self {
text: text.into(),
secondary_text,
padding,
is_header: true,
}
}
pub fn body(text: &str, secondary_text: Option<String>, padding: Padding) -> Self {
Self {
text: text.into(),
secondary_text,
padding,
is_header: false,
}
}
}
impl fmt::Display for TableCell {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut content = if self.is_header {
format!("**{}**", self.text)
} else {
self.text.clone()
};
if let Some(ref sec) = self.secondary_text {
content.push_str(&format!(" *{sec}*"));
}
let (terminal_width, _) = terminal_size();
let should_pad = match self.padding {
Padding::NoPad => false,
Padding::Auto => terminal_width > SMALL_TERMINAL_THRESHOLD,
};
if should_pad {
write!(f, "\u{2800}{content}\u{2800}")
} else {
write!(f, "{content}")
}
}
}
pub fn get_row_alignment(is_compact: bool) -> MarkdownRowAlignment {
if is_compact {
MarkdownRowAlignment::Left
} else {
MarkdownRowAlignment::Center
}
}
pub struct Table {
markdown_skin: MadSkin,
content: String,
row_separator: String,
}
impl Table {
pub fn new(num_cols: usize, alignment: MarkdownRowAlignment) -> Self {
let mut markdown_skin = MadSkin::default();
markdown_skin.bold.set_fg(Cyan);
markdown_skin.italic.set_fg(gray(11));
let row_separator = Table::markdown_fmt_row(num_cols, alignment);
Self {
markdown_skin,
content: String::new(),
row_separator,
}
}
fn markdown_fmt_row(num_cols: usize, alignment: MarkdownRowAlignment) -> String {
let cell = match alignment {
MarkdownRowAlignment::Left => ":--",
MarkdownRowAlignment::Center => ":-:",
};
let row = std::iter::repeat_n(cell, num_cols)
.collect::<Vec<_>>()
.join(" | ");
format!("| {row} |\n")
}
fn build_table_row(row_cells: &[TableCell]) -> String {
format!(
"|{}|\n",
row_cells
.iter()
.map(|c| c.to_string())
.collect::<Vec<_>>()
.join("|")
)
}
pub fn add_header(&mut self, header_cells: Vec<TableCell>) {
self.content.push_str(&self.row_separator);
self.content
.push_str(&Table::build_table_row(&header_cells));
self.content.push_str(&self.row_separator);
}
pub fn add_row(&mut self, row_cells: Vec<TableCell>, include_row_separator: bool) {
self.content.push_str(&Table::build_table_row(&row_cells));
if include_row_separator {
self.content.push_str(&self.row_separator);
}
}
pub fn build(&self) -> String {
format!("{}", self.markdown_skin.term_text(&self.content))
}
}
impl fmt::Display for Table {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", &self.content)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_row_alignment_compact_mode() {
assert!(matches!(
get_row_alignment(true),
MarkdownRowAlignment::Left
));
assert!(matches!(
get_row_alignment(false),
MarkdownRowAlignment::Center
));
}
#[test]
fn test_table_cell_header() {
let cell = TableCell::header("text", None, Padding::NoPad);
let output = format!("{cell}");
assert_eq!(output, "**text**");
let cell = TableCell::header("text", Some("secondary text".to_string()), Padding::NoPad);
let output = format!("{cell}");
assert_eq!(output, "**text** *secondary text*");
}
#[test]
fn test_table_cell_body() {
let cell = TableCell::body("text", None, Padding::NoPad);
let output = format!("{cell}");
assert_eq!(output, "text");
let cell = TableCell::body("text", Some("secondary text".to_string()), Padding::NoPad);
let output = format!("{cell}");
assert_eq!(output, "text *secondary text*");
}
#[test]
fn test_markdown_fmt_row_left_alignment() {
let row = Table::markdown_fmt_row(3, MarkdownRowAlignment::Left);
assert_eq!(row, "| :-- | :-- | :-- |\n");
}
#[test]
fn test_markdown_fmt_row_center_alignment() {
let row = Table::markdown_fmt_row(2, MarkdownRowAlignment::Center);
assert_eq!(row, "| :-: | :-: |\n");
}
#[test]
fn test_table_add_header_and_row() {
let mut table = Table::new(2, MarkdownRowAlignment::Center);
let headers = vec![
TableCell::header("ID", None, Padding::NoPad),
TableCell::header("Value", None, Padding::NoPad),
];
let row = vec![
TableCell::body("1", None, Padding::NoPad),
TableCell::body("foo", None, Padding::NoPad),
];
table.add_header(headers);
table.add_row(row, false);
let output = table.to_string();
assert!(output.contains("**ID**"));
assert!(output.contains("**Value**"));
assert!(output.contains("1"));
assert!(output.contains("foo"));
assert!(output.contains("|"), "Expected table formatting with pipes");
}
}