use hwpforge_foundation::{Color, HwpUnit};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::caption::Caption;
use crate::paragraph::Paragraph;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case")]
pub enum TablePageBreak {
#[default]
Cell,
Table,
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case")]
pub enum TableVerticalAlign {
Top,
#[default]
Center,
Bottom,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
pub struct TableMargin {
pub left: HwpUnit,
pub right: HwpUnit,
pub top: HwpUnit,
pub bottom: HwpUnit,
}
fn default_repeat_header() -> bool {
true
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[non_exhaustive]
pub struct Table {
pub rows: Vec<TableRow>,
pub width: Option<HwpUnit>,
pub caption: Option<Caption>,
#[serde(default)]
pub page_break: TablePageBreak,
#[serde(default = "default_repeat_header")]
pub repeat_header: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cell_spacing: Option<HwpUnit>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub border_fill_id: Option<u32>,
}
impl Table {
#[must_use]
pub fn new(rows: Vec<TableRow>) -> Self {
Self {
rows,
width: None,
caption: None,
page_break: TablePageBreak::Cell,
repeat_header: true,
cell_spacing: None,
border_fill_id: None,
}
}
#[must_use]
pub fn with_width(mut self, width: HwpUnit) -> Self {
self.width = Some(width);
self
}
#[must_use]
pub fn with_caption(mut self, caption: Caption) -> Self {
self.caption = Some(caption);
self
}
#[must_use]
pub fn with_page_break(mut self, page_break: TablePageBreak) -> Self {
self.page_break = page_break;
self
}
#[must_use]
pub fn with_repeat_header(mut self, repeat_header: bool) -> Self {
self.repeat_header = repeat_header;
self
}
#[must_use]
pub fn with_cell_spacing(mut self, cell_spacing: HwpUnit) -> Self {
self.cell_spacing = Some(cell_spacing);
self
}
#[must_use]
pub fn with_border_fill_id(mut self, border_fill_id: u32) -> Self {
self.border_fill_id = Some(border_fill_id);
self
}
pub fn row_count(&self) -> usize {
self.rows.len()
}
pub fn col_count(&self) -> usize {
self.rows.first().map_or(0, |r| r.cells.len())
}
pub fn is_empty(&self) -> bool {
self.rows.is_empty()
}
}
impl std::fmt::Display for Table {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Table({}x{})", self.row_count(), self.col_count())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[non_exhaustive]
pub struct TableRow {
pub cells: Vec<TableCell>,
pub height: Option<HwpUnit>,
#[serde(default)]
pub is_header: bool,
}
impl TableRow {
#[must_use]
pub fn new(cells: Vec<TableCell>) -> Self {
Self { cells, height: None, is_header: false }
}
#[must_use]
pub fn with_height(cells: Vec<TableCell>, height: HwpUnit) -> Self {
Self { cells, height: Some(height), is_header: false }
}
#[must_use]
pub fn with_header(mut self, is_header: bool) -> Self {
self.is_header = is_header;
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[non_exhaustive]
pub struct TableCell {
pub paragraphs: Vec<Paragraph>,
pub col_span: u16,
pub row_span: u16,
pub width: HwpUnit,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub height: Option<HwpUnit>,
pub background: Option<Color>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub border_fill_id: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub margin: Option<TableMargin>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub vertical_align: Option<TableVerticalAlign>,
}
impl TableCell {
#[must_use]
pub fn new(paragraphs: Vec<Paragraph>, width: HwpUnit) -> Self {
Self {
paragraphs,
col_span: 1,
row_span: 1,
width,
height: None,
background: None,
border_fill_id: None,
margin: None,
vertical_align: None,
}
}
#[must_use]
pub fn with_span(
paragraphs: Vec<Paragraph>,
width: HwpUnit,
col_span: u16,
row_span: u16,
) -> Self {
Self {
paragraphs,
col_span,
row_span,
width,
height: None,
background: None,
border_fill_id: None,
margin: None,
vertical_align: None,
}
}
#[must_use]
pub fn with_height(mut self, height: HwpUnit) -> Self {
self.height = Some(height);
self
}
#[must_use]
pub fn with_background(mut self, background: Color) -> Self {
self.background = Some(background);
self
}
#[must_use]
pub fn with_border_fill_id(mut self, border_fill_id: u32) -> Self {
self.border_fill_id = Some(border_fill_id);
self
}
#[must_use]
pub fn with_margin(mut self, margin: TableMargin) -> Self {
self.margin = Some(margin);
self
}
#[must_use]
pub fn with_vertical_align(mut self, vertical_align: TableVerticalAlign) -> Self {
self.vertical_align = Some(vertical_align);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::run::Run;
use hwpforge_foundation::{CharShapeIndex, ParaShapeIndex};
fn simple_paragraph() -> Paragraph {
Paragraph::with_runs(
vec![Run::text("cell", CharShapeIndex::new(0))],
ParaShapeIndex::new(0),
)
}
fn simple_cell() -> TableCell {
TableCell::new(vec![simple_paragraph()], HwpUnit::from_mm(50.0).unwrap())
}
fn simple_row() -> TableRow {
TableRow::new(vec![simple_cell(), simple_cell()])
}
fn simple_table() -> Table {
Table::new(vec![simple_row(), simple_row()])
}
#[test]
fn table_new() {
let t = simple_table();
assert_eq!(t.row_count(), 2);
assert_eq!(t.col_count(), 2);
assert!(!t.is_empty());
assert!(t.width.is_none());
assert!(t.caption.is_none());
assert_eq!(t.page_break, TablePageBreak::Cell);
assert!(t.repeat_header);
assert!(t.cell_spacing.is_none());
assert!(t.border_fill_id.is_none());
}
#[test]
fn empty_table() {
let t = Table::new(vec![]);
assert_eq!(t.row_count(), 0);
assert_eq!(t.col_count(), 0);
assert!(t.is_empty());
}
#[test]
fn table_with_caption() {
let t = simple_table().with_caption(crate::caption::Caption::default());
assert!(t.caption.is_some());
}
#[test]
fn table_with_width() {
let t = simple_table().with_width(HwpUnit::from_mm(150.0).unwrap());
assert!(t.width.is_some());
}
#[test]
fn table_with_page_break() {
let t = simple_table().with_page_break(TablePageBreak::Table);
assert_eq!(t.page_break, TablePageBreak::Table);
}
#[test]
fn table_with_repeat_header_disabled() {
let t = simple_table().with_repeat_header(false);
assert!(!t.repeat_header);
}
#[test]
fn cell_new_defaults() {
let cell = simple_cell();
assert_eq!(cell.col_span, 1);
assert_eq!(cell.row_span, 1);
assert!(cell.height.is_none());
assert!(cell.background.is_none());
assert!(cell.border_fill_id.is_none());
assert!(cell.margin.is_none());
assert!(cell.vertical_align.is_none());
assert_eq!(cell.paragraphs.len(), 1);
}
#[test]
fn cell_with_span() {
let cell =
TableCell::with_span(vec![simple_paragraph()], HwpUnit::from_mm(100.0).unwrap(), 3, 2);
assert_eq!(cell.col_span, 3);
assert_eq!(cell.row_span, 2);
}
#[test]
fn cell_with_background() {
let cell = simple_cell().with_background(Color::from_rgb(200, 200, 200));
assert!(cell.background.is_some());
}
#[test]
fn table_display() {
let t = simple_table();
assert_eq!(t.to_string(), "Table(2x2)");
}
#[test]
fn single_cell_table() {
let table = Table::new(vec![TableRow::with_height(
vec![simple_cell()],
HwpUnit::from_mm(10.0).unwrap(),
)]);
assert_eq!(table.row_count(), 1);
assert_eq!(table.col_count(), 1);
}
#[test]
fn row_with_fixed_height() {
let row = TableRow::with_height(vec![simple_cell()], HwpUnit::from_mm(25.0).unwrap());
assert!(row.height.is_some());
}
#[test]
fn row_new_auto_height() {
let row = TableRow::new(vec![simple_cell(), simple_cell()]);
assert_eq!(row.cells.len(), 2);
assert!(row.height.is_none());
}
#[test]
fn row_new_empty_cells() {
let row = TableRow::new(vec![]);
assert!(row.cells.is_empty());
assert!(row.height.is_none());
}
#[test]
fn row_with_height_constructor() {
let h = HwpUnit::from_mm(20.0).unwrap();
let row = TableRow::with_height(vec![simple_cell()], h);
assert_eq!(row.cells.len(), 1);
assert_eq!(row.height, Some(h));
}
#[test]
fn equality() {
let a = simple_table();
let b = simple_table();
assert_eq!(a, b);
}
#[test]
fn clone_independence() {
let t = simple_table();
let mut cloned = t.clone();
cloned.caption = Some(crate::caption::Caption::default());
assert!(t.caption.is_none());
}
#[test]
fn serde_roundtrip() {
let t = simple_table();
let json = serde_json::to_string(&t).unwrap();
let back: Table = serde_json::from_str(&json).unwrap();
assert_eq!(t, back);
}
#[test]
fn serde_with_all_optional_fields() {
let mut t = simple_table()
.with_width(HwpUnit::from_mm(150.0).unwrap())
.with_caption(crate::caption::Caption::default())
.with_page_break(TablePageBreak::None)
.with_repeat_header(false)
.with_cell_spacing(HwpUnit::from_mm(2.0).unwrap())
.with_border_fill_id(7);
t.rows[0].height = Some(HwpUnit::from_mm(20.0).unwrap());
t.rows[0].cells[0] = t.rows[0].cells[0]
.clone()
.with_background(Color::from_rgb(255, 0, 0))
.with_height(HwpUnit::from_mm(8.0).unwrap())
.with_border_fill_id(9)
.with_margin(TableMargin {
left: HwpUnit::from_mm(1.0).unwrap(),
right: HwpUnit::from_mm(2.0).unwrap(),
top: HwpUnit::from_mm(0.5).unwrap(),
bottom: HwpUnit::from_mm(0.25).unwrap(),
})
.with_vertical_align(TableVerticalAlign::Bottom);
let json = serde_json::to_string(&t).unwrap();
let back: Table = serde_json::from_str(&json).unwrap();
assert_eq!(t, back);
}
#[test]
fn serde_defaults_missing_new_fields() {
let json = r#"{"rows":[],"width":null,"caption":null}"#;
let back: Table = serde_json::from_str(json).unwrap();
assert_eq!(back.page_break, TablePageBreak::Cell);
assert!(back.repeat_header);
assert!(back.cell_spacing.is_none());
assert!(back.border_fill_id.is_none());
}
#[test]
fn table_margin_defaults_to_zero() {
let margin = TableMargin::default();
assert_eq!(margin.left, HwpUnit::ZERO);
assert_eq!(margin.right, HwpUnit::ZERO);
assert_eq!(margin.top, HwpUnit::ZERO);
assert_eq!(margin.bottom, HwpUnit::ZERO);
}
#[test]
fn cell_zero_span_allowed_at_construction() {
let cell = TableCell::with_span(
vec![simple_paragraph()],
HwpUnit::from_mm(50.0).unwrap(),
0, 0,
);
assert_eq!(cell.col_span, 0);
assert_eq!(cell.row_span, 0);
}
#[test]
fn row_new_sets_expected_defaults() {
let cells = vec![simple_cell()];
let row = TableRow::new(cells.clone());
assert_eq!(row.cells, cells);
assert!(row.height.is_none());
assert!(!row.is_header);
}
}