use super::{Alignment, Paragraph};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Table {
pub rows: Vec<TableRow>,
pub header_rows: u8,
pub column_widths: Option<Vec<f32>>,
pub caption: Option<String>,
}
impl Table {
pub fn new() -> Self {
Self {
rows: Vec::new(),
header_rows: 0,
column_widths: None,
caption: None,
}
}
pub fn with_header(header_rows: u8) -> Self {
Self {
header_rows,
..Self::new()
}
}
pub fn add_row(&mut self, row: TableRow) {
self.rows.push(row);
}
pub fn row_count(&self) -> usize {
self.rows.len()
}
pub fn column_count(&self) -> usize {
self.rows.first().map(|r| r.cells.len()).unwrap_or(0)
}
pub fn is_empty(&self) -> bool {
self.rows.is_empty()
}
pub fn header(&self) -> &[TableRow] {
&self.rows[..self.header_rows as usize]
}
pub fn body(&self) -> &[TableRow] {
&self.rows[self.header_rows as usize..]
}
pub fn plain_text(&self) -> String {
self.rows
.iter()
.map(|row| row.plain_text())
.collect::<Vec<_>>()
.join("\n")
}
pub fn has_merged_cells(&self) -> bool {
self.rows
.iter()
.flat_map(|r| &r.cells)
.any(|c| c.rowspan > 1 || c.colspan > 1)
}
}
impl Default for Table {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TableRow {
pub cells: Vec<TableCell>,
pub is_header: bool,
}
impl TableRow {
pub fn new(cells: Vec<TableCell>) -> Self {
Self {
cells,
is_header: false,
}
}
pub fn header(cells: Vec<TableCell>) -> Self {
Self {
cells,
is_header: true,
}
}
pub fn from_strings<S: Into<String>>(values: impl IntoIterator<Item = S>) -> Self {
Self::new(values.into_iter().map(TableCell::text).collect())
}
pub fn plain_text(&self) -> String {
self.cells
.iter()
.map(|c| c.plain_text())
.collect::<Vec<_>>()
.join("\t")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TableCell {
pub content: Vec<Paragraph>,
pub rowspan: u8,
pub colspan: u8,
pub alignment: Alignment,
pub vertical_alignment: VerticalAlignment,
}
impl TableCell {
pub fn text(text: impl Into<String>) -> Self {
Self {
content: vec![Paragraph::with_text(text)],
rowspan: 1,
colspan: 1,
alignment: Alignment::Left,
vertical_alignment: VerticalAlignment::Top,
}
}
pub fn empty() -> Self {
Self {
content: Vec::new(),
rowspan: 1,
colspan: 1,
alignment: Alignment::Left,
vertical_alignment: VerticalAlignment::Top,
}
}
pub fn with_content(content: Vec<Paragraph>) -> Self {
Self {
content,
rowspan: 1,
colspan: 1,
alignment: Alignment::Left,
vertical_alignment: VerticalAlignment::Top,
}
}
pub fn colspan(mut self, span: u8) -> Self {
self.colspan = span;
self
}
pub fn rowspan(mut self, span: u8) -> Self {
self.rowspan = span;
self
}
pub fn align(mut self, alignment: Alignment) -> Self {
self.alignment = alignment;
self
}
pub fn plain_text(&self) -> String {
self.content
.iter()
.map(|p| p.plain_text())
.collect::<Vec<_>>()
.join(" ")
}
pub fn is_empty(&self) -> bool {
self.content.is_empty() || self.plain_text().trim().is_empty()
}
pub fn is_merged(&self) -> bool {
self.rowspan > 1 || self.colspan > 1
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum VerticalAlignment {
#[default]
Top,
Middle,
Bottom,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_table_new() {
let table = Table::new();
assert!(table.is_empty());
assert_eq!(table.row_count(), 0);
assert_eq!(table.column_count(), 0);
}
#[test]
fn test_table_with_data() {
let mut table = Table::with_header(1);
table.add_row(TableRow::header(vec![
TableCell::text("Name"),
TableCell::text("Age"),
]));
table.add_row(TableRow::from_strings(["Alice", "30"]));
table.add_row(TableRow::from_strings(["Bob", "25"]));
assert_eq!(table.row_count(), 3);
assert_eq!(table.column_count(), 2);
assert_eq!(table.header().len(), 1);
assert_eq!(table.body().len(), 2);
}
#[test]
fn test_merged_cells() {
let mut table = Table::new();
table.add_row(TableRow::new(vec![TableCell::text("Merged").colspan(2)]));
assert!(table.has_merged_cells());
}
#[test]
fn test_cell_text() {
let cell = TableCell::text("Hello");
assert_eq!(cell.plain_text(), "Hello");
assert!(!cell.is_empty());
}
}