#![deny(missing_docs)]
use std::collections::HashMap;
use std::sync::LazyLock;
mod cell;
mod row;
mod style;
use regex::Regex;
use unicode_width::UnicodeWidthStr;
pub use crate::cell::{Alignment, Cell};
pub use crate::row::Row;
pub use crate::style::Style;
#[derive(Debug, Clone, PartialEq)]
pub struct Table {
pub rows: Vec<Row>,
pub style: Style,
pub max_column_width: Option<usize>,
pub max_column_widths: HashMap<usize, usize>,
pub has_separate_rows: bool,
pub has_top_border: bool,
pub has_bottom_border: bool,
}
impl Default for Table {
fn default() -> Self {
Self::new()
}
}
impl Table {
pub fn new() -> Self {
Self {
rows: Vec::default(),
style: Style::rounded(),
max_column_width: None,
max_column_widths: HashMap::default(),
has_separate_rows: true,
has_top_border: true,
has_bottom_border: true,
}
}
pub fn with_row(mut self, row: Row) -> Self {
self.rows.push(row);
self
}
pub fn with_rows(mut self, rows: impl IntoIterator<Item = Row>) -> Self {
self.rows.extend(rows);
self
}
pub fn with_style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn with_max_column_width(mut self, max_column_width: usize) -> Self {
self.max_column_width = Some(max_column_width);
self
}
pub fn with_max_column_width_at_index(mut self, index: usize, max_column_width: usize) -> Self {
self.max_column_widths.insert(index, max_column_width);
self
}
pub fn without_separate_rows(mut self) -> Self {
self.has_separate_rows = false;
self
}
pub fn without_top_border(mut self) -> Self {
self.has_top_border = false;
self
}
pub fn without_bottom_border(mut self) -> Self {
self.has_bottom_border = false;
self
}
fn calculate_column_widths(&self) -> Vec<usize> {
let max_number_of_columns = self
.rows
.iter()
.map(|row| row.cells.iter().map(|cell| cell.column_span).sum::<usize>())
.max()
.unwrap_or(0);
let mut column_widths = vec![0; max_number_of_columns];
for row in self.rows.iter() {
let mut spanned_columns = 0;
for cell in row.cells.iter() {
let cell_whole_width = cell.width();
let subcell_width = (cell_whole_width - cell.column_span + 1) / cell.column_span;
let leftover_width = (cell_whole_width - cell.column_span + 1) % cell.column_span;
for (index, column_width) in column_widths
.iter_mut()
.skip(spanned_columns)
.take(cell.column_span)
.enumerate()
{
*column_width = (*column_width).max(if index == 0 {
subcell_width + leftover_width
} else {
subcell_width
});
}
spanned_columns += cell.column_span;
}
}
for (index, column_width) in column_widths.iter_mut().enumerate() {
if let Some(max_width) = self
.max_column_widths
.get(&index)
.copied()
.or(self.max_column_width)
{
*column_width = (*column_width).min(max_width);
}
}
column_widths
}
fn render_bottom_border(&self, rendered: &mut String, column_widths: &[usize], last_row: &Row) {
let mut spanned_columns = 0;
rendered.push(self.style.bottom_left_corner);
for (index, cell) in last_row.cells.iter().enumerate() {
if index != 0 {
rendered.push(self.style.outer_bottom_horizontal);
}
let width = column_widths
.iter()
.copied()
.skip(spanned_columns)
.take(cell.column_span)
.sum::<usize>()
+ (cell.column_span - 1);
rendered.extend(std::iter::repeat_n(self.style.horizontal, width));
spanned_columns += cell.column_span;
}
rendered.push(self.style.bottom_right_corner);
}
pub fn render(&self) -> String {
let mut rendered = String::new();
let column_widths = self.calculate_column_widths();
let mut last_row = None;
for (index, row) in self.rows.iter().enumerate() {
if last_row.is_some() {
rendered.push('\n');
}
if row.has_top_border
&& ((index == 0 && self.has_top_border) || (index != 0 && self.has_separate_rows))
{
row.render_top_border(&mut rendered, &column_widths, &self.style, last_row);
rendered.push('\n');
}
row.render_content(&mut rendered, &column_widths, &self.style);
last_row = Some(row);
}
if self.has_bottom_border {
rendered.push('\n');
self.render_bottom_border(&mut rendered, &column_widths, self.rows.last().unwrap());
}
rendered
}
}
pub(crate) static ANSI_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"[\x1b\x9b][\[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PRZcf-nqry=><]").unwrap()
});
pub(crate) fn visible_width(s: &str) -> usize {
ANSI_REGEX
.replace_all(s, "")
.lines()
.map(|line| line.width())
.max()
.unwrap_or(0)
}