hwpforge_core/
table.rs

1//! Table types: [`Table`], [`TableRow`], [`TableCell`].
2//!
3//! Tables in HWP documents are structural containers. Each cell holds
4//! its own paragraphs (rich content, not just text). Cells can span
5//! multiple columns or rows via `col_span` / `row_span`.
6//!
7//! # Validation
8//!
9//! Table validation is performed at the Document level (not by Table
10//! constructors) so that tables can be built incrementally. The
11//! validation rules are:
12//!
13//! - At least 1 row
14//! - Each row has at least 1 cell
15//! - Each cell has at least 1 paragraph
16//! - `col_span >= 1`, `row_span >= 1`
17//!
18//! # Examples
19//!
20//! ```
21//! use hwpforge_core::table::{Table, TableRow, TableCell};
22//! use hwpforge_core::paragraph::Paragraph;
23//! use hwpforge_foundation::{HwpUnit, ParaShapeIndex, CharShapeIndex};
24//! use hwpforge_core::run::Run;
25//!
26//! let cell = TableCell::new(
27//!     vec![Paragraph::with_runs(
28//!         vec![Run::text("Hello", CharShapeIndex::new(0))],
29//!         ParaShapeIndex::new(0),
30//!     )],
31//!     HwpUnit::from_mm(50.0).unwrap(),
32//! );
33//! let row = TableRow { cells: vec![cell], height: None };
34//! let table = Table::new(vec![row]);
35//! assert_eq!(table.row_count(), 1);
36//! ```
37
38use hwpforge_foundation::{Color, HwpUnit};
39use schemars::JsonSchema;
40use serde::{Deserialize, Serialize};
41
42use crate::caption::Caption;
43use crate::paragraph::Paragraph;
44
45/// A table: a sequence of rows, with optional width and caption.
46///
47/// # Design Decision
48///
49/// No `border: Option<BorderStyle>` in Phase 1. Border styling is a
50/// Blueprint concern (Phase 2). Core tables are purely structural.
51///
52/// # Examples
53///
54/// ```
55/// use hwpforge_core::table::{Table, TableRow, TableCell};
56/// use hwpforge_core::paragraph::Paragraph;
57/// use hwpforge_foundation::{HwpUnit, ParaShapeIndex};
58///
59/// let table = Table {
60///     rows: vec![TableRow {
61///         cells: vec![TableCell::new(
62///             vec![Paragraph::new(ParaShapeIndex::new(0))],
63///             HwpUnit::from_mm(100.0).unwrap(),
64///         )],
65///         height: None,
66///     }],
67///     width: None,
68///     caption: None,
69/// };
70/// assert_eq!(table.row_count(), 1);
71/// ```
72#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
73pub struct Table {
74    /// Rows of the table.
75    pub rows: Vec<TableRow>,
76    /// Optional explicit table width. `None` means auto-width.
77    pub width: Option<HwpUnit>,
78    /// Optional table caption.
79    pub caption: Option<Caption>,
80}
81
82impl Table {
83    /// Creates a table from rows.
84    ///
85    /// # Examples
86    ///
87    /// ```
88    /// use hwpforge_core::table::{Table, TableRow};
89    ///
90    /// let table = Table::new(vec![TableRow { cells: vec![], height: None }]);
91    /// assert_eq!(table.row_count(), 1);
92    /// ```
93    pub fn new(rows: Vec<TableRow>) -> Self {
94        Self { rows, width: None, caption: None }
95    }
96
97    /// Returns the number of rows.
98    pub fn row_count(&self) -> usize {
99        self.rows.len()
100    }
101
102    /// Returns the number of columns (from the first row).
103    ///
104    /// Returns 0 if the table has no rows.
105    pub fn col_count(&self) -> usize {
106        self.rows.first().map_or(0, |r| r.cells.len())
107    }
108
109    /// Returns `true` if the table has no rows.
110    pub fn is_empty(&self) -> bool {
111        self.rows.is_empty()
112    }
113}
114
115impl std::fmt::Display for Table {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        write!(f, "Table({}x{})", self.row_count(), self.col_count())
118    }
119}
120
121/// A single row of a table.
122///
123/// # Examples
124///
125/// ```
126/// use hwpforge_core::table::{TableRow, TableCell};
127/// use hwpforge_core::paragraph::Paragraph;
128/// use hwpforge_foundation::{HwpUnit, ParaShapeIndex};
129///
130/// let row = TableRow {
131///     cells: vec![
132///         TableCell::new(vec![Paragraph::new(ParaShapeIndex::new(0))], HwpUnit::from_mm(50.0).unwrap()),
133///         TableCell::new(vec![Paragraph::new(ParaShapeIndex::new(0))], HwpUnit::from_mm(50.0).unwrap()),
134///     ],
135///     height: None,
136/// };
137/// assert_eq!(row.cells.len(), 2);
138/// ```
139#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
140pub struct TableRow {
141    /// Cells in this row.
142    pub cells: Vec<TableCell>,
143    /// Optional fixed row height. `None` means auto-height.
144    pub height: Option<HwpUnit>,
145}
146
147impl TableRow {
148    /// Creates a new table row with the given cells and auto-calculated height.
149    ///
150    /// # Examples
151    ///
152    /// ```
153    /// use hwpforge_core::table::{TableRow, TableCell};
154    /// use hwpforge_core::paragraph::Paragraph;
155    /// use hwpforge_foundation::{HwpUnit, ParaShapeIndex};
156    ///
157    /// let cell = TableCell::new(
158    ///     vec![Paragraph::new(ParaShapeIndex::new(0))],
159    ///     HwpUnit::from_mm(40.0).unwrap(),
160    /// );
161    /// let row = TableRow::new(vec![cell]);
162    /// assert!(row.height.is_none());
163    /// ```
164    pub fn new(cells: Vec<TableCell>) -> Self {
165        Self { cells, height: None }
166    }
167
168    /// Creates a new table row with an explicit fixed height.
169    ///
170    /// # Examples
171    ///
172    /// ```
173    /// use hwpforge_core::table::{TableRow, TableCell};
174    /// use hwpforge_core::paragraph::Paragraph;
175    /// use hwpforge_foundation::{HwpUnit, ParaShapeIndex};
176    ///
177    /// let cell = TableCell::new(
178    ///     vec![Paragraph::new(ParaShapeIndex::new(0))],
179    ///     HwpUnit::from_mm(40.0).unwrap(),
180    /// );
181    /// let row = TableRow::with_height(vec![cell], HwpUnit::from_mm(20.0).unwrap());
182    /// assert!(row.height.is_some());
183    /// ```
184    pub fn with_height(cells: Vec<TableCell>, height: HwpUnit) -> Self {
185        Self { cells, height: Some(height) }
186    }
187}
188
189/// A single cell within a table row.
190///
191/// Each cell contains its own paragraphs (rich content). Spans
192/// default to 1 (no spanning).
193///
194/// # Examples
195///
196/// ```
197/// use hwpforge_core::table::TableCell;
198/// use hwpforge_core::paragraph::Paragraph;
199/// use hwpforge_foundation::{HwpUnit, ParaShapeIndex};
200///
201/// let cell = TableCell::new(
202///     vec![Paragraph::new(ParaShapeIndex::new(0))],
203///     HwpUnit::from_mm(40.0).unwrap(),
204/// );
205/// assert_eq!(cell.col_span, 1);
206/// assert_eq!(cell.row_span, 1);
207/// ```
208#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
209pub struct TableCell {
210    /// Rich content within the cell.
211    pub paragraphs: Vec<Paragraph>,
212    /// Number of columns this cell spans. Must be >= 1.
213    pub col_span: u16,
214    /// Number of rows this cell spans. Must be >= 1.
215    pub row_span: u16,
216    /// Cell width.
217    pub width: HwpUnit,
218    /// Optional cell background color.
219    pub background: Option<Color>,
220}
221
222impl TableCell {
223    /// Creates a cell with default spans (1x1) and no background.
224    ///
225    /// # Examples
226    ///
227    /// ```
228    /// use hwpforge_core::table::TableCell;
229    /// use hwpforge_core::paragraph::Paragraph;
230    /// use hwpforge_foundation::{HwpUnit, ParaShapeIndex};
231    ///
232    /// let cell = TableCell::new(
233    ///     vec![Paragraph::new(ParaShapeIndex::new(0))],
234    ///     HwpUnit::from_mm(50.0).unwrap(),
235    /// );
236    /// assert_eq!(cell.col_span, 1);
237    /// assert_eq!(cell.row_span, 1);
238    /// assert!(cell.background.is_none());
239    /// ```
240    pub fn new(paragraphs: Vec<Paragraph>, width: HwpUnit) -> Self {
241        Self { paragraphs, col_span: 1, row_span: 1, width, background: None }
242    }
243
244    /// Creates a cell with explicit span values.
245    ///
246    /// # Examples
247    ///
248    /// ```
249    /// use hwpforge_core::table::TableCell;
250    /// use hwpforge_core::paragraph::Paragraph;
251    /// use hwpforge_foundation::{HwpUnit, ParaShapeIndex};
252    ///
253    /// let merged = TableCell::with_span(
254    ///     vec![Paragraph::new(ParaShapeIndex::new(0))],
255    ///     HwpUnit::from_mm(100.0).unwrap(),
256    ///     2, // col_span
257    ///     3, // row_span
258    /// );
259    /// assert_eq!(merged.col_span, 2);
260    /// assert_eq!(merged.row_span, 3);
261    /// ```
262    pub fn with_span(
263        paragraphs: Vec<Paragraph>,
264        width: HwpUnit,
265        col_span: u16,
266        row_span: u16,
267    ) -> Self {
268        Self { paragraphs, col_span, row_span, width, background: None }
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275    use crate::run::Run;
276    use hwpforge_foundation::{CharShapeIndex, ParaShapeIndex};
277
278    fn simple_paragraph() -> Paragraph {
279        Paragraph::with_runs(
280            vec![Run::text("cell", CharShapeIndex::new(0))],
281            ParaShapeIndex::new(0),
282        )
283    }
284
285    fn simple_cell() -> TableCell {
286        TableCell::new(vec![simple_paragraph()], HwpUnit::from_mm(50.0).unwrap())
287    }
288
289    fn simple_row() -> TableRow {
290        TableRow { cells: vec![simple_cell(), simple_cell()], height: None }
291    }
292
293    fn simple_table() -> Table {
294        Table::new(vec![simple_row(), simple_row()])
295    }
296
297    #[test]
298    fn table_new() {
299        let t = simple_table();
300        assert_eq!(t.row_count(), 2);
301        assert_eq!(t.col_count(), 2);
302        assert!(!t.is_empty());
303        assert!(t.width.is_none());
304        assert!(t.caption.is_none());
305    }
306
307    #[test]
308    fn empty_table() {
309        let t = Table::new(vec![]);
310        assert_eq!(t.row_count(), 0);
311        assert_eq!(t.col_count(), 0);
312        assert!(t.is_empty());
313    }
314
315    #[test]
316    fn table_with_caption() {
317        let mut t = simple_table();
318        t.caption = Some(crate::caption::Caption::default());
319        assert!(t.caption.is_some());
320    }
321
322    #[test]
323    fn table_with_width() {
324        let mut t = simple_table();
325        t.width = Some(HwpUnit::from_mm(150.0).unwrap());
326        assert!(t.width.is_some());
327    }
328
329    #[test]
330    fn cell_new_defaults() {
331        let cell = simple_cell();
332        assert_eq!(cell.col_span, 1);
333        assert_eq!(cell.row_span, 1);
334        assert!(cell.background.is_none());
335        assert_eq!(cell.paragraphs.len(), 1);
336    }
337
338    #[test]
339    fn cell_with_span() {
340        let cell =
341            TableCell::with_span(vec![simple_paragraph()], HwpUnit::from_mm(100.0).unwrap(), 3, 2);
342        assert_eq!(cell.col_span, 3);
343        assert_eq!(cell.row_span, 2);
344    }
345
346    #[test]
347    fn cell_with_background() {
348        let mut cell = simple_cell();
349        cell.background = Some(Color::from_rgb(200, 200, 200));
350        assert!(cell.background.is_some());
351    }
352
353    #[test]
354    fn table_display() {
355        let t = simple_table();
356        assert_eq!(t.to_string(), "Table(2x2)");
357    }
358
359    #[test]
360    fn single_cell_table() {
361        let table = Table::new(vec![TableRow {
362            cells: vec![simple_cell()],
363            height: Some(HwpUnit::from_mm(10.0).unwrap()),
364        }]);
365        assert_eq!(table.row_count(), 1);
366        assert_eq!(table.col_count(), 1);
367    }
368
369    #[test]
370    fn row_with_fixed_height() {
371        let row =
372            TableRow { cells: vec![simple_cell()], height: Some(HwpUnit::from_mm(25.0).unwrap()) };
373        assert!(row.height.is_some());
374    }
375
376    #[test]
377    fn row_new_auto_height() {
378        let row = TableRow::new(vec![simple_cell(), simple_cell()]);
379        assert_eq!(row.cells.len(), 2);
380        assert!(row.height.is_none());
381    }
382
383    #[test]
384    fn row_new_empty_cells() {
385        let row = TableRow::new(vec![]);
386        assert!(row.cells.is_empty());
387        assert!(row.height.is_none());
388    }
389
390    #[test]
391    fn row_with_height_constructor() {
392        let h = HwpUnit::from_mm(20.0).unwrap();
393        let row = TableRow::with_height(vec![simple_cell()], h);
394        assert_eq!(row.cells.len(), 1);
395        assert_eq!(row.height, Some(h));
396    }
397
398    #[test]
399    fn equality() {
400        let a = simple_table();
401        let b = simple_table();
402        assert_eq!(a, b);
403    }
404
405    #[test]
406    fn clone_independence() {
407        let t = simple_table();
408        let mut cloned = t.clone();
409        cloned.caption = Some(crate::caption::Caption::default());
410        assert!(t.caption.is_none());
411    }
412
413    #[test]
414    fn serde_roundtrip() {
415        let t = simple_table();
416        let json = serde_json::to_string(&t).unwrap();
417        let back: Table = serde_json::from_str(&json).unwrap();
418        assert_eq!(t, back);
419    }
420
421    #[test]
422    fn serde_with_all_optional_fields() {
423        let mut t = simple_table();
424        t.width = Some(HwpUnit::from_mm(150.0).unwrap());
425        t.caption = Some(crate::caption::Caption::default());
426        t.rows[0].height = Some(HwpUnit::from_mm(20.0).unwrap());
427        t.rows[0].cells[0].background = Some(Color::from_rgb(255, 0, 0));
428
429        let json = serde_json::to_string(&t).unwrap();
430        let back: Table = serde_json::from_str(&json).unwrap();
431        assert_eq!(t, back);
432    }
433
434    #[test]
435    fn cell_zero_span_allowed_at_construction() {
436        // Zero spans are allowed during construction; validation catches them
437        let cell = TableCell::with_span(
438            vec![simple_paragraph()],
439            HwpUnit::from_mm(50.0).unwrap(),
440            0, // invalid, but construction doesn't prevent it
441            0,
442        );
443        assert_eq!(cell.col_span, 0);
444        assert_eq!(cell.row_span, 0);
445    }
446
447    #[test]
448    fn row_new_equals_struct_literal() {
449        let cells = vec![simple_cell()];
450        let from_new = TableRow::new(cells.clone());
451        let from_literal = TableRow { cells, height: None };
452        assert_eq!(from_new, from_literal);
453    }
454}