use serde::{Deserialize, Serialize};
use crate::paragraph::Paragraph;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BorderStyle {
None,
Single,
Double,
Dotted,
Dashed,
Custom(String),
}
impl BorderStyle {
pub(crate) fn from_xml(value: &str) -> Self {
match value {
"nil" | "none" => Self::None,
"single" => Self::Single,
"double" => Self::Double,
"dotted" => Self::Dotted,
"dashed" => Self::Dashed,
other => Self::Custom(other.to_string()),
}
}
pub(crate) fn as_xml_value(&self) -> &str {
match self {
Self::None => "nil",
Self::Single => "single",
Self::Double => "double",
Self::Dotted => "dotted",
Self::Dashed => "dashed",
Self::Custom(value) => value.as_str(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Border {
pub style: BorderStyle,
pub size: Option<u16>,
pub color: Option<String>,
}
impl Border {
pub fn new(style: BorderStyle) -> Self {
Self {
style,
size: None,
color: None,
}
}
pub fn size(mut self, size: u16) -> Self {
self.size = Some(size);
self
}
pub fn color(mut self, color: impl Into<String>) -> Self {
self.color = Some(color.into());
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct TableBorders {
pub top: Option<Border>,
pub bottom: Option<Border>,
pub left: Option<Border>,
pub right: Option<Border>,
pub inside_horizontal: Option<Border>,
pub inside_vertical: Option<Border>,
}
impl TableBorders {
pub fn new() -> Self {
Self::default()
}
pub fn top(mut self, border: Border) -> Self {
self.top = Some(border);
self
}
pub fn bottom(mut self, border: Border) -> Self {
self.bottom = Some(border);
self
}
pub fn left(mut self, border: Border) -> Self {
self.left = Some(border);
self
}
pub fn right(mut self, border: Border) -> Self {
self.right = Some(border);
self
}
pub fn inside_horizontal(mut self, border: Border) -> Self {
self.inside_horizontal = Some(border);
self
}
pub fn inside_vertical(mut self, border: Border) -> Self {
self.inside_vertical = Some(border);
self
}
pub(crate) fn has_serialized_content(&self) -> bool {
self.top.is_some()
|| self.bottom.is_some()
|| self.left.is_some()
|| self.right.is_some()
|| self.inside_horizontal.is_some()
|| self.inside_vertical.is_some()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TableProperties {
pub style_id: Option<String>,
pub width: Option<u32>,
pub borders: Option<TableBorders>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TableCellProperties {
pub width: Option<u32>,
pub grid_span: Option<u32>,
pub borders: Option<TableBorders>,
pub background_color: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TableCell {
paragraphs: Vec<Paragraph>,
properties: TableCellProperties,
}
impl TableCell {
pub fn new() -> Self {
Self::default()
}
pub fn add_paragraph(mut self, paragraph: Paragraph) -> Self {
self.paragraphs.push(paragraph);
self
}
pub fn push_paragraph(&mut self, paragraph: Paragraph) -> &mut Self {
self.paragraphs.push(paragraph);
self
}
pub fn paragraphs(&self) -> std::slice::Iter<'_, Paragraph> {
self.paragraphs.iter()
}
pub fn paragraphs_mut(&mut self) -> std::slice::IterMut<'_, Paragraph> {
self.paragraphs.iter_mut()
}
pub fn width(mut self, width: u32) -> Self {
self.properties.width = Some(width);
self
}
pub fn grid_span(mut self, grid_span: u32) -> Self {
self.properties.grid_span = Some(grid_span);
self
}
pub fn borders(mut self, borders: TableBorders) -> Self {
self.properties.borders = Some(borders);
self
}
pub fn background(mut self, color: impl Into<String>) -> Self {
self.properties.background_color = Some(color.into());
self
}
pub fn properties(&self) -> &TableCellProperties {
&self.properties
}
pub fn properties_mut(&mut self) -> &mut TableCellProperties {
&mut self.properties
}
pub fn text(&self) -> String {
self.paragraphs
.iter()
.map(Paragraph::text)
.collect::<Vec<_>>()
.join("\n")
}
pub(crate) fn from_parts(paragraphs: Vec<Paragraph>, properties: TableCellProperties) -> Self {
Self {
paragraphs,
properties,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TableRowProperties {
pub repeat_as_header: bool,
pub allow_split_across_pages: bool,
}
impl Default for TableRowProperties {
fn default() -> Self {
Self {
repeat_as_header: false,
allow_split_across_pages: true,
}
}
}
impl TableRowProperties {
pub(crate) fn has_serialized_content(&self) -> bool {
self.repeat_as_header || !self.allow_split_across_pages
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TableRow {
cells: Vec<TableCell>,
properties: TableRowProperties,
}
impl TableRow {
pub fn new() -> Self {
Self::default()
}
pub fn add_cell(mut self, cell: TableCell) -> Self {
self.cells.push(cell);
self
}
pub fn push_cell(&mut self, cell: TableCell) -> &mut Self {
self.cells.push(cell);
self
}
pub fn repeat_as_header(mut self) -> Self {
self.properties.repeat_as_header = true;
self
}
pub fn allow_split_across_pages(mut self, allow: bool) -> Self {
self.properties.allow_split_across_pages = allow;
self
}
pub fn cells(&self) -> std::slice::Iter<'_, TableCell> {
self.cells.iter()
}
pub fn cells_mut(&mut self) -> std::slice::IterMut<'_, TableCell> {
self.cells.iter_mut()
}
pub fn properties(&self) -> &TableRowProperties {
&self.properties
}
pub fn properties_mut(&mut self) -> &mut TableRowProperties {
&mut self.properties
}
pub(crate) fn from_parts(cells: Vec<TableCell>, properties: TableRowProperties) -> Self {
Self { cells, properties }
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Table {
rows: Vec<TableRow>,
properties: TableProperties,
}
impl Table {
pub fn new() -> Self {
Self::default()
}
pub fn add_row(mut self, row: TableRow) -> Self {
self.rows.push(row);
self
}
pub fn push_row(&mut self, row: TableRow) -> &mut Self {
self.rows.push(row);
self
}
pub fn width(mut self, width: u32) -> Self {
self.properties.width = Some(width);
self
}
pub fn borders(mut self, borders: TableBorders) -> Self {
self.properties.borders = Some(borders);
self
}
pub fn style(mut self, style_id: impl Into<String>) -> Self {
self.properties.style_id = Some(style_id.into());
self
}
pub fn rows(&self) -> std::slice::Iter<'_, TableRow> {
self.rows.iter()
}
pub fn rows_mut(&mut self) -> std::slice::IterMut<'_, TableRow> {
self.rows.iter_mut()
}
pub fn properties(&self) -> &TableProperties {
&self.properties
}
pub fn properties_mut(&mut self) -> &mut TableProperties {
&mut self.properties
}
pub fn style_id(&self) -> Option<&str> {
self.properties.style_id.as_deref()
}
pub fn text(&self) -> String {
self.rows
.iter()
.map(|row| {
row.cells()
.map(TableCell::text)
.collect::<Vec<_>>()
.join("\t")
})
.collect::<Vec<_>>()
.join("\n")
}
pub(crate) fn from_parts(rows: Vec<TableRow>, properties: TableProperties) -> Self {
Self { rows, properties }
}
}
#[cfg(test)]
mod tests {
use super::{
Border, BorderStyle, Table, TableBorders, TableCell, TableCellProperties, TableProperties,
TableRow, TableRowProperties,
};
use crate::{Paragraph, Run};
#[test]
fn border_style_round_trips_known_values() {
let cases = [
("nil", BorderStyle::None, "nil"),
("none", BorderStyle::None, "nil"),
("single", BorderStyle::Single, "single"),
("double", BorderStyle::Double, "double"),
("dotted", BorderStyle::Dotted, "dotted"),
("dashed", BorderStyle::Dashed, "dashed"),
];
for (xml, expected, roundtrip_xml) in cases {
let parsed = BorderStyle::from_xml(xml);
assert_eq!(parsed, expected);
assert_eq!(parsed.as_xml_value(), roundtrip_xml);
}
}
#[test]
fn border_style_custom_value_is_preserved() {
let parsed = BorderStyle::from_xml("thickThinLargeGap");
assert_eq!(parsed, BorderStyle::Custom("thickThinLargeGap".to_string()));
assert_eq!(parsed.as_xml_value(), "thickThinLargeGap");
}
#[test]
fn border_builder_sets_size_and_color() {
let border = Border::new(BorderStyle::Single).size(16).color("AABBCC");
assert_eq!(border.style, BorderStyle::Single);
assert_eq!(border.size, Some(16));
assert_eq!(border.color.as_deref(), Some("AABBCC"));
}
#[test]
fn table_borders_builder_and_serialization_flag() {
let empty = TableBorders::new();
assert!(!empty.has_serialized_content());
let border = Border::new(BorderStyle::Single).size(8).color("111111");
let filled = TableBorders::new()
.top(border.clone())
.bottom(border.clone())
.left(border.clone())
.right(border.clone())
.inside_horizontal(border.clone())
.inside_vertical(border);
assert!(filled.has_serialized_content());
assert!(filled.top.is_some());
assert!(filled.bottom.is_some());
assert!(filled.left.is_some());
assert!(filled.right.is_some());
assert!(filled.inside_horizontal.is_some());
assert!(filled.inside_vertical.is_some());
}
#[test]
fn table_cell_builder_sets_all_properties_and_text() {
let borders = TableBorders::new().top(Border::new(BorderStyle::Single));
let mut cell = TableCell::new()
.width(1234)
.grid_span(2)
.borders(borders.clone())
.background("DDEEFF")
.add_paragraph(Paragraph::new().add_run(Run::from_text("A")))
.add_paragraph(Paragraph::new().add_run(Run::from_text("B")));
cell.push_paragraph(Paragraph::new().add_run(Run::from_text("C")));
cell.properties_mut().width = Some(5678);
assert_eq!(cell.properties().width, Some(5678));
assert_eq!(cell.properties().grid_span, Some(2));
assert_eq!(cell.properties().borders.as_ref(), Some(&borders));
assert_eq!(
cell.properties().background_color.as_deref(),
Some("DDEEFF")
);
assert_eq!(cell.text(), "A\nB\nC");
}
#[test]
fn table_row_builder_and_cells_mut_allow_changes() {
let mut row = TableRow::new()
.repeat_as_header()
.allow_split_across_pages(false)
.add_cell(TableCell::new().add_paragraph(Paragraph::new().add_run(Run::from_text("L"))))
.add_cell(
TableCell::new().add_paragraph(Paragraph::new().add_run(Run::from_text("R"))),
);
row.push_cell(
TableCell::new().add_paragraph(Paragraph::new().add_run(Run::from_text("X"))),
);
for cell in row.cells_mut() {
if cell.text() == "R" {
cell.push_paragraph(Paragraph::new().add_run(Run::from_text("2")));
}
}
let texts: Vec<_> = row.cells().map(TableCell::text).collect();
assert_eq!(
texts,
vec!["L".to_string(), "R\n2".to_string(), "X".to_string()]
);
assert!(row.properties().repeat_as_header);
assert!(!row.properties().allow_split_across_pages);
}
#[test]
fn table_builder_sets_properties_and_formats_text_grid() {
let borders = TableBorders::new().top(Border::new(BorderStyle::Single));
let mut table = Table::new().width(9360).borders(borders.clone()).add_row(
TableRow::new()
.add_cell(
TableCell::new().add_paragraph(Paragraph::new().add_run(Run::from_text("H1"))),
)
.add_cell(
TableCell::new().add_paragraph(Paragraph::new().add_run(Run::from_text("H2"))),
),
);
table.push_row(
TableRow::new()
.add_cell(
TableCell::new().add_paragraph(Paragraph::new().add_run(Run::from_text("V1"))),
)
.add_cell(
TableCell::new().add_paragraph(Paragraph::new().add_run(Run::from_text("V2"))),
),
);
table.properties_mut().width = Some(9000);
assert_eq!(table.properties().width, Some(9000));
assert_eq!(table.properties().borders.as_ref(), Some(&borders));
assert_eq!(table.text(), "H1\tH2\nV1\tV2");
}
#[test]
fn from_parts_builders_preserve_exact_state() {
let cell_properties = TableCellProperties {
width: Some(1000),
grid_span: Some(3),
borders: Some(TableBorders::new().left(Border::new(BorderStyle::Double))),
background_color: Some("ABCDEF".to_string()),
};
let cell = TableCell::from_parts(
vec![Paragraph::new().add_run(Run::from_text("value"))],
cell_properties.clone(),
);
assert_eq!(cell.properties(), &cell_properties);
let row_properties = TableRowProperties {
repeat_as_header: true,
allow_split_across_pages: false,
};
let row = TableRow::from_parts(vec![cell.clone()], row_properties.clone());
assert_eq!(row.cells().count(), 1);
assert_eq!(row.cells().next(), Some(&cell));
assert_eq!(row.properties(), &row_properties);
let table_properties = TableProperties {
style_id: None,
width: Some(7777),
borders: Some(TableBorders::new().right(Border::new(BorderStyle::Dashed))),
};
let table = Table::from_parts(vec![row], table_properties.clone());
assert_eq!(table.properties(), &table_properties);
assert_eq!(table.rows().count(), 1);
}
}