use super::Paragraph;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CellAlignment {
#[default]
Left,
Center,
Right,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum VerticalAlignment {
#[default]
Top,
Middle,
Bottom,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Cell {
#[serde(default)]
pub content: Vec<Paragraph>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub nested_tables: Vec<Table>,
#[serde(default = "default_span", skip_serializing_if = "is_default_span")]
pub col_span: u32,
#[serde(default = "default_span", skip_serializing_if = "is_default_span")]
pub row_span: u32,
#[serde(default, skip_serializing_if = "is_default_cell_alignment")]
pub alignment: CellAlignment,
#[serde(default, skip_serializing_if = "is_default_vertical_alignment")]
pub vertical_alignment: VerticalAlignment,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub is_header: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub background: Option<String>,
}
fn default_span() -> u32 {
1
}
fn is_default_span(n: &u32) -> bool {
*n == 1
}
fn is_default_cell_alignment(a: &CellAlignment) -> bool {
*a == CellAlignment::Left
}
fn is_default_vertical_alignment(a: &VerticalAlignment) -> bool {
*a == VerticalAlignment::Top
}
impl Cell {
pub fn new() -> Self {
Self {
col_span: 1,
row_span: 1,
..Default::default()
}
}
pub fn with_text(text: impl Into<String>) -> Self {
Self {
content: vec![Paragraph::with_text(text)],
col_span: 1,
row_span: 1,
..Default::default()
}
}
pub fn header(text: impl Into<String>) -> Self {
Self {
content: vec![Paragraph::with_text(text)],
col_span: 1,
row_span: 1,
is_header: true,
..Default::default()
}
}
pub fn plain_text(&self) -> String {
self.content
.iter()
.map(|p| p.plain_text())
.collect::<Vec<_>>()
.join("\n")
}
pub fn is_empty(&self) -> bool {
self.content.is_empty() || self.content.iter().all(|p| p.is_empty())
}
pub fn has_col_span(&self) -> bool {
self.col_span > 1
}
pub fn has_row_span(&self) -> bool {
self.row_span > 1
}
pub fn has_spans(&self) -> bool {
self.col_span > 1 || self.row_span > 1
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Row {
#[serde(default)]
pub cells: Vec<Cell>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub is_header: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub height: Option<u32>,
}
impl Row {
pub fn new() -> Self {
Self::default()
}
pub fn header(cells: Vec<Cell>) -> Self {
Self {
cells,
is_header: true,
height: None,
}
}
pub fn add_cell(&mut self, cell: Cell) {
self.cells.push(cell);
}
pub fn len(&self) -> usize {
self.cells.len()
}
pub fn is_empty(&self) -> bool {
self.cells.is_empty()
}
pub fn effective_columns(&self) -> usize {
self.cells.iter().map(|c| c.col_span as usize).sum()
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Table {
#[serde(default)]
pub rows: Vec<Row>,
#[serde(skip_serializing_if = "Option::is_none")]
pub column_widths: Option<Vec<u32>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub caption: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub style_id: Option<String>,
}
impl Table {
pub fn new() -> Self {
Self::default()
}
pub fn add_row(&mut self, row: Row) {
self.rows.push(row);
}
pub fn row_count(&self) -> usize {
self.rows.len()
}
pub fn column_count(&self) -> usize {
self.rows
.iter()
.map(|r| r.effective_columns())
.max()
.unwrap_or(0)
}
pub fn is_empty(&self) -> bool {
self.rows.is_empty()
}
pub fn has_merged_cells(&self) -> bool {
self.rows
.iter()
.any(|r| r.cells.iter().any(|c| c.has_spans()))
}
pub fn header_rows(&self) -> Vec<&Row> {
self.rows.iter().filter(|r| r.is_header).collect()
}
pub fn data_rows(&self) -> Vec<&Row> {
self.rows.iter().filter(|r| !r.is_header).collect()
}
pub fn plain_text(&self) -> String {
let mut text = String::new();
for row in &self.rows {
let cells: Vec<String> = row.cells.iter().map(|c| c.plain_text()).collect();
text.push_str(&cells.join("\t"));
text.push('\n');
}
text
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cell_creation() {
let cell = Cell::with_text("Hello");
assert_eq!(cell.plain_text(), "Hello");
assert!(!cell.is_empty());
assert!(!cell.has_spans());
}
#[test]
fn test_cell_spans() {
let mut cell = Cell::with_text("Merged");
cell.col_span = 2;
cell.row_span = 3;
assert!(cell.has_col_span());
assert!(cell.has_row_span());
assert!(cell.has_spans());
}
#[test]
fn test_row_creation() {
let mut row = Row::new();
row.add_cell(Cell::with_text("A"));
row.add_cell(Cell::with_text("B"));
assert_eq!(row.len(), 2);
assert_eq!(row.effective_columns(), 2);
}
#[test]
fn test_row_with_spans() {
let mut row = Row::new();
let mut cell = Cell::with_text("Merged");
cell.col_span = 2;
row.add_cell(cell);
row.add_cell(Cell::with_text("Single"));
assert_eq!(row.len(), 2);
assert_eq!(row.effective_columns(), 3);
}
#[test]
fn test_table_creation() {
let mut table = Table::new();
let mut header = Row::new();
header.add_cell(Cell::header("Name"));
header.add_cell(Cell::header("Value"));
header.is_header = true;
table.add_row(header);
let mut row = Row::new();
row.add_cell(Cell::with_text("foo"));
row.add_cell(Cell::with_text("bar"));
table.add_row(row);
assert_eq!(table.row_count(), 2);
assert_eq!(table.column_count(), 2);
assert_eq!(table.header_rows().len(), 1);
assert_eq!(table.data_rows().len(), 1);
}
#[test]
fn test_table_has_merged_cells() {
let mut table = Table::new();
let mut row = Row::new();
row.add_cell(Cell::with_text("Normal"));
table.add_row(row);
assert!(!table.has_merged_cells());
let mut row2 = Row::new();
let mut merged = Cell::with_text("Merged");
merged.col_span = 2;
row2.add_cell(merged);
table.add_row(row2);
assert!(table.has_merged_cells());
}
#[test]
fn test_table_plain_text() {
let mut table = Table::new();
let mut row = Row::new();
row.add_cell(Cell::with_text("A1"));
row.add_cell(Cell::with_text("B1"));
table.add_row(row);
let text = table.plain_text();
assert!(text.contains("A1"));
assert!(text.contains("B1"));
}
}