Skip to main content

hwpforge_core/table/
mod.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::new(vec![cell]);
34//! let table = Table::new(vec![row]);
35//! assert_eq!(table.row_count(), 1);
36//! ```
37
38pub mod grid;
39
40use hwpforge_foundation::{Color, HwpUnit};
41use schemars::JsonSchema;
42use serde::{Deserialize, Serialize};
43
44use crate::caption::Caption;
45use crate::object_id::ObjectId;
46use crate::paragraph::Paragraph;
47
48/// Page-break policy for a table.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
50#[serde(rename_all = "snake_case")]
51pub enum TablePageBreak {
52    /// Split the table at cell boundaries.
53    #[default]
54    Cell,
55    /// Split the table as a whole unit.
56    Table,
57    /// Do not split the table across pages.
58    None,
59}
60
61/// Vertical alignment for content inside a table cell.
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
63#[serde(rename_all = "snake_case")]
64pub enum TableVerticalAlign {
65    /// Align cell content to the top edge.
66    Top,
67    /// Center cell content vertically.
68    #[default]
69    Center,
70    /// Align cell content to the bottom edge.
71    Bottom,
72}
73
74/// Explicit margins inside a table cell.
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
76pub struct TableMargin {
77    /// Left margin in HWP units.
78    pub left: HwpUnit,
79    /// Right margin in HWP units.
80    pub right: HwpUnit,
81    /// Top margin in HWP units.
82    pub top: HwpUnit,
83    /// Bottom margin in HWP units.
84    pub bottom: HwpUnit,
85}
86
87fn default_repeat_header() -> bool {
88    true
89}
90
91/// A table: a sequence of rows, with optional width and caption.
92///
93/// # Design Decision
94///
95/// No `border: Option<BorderStyle>` in Phase 1. Border styling is a
96/// Blueprint concern (Phase 2). Core tables are purely structural.
97///
98/// # Examples
99///
100/// ```
101/// use hwpforge_core::table::{Table, TableCell, TablePageBreak, TableRow};
102/// use hwpforge_core::paragraph::Paragraph;
103/// use hwpforge_foundation::{HwpUnit, ParaShapeIndex};
104///
105/// let table = Table::new(vec![TableRow::new(vec![TableCell::new(
106///     vec![Paragraph::new(ParaShapeIndex::new(0))],
107///     HwpUnit::from_mm(100.0).unwrap(),
108/// )])])
109/// .with_page_break(TablePageBreak::Cell);
110/// assert_eq!(table.row_count(), 1);
111/// ```
112#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
113#[non_exhaustive]
114pub struct Table {
115    /// Rows of the table.
116    pub rows: Vec<TableRow>,
117    /// Optional explicit table width. `None` means auto-width.
118    pub width: Option<HwpUnit>,
119    /// Optional table caption.
120    pub caption: Option<Caption>,
121    /// Page-break policy for this table.
122    #[serde(default)]
123    pub page_break: TablePageBreak,
124    /// Whether the first row repeats across page breaks.
125    #[serde(default = "default_repeat_header")]
126    pub repeat_header: bool,
127    /// Optional explicit spacing between table cells.
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    pub cell_spacing: Option<HwpUnit>,
130    /// Optional table-level border/fill reference.
131    #[serde(default, skip_serializing_if = "Option::is_none")]
132    pub border_fill_id: Option<u32>,
133    /// Wave 12p Step 2b: instance ID for cross-ref target lookup. HWP5
134    /// 변환 시 Table CtrlHeader trailer 의 instance ID 가 채워지고,
135    /// HWPX encoder 가 `<hp:tbl id="...">` attribute 로 emit. `None`
136    /// 이면 encoder 가 fallback 값 (예: sequential counter) 사용 허용.
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub inst_id: Option<ObjectId>,
139}
140
141impl Table {
142    /// Creates a table from rows.
143    ///
144    /// # Examples
145    ///
146    /// ```
147    /// use hwpforge_core::table::{Table, TableRow};
148    ///
149    /// let table = Table::new(vec![TableRow::new(vec![])]);
150    /// assert_eq!(table.row_count(), 1);
151    /// ```
152    #[must_use]
153    pub fn new(rows: Vec<TableRow>) -> Self {
154        Self {
155            rows,
156            width: None,
157            caption: None,
158            page_break: TablePageBreak::Cell,
159            repeat_header: true,
160            cell_spacing: None,
161            border_fill_id: None,
162            inst_id: None,
163        }
164    }
165
166    /// 표의 모든 셀 문단과 캡션 문단을 재귀 방문한다 (중첩 표 포함).
167    pub(crate) fn walk_paragraphs_mut(
168        &mut self,
169        f: &mut dyn FnMut(&mut crate::paragraph::Paragraph),
170    ) {
171        for row in &mut self.rows {
172            for cell in &mut row.cells {
173                for p in &mut cell.paragraphs {
174                    p.walk_paragraphs_mut(f);
175                }
176            }
177        }
178        if let Some(caption) = &mut self.caption {
179            caption.walk_paragraphs_mut(f);
180        }
181    }
182
183    /// Sets an explicit table width.
184    #[must_use]
185    pub fn with_width(mut self, width: HwpUnit) -> Self {
186        self.width = Some(width);
187        self
188    }
189
190    /// Attaches a table caption.
191    #[must_use]
192    pub fn with_caption(mut self, caption: Caption) -> Self {
193        self.caption = Some(caption);
194        self
195    }
196
197    /// Sets the page-break policy for this table.
198    #[must_use]
199    pub fn with_page_break(mut self, page_break: TablePageBreak) -> Self {
200        self.page_break = page_break;
201        self
202    }
203
204    /// Controls whether the leading header block repeats across page breaks.
205    #[must_use]
206    pub fn with_repeat_header(mut self, repeat_header: bool) -> Self {
207        self.repeat_header = repeat_header;
208        self
209    }
210
211    /// Sets the explicit spacing between cells.
212    #[must_use]
213    pub fn with_cell_spacing(mut self, cell_spacing: HwpUnit) -> Self {
214        self.cell_spacing = Some(cell_spacing);
215        self
216    }
217
218    /// Sets the table-level border/fill reference.
219    #[must_use]
220    pub fn with_border_fill_id(mut self, border_fill_id: u32) -> Self {
221        self.border_fill_id = Some(border_fill_id);
222        self
223    }
224
225    /// Returns the number of rows.
226    pub fn row_count(&self) -> usize {
227        self.rows.len()
228    }
229
230    /// Returns the number of columns (from the first row).
231    ///
232    /// Returns 0 if the table has no rows.
233    pub fn col_count(&self) -> usize {
234        self.rows.first().map_or(0, |r| r.cells.len())
235    }
236
237    /// Returns `true` if the table has no rows.
238    pub fn is_empty(&self) -> bool {
239        self.rows.is_empty()
240    }
241}
242
243impl std::fmt::Display for Table {
244    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245        write!(f, "Table({}x{})", self.row_count(), self.col_count())
246    }
247}
248
249/// A single row of a table.
250///
251/// # Examples
252///
253/// ```
254/// use hwpforge_core::table::{TableRow, TableCell};
255/// use hwpforge_core::paragraph::Paragraph;
256/// use hwpforge_foundation::{HwpUnit, ParaShapeIndex};
257///
258/// let row = TableRow::new(vec![
259///     TableCell::new(vec![Paragraph::new(ParaShapeIndex::new(0))], HwpUnit::from_mm(50.0).unwrap()),
260///     TableCell::new(vec![Paragraph::new(ParaShapeIndex::new(0))], HwpUnit::from_mm(50.0).unwrap()),
261/// ]);
262/// assert_eq!(row.cells.len(), 2);
263/// ```
264#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
265#[non_exhaustive]
266pub struct TableRow {
267    /// Cells in this row.
268    pub cells: Vec<TableCell>,
269    /// Optional fixed row height. `None` means auto-height.
270    pub height: Option<HwpUnit>,
271    /// Whether this row is part of the table's leading header-row block.
272    #[serde(default)]
273    pub is_header: bool,
274}
275
276impl TableRow {
277    /// Creates a new table row with the given cells and auto-calculated height.
278    ///
279    /// # Examples
280    ///
281    /// ```
282    /// use hwpforge_core::table::{TableRow, TableCell};
283    /// use hwpforge_core::paragraph::Paragraph;
284    /// use hwpforge_foundation::{HwpUnit, ParaShapeIndex};
285    ///
286    /// let cell = TableCell::new(
287    ///     vec![Paragraph::new(ParaShapeIndex::new(0))],
288    ///     HwpUnit::from_mm(40.0).unwrap(),
289    /// );
290    /// let row = TableRow::new(vec![cell]);
291    /// assert!(row.height.is_none());
292    /// ```
293    #[must_use]
294    pub fn new(cells: Vec<TableCell>) -> Self {
295        Self { cells, height: None, is_header: false }
296    }
297
298    /// Creates a new table row with an explicit fixed height.
299    ///
300    /// # Examples
301    ///
302    /// ```
303    /// use hwpforge_core::table::{TableRow, TableCell};
304    /// use hwpforge_core::paragraph::Paragraph;
305    /// use hwpforge_foundation::{HwpUnit, ParaShapeIndex};
306    ///
307    /// let cell = TableCell::new(
308    ///     vec![Paragraph::new(ParaShapeIndex::new(0))],
309    ///     HwpUnit::from_mm(40.0).unwrap(),
310    /// );
311    /// let row = TableRow::with_height(vec![cell], HwpUnit::from_mm(20.0).unwrap());
312    /// assert!(row.height.is_some());
313    /// ```
314    #[must_use]
315    pub fn with_height(cells: Vec<TableCell>, height: HwpUnit) -> Self {
316        Self { cells, height: Some(height), is_header: false }
317    }
318
319    /// Marks whether this row belongs to the table's leading header-row block.
320    #[must_use]
321    pub fn with_header(mut self, is_header: bool) -> Self {
322        self.is_header = is_header;
323        self
324    }
325}
326
327/// A single cell within a table row.
328///
329/// Each cell contains its own paragraphs (rich content). Spans
330/// default to 1 (no spanning).
331///
332/// # Examples
333///
334/// ```
335/// use hwpforge_core::table::TableCell;
336/// use hwpforge_core::paragraph::Paragraph;
337/// use hwpforge_foundation::{HwpUnit, ParaShapeIndex};
338///
339/// let cell = TableCell::new(
340///     vec![Paragraph::new(ParaShapeIndex::new(0))],
341///     HwpUnit::from_mm(40.0).unwrap(),
342/// );
343/// assert_eq!(cell.col_span, 1);
344/// assert_eq!(cell.row_span, 1);
345/// ```
346#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
347#[non_exhaustive]
348pub struct TableCell {
349    /// Rich content within the cell.
350    pub paragraphs: Vec<Paragraph>,
351    /// Number of columns this cell spans. Must be >= 1.
352    pub col_span: u16,
353    /// Number of rows this cell spans. Must be >= 1.
354    pub row_span: u16,
355    /// Cell width.
356    pub width: HwpUnit,
357    /// Optional explicit cell height.
358    #[serde(default, skip_serializing_if = "Option::is_none")]
359    pub height: Option<HwpUnit>,
360    /// Optional cell background color.
361    pub background: Option<Color>,
362    /// Optional border/fill reference for this cell.
363    #[serde(default, skip_serializing_if = "Option::is_none")]
364    pub border_fill_id: Option<u32>,
365    /// Optional cell-local margin override.
366    #[serde(default, skip_serializing_if = "Option::is_none")]
367    pub margin: Option<TableMargin>,
368    /// Optional vertical alignment override for the cell content box.
369    #[serde(default, skip_serializing_if = "Option::is_none")]
370    pub vertical_align: Option<TableVerticalAlign>,
371}
372
373impl TableCell {
374    /// Creates a cell with default spans (1x1) and no background.
375    ///
376    /// # Examples
377    ///
378    /// ```
379    /// use hwpforge_core::table::TableCell;
380    /// use hwpforge_core::paragraph::Paragraph;
381    /// use hwpforge_foundation::{HwpUnit, ParaShapeIndex};
382    ///
383    /// let cell = TableCell::new(
384    ///     vec![Paragraph::new(ParaShapeIndex::new(0))],
385    ///     HwpUnit::from_mm(50.0).unwrap(),
386    /// );
387    /// assert_eq!(cell.col_span, 1);
388    /// assert_eq!(cell.row_span, 1);
389    /// assert!(cell.background.is_none());
390    /// ```
391    #[must_use]
392    pub fn new(paragraphs: Vec<Paragraph>, width: HwpUnit) -> Self {
393        Self {
394            paragraphs,
395            col_span: 1,
396            row_span: 1,
397            width,
398            height: None,
399            background: None,
400            border_fill_id: None,
401            margin: None,
402            vertical_align: None,
403        }
404    }
405
406    /// Creates a cell with explicit span values.
407    ///
408    /// # Examples
409    ///
410    /// ```
411    /// use hwpforge_core::table::TableCell;
412    /// use hwpforge_core::paragraph::Paragraph;
413    /// use hwpforge_foundation::{HwpUnit, ParaShapeIndex};
414    ///
415    /// let merged = TableCell::with_span(
416    ///     vec![Paragraph::new(ParaShapeIndex::new(0))],
417    ///     HwpUnit::from_mm(100.0).unwrap(),
418    ///     2, // col_span
419    ///     3, // row_span
420    /// );
421    /// assert_eq!(merged.col_span, 2);
422    /// assert_eq!(merged.row_span, 3);
423    /// ```
424    #[must_use]
425    pub fn with_span(
426        paragraphs: Vec<Paragraph>,
427        width: HwpUnit,
428        col_span: u16,
429        row_span: u16,
430    ) -> Self {
431        Self {
432            paragraphs,
433            col_span,
434            row_span,
435            width,
436            height: None,
437            background: None,
438            border_fill_id: None,
439            margin: None,
440            vertical_align: None,
441        }
442    }
443
444    /// Sets an explicit cell height.
445    #[must_use]
446    pub fn with_height(mut self, height: HwpUnit) -> Self {
447        self.height = Some(height);
448        self
449    }
450
451    /// Sets the cell background color.
452    #[must_use]
453    pub fn with_background(mut self, background: Color) -> Self {
454        self.background = Some(background);
455        self
456    }
457
458    /// Sets the cell border/fill reference.
459    #[must_use]
460    pub fn with_border_fill_id(mut self, border_fill_id: u32) -> Self {
461        self.border_fill_id = Some(border_fill_id);
462        self
463    }
464
465    /// Sets the cell-local margin override.
466    #[must_use]
467    pub fn with_margin(mut self, margin: TableMargin) -> Self {
468        self.margin = Some(margin);
469        self
470    }
471
472    /// Sets the vertical alignment override for the cell content box.
473    #[must_use]
474    pub fn with_vertical_align(mut self, vertical_align: TableVerticalAlign) -> Self {
475        self.vertical_align = Some(vertical_align);
476        self
477    }
478}
479
480#[cfg(test)]
481mod tests {
482    use super::*;
483    use crate::run::Run;
484    use hwpforge_foundation::{CharShapeIndex, ParaShapeIndex};
485
486    fn simple_paragraph() -> Paragraph {
487        Paragraph::with_runs(
488            vec![Run::text("cell", CharShapeIndex::new(0))],
489            ParaShapeIndex::new(0),
490        )
491    }
492
493    fn simple_cell() -> TableCell {
494        TableCell::new(vec![simple_paragraph()], HwpUnit::from_mm(50.0).unwrap())
495    }
496
497    fn simple_row() -> TableRow {
498        TableRow::new(vec![simple_cell(), simple_cell()])
499    }
500
501    fn simple_table() -> Table {
502        Table::new(vec![simple_row(), simple_row()])
503    }
504
505    #[test]
506    fn table_new() {
507        let t = simple_table();
508        assert_eq!(t.row_count(), 2);
509        assert_eq!(t.col_count(), 2);
510        assert!(!t.is_empty());
511        assert!(t.width.is_none());
512        assert!(t.caption.is_none());
513        assert_eq!(t.page_break, TablePageBreak::Cell);
514        assert!(t.repeat_header);
515        assert!(t.cell_spacing.is_none());
516        assert!(t.border_fill_id.is_none());
517    }
518
519    #[test]
520    fn empty_table() {
521        let t = Table::new(vec![]);
522        assert_eq!(t.row_count(), 0);
523        assert_eq!(t.col_count(), 0);
524        assert!(t.is_empty());
525    }
526
527    #[test]
528    fn table_with_caption() {
529        let t = simple_table().with_caption(crate::caption::Caption::default());
530        assert!(t.caption.is_some());
531    }
532
533    #[test]
534    fn table_with_width() {
535        let t = simple_table().with_width(HwpUnit::from_mm(150.0).unwrap());
536        assert!(t.width.is_some());
537    }
538
539    #[test]
540    fn table_with_page_break() {
541        let t = simple_table().with_page_break(TablePageBreak::Table);
542        assert_eq!(t.page_break, TablePageBreak::Table);
543    }
544
545    #[test]
546    fn table_with_repeat_header_disabled() {
547        let t = simple_table().with_repeat_header(false);
548        assert!(!t.repeat_header);
549    }
550
551    #[test]
552    fn cell_new_defaults() {
553        let cell = simple_cell();
554        assert_eq!(cell.col_span, 1);
555        assert_eq!(cell.row_span, 1);
556        assert!(cell.height.is_none());
557        assert!(cell.background.is_none());
558        assert!(cell.border_fill_id.is_none());
559        assert!(cell.margin.is_none());
560        assert!(cell.vertical_align.is_none());
561        assert_eq!(cell.paragraphs.len(), 1);
562    }
563
564    #[test]
565    fn cell_with_span() {
566        let cell =
567            TableCell::with_span(vec![simple_paragraph()], HwpUnit::from_mm(100.0).unwrap(), 3, 2);
568        assert_eq!(cell.col_span, 3);
569        assert_eq!(cell.row_span, 2);
570    }
571
572    #[test]
573    fn cell_with_background() {
574        let cell = simple_cell().with_background(Color::from_rgb(200, 200, 200));
575        assert!(cell.background.is_some());
576    }
577
578    #[test]
579    fn table_display() {
580        let t = simple_table();
581        assert_eq!(t.to_string(), "Table(2x2)");
582    }
583
584    #[test]
585    fn single_cell_table() {
586        let table = Table::new(vec![TableRow::with_height(
587            vec![simple_cell()],
588            HwpUnit::from_mm(10.0).unwrap(),
589        )]);
590        assert_eq!(table.row_count(), 1);
591        assert_eq!(table.col_count(), 1);
592    }
593
594    #[test]
595    fn row_with_fixed_height() {
596        let row = TableRow::with_height(vec![simple_cell()], HwpUnit::from_mm(25.0).unwrap());
597        assert!(row.height.is_some());
598    }
599
600    #[test]
601    fn row_new_auto_height() {
602        let row = TableRow::new(vec![simple_cell(), simple_cell()]);
603        assert_eq!(row.cells.len(), 2);
604        assert!(row.height.is_none());
605    }
606
607    #[test]
608    fn row_new_empty_cells() {
609        let row = TableRow::new(vec![]);
610        assert!(row.cells.is_empty());
611        assert!(row.height.is_none());
612    }
613
614    #[test]
615    fn row_with_height_constructor() {
616        let h = HwpUnit::from_mm(20.0).unwrap();
617        let row = TableRow::with_height(vec![simple_cell()], h);
618        assert_eq!(row.cells.len(), 1);
619        assert_eq!(row.height, Some(h));
620    }
621
622    #[test]
623    fn equality() {
624        let a = simple_table();
625        let b = simple_table();
626        assert_eq!(a, b);
627    }
628
629    #[test]
630    fn clone_independence() {
631        let t = simple_table();
632        let mut cloned = t.clone();
633        cloned.caption = Some(crate::caption::Caption::default());
634        assert!(t.caption.is_none());
635    }
636
637    #[test]
638    fn serde_roundtrip() {
639        let t = simple_table();
640        let json = serde_json::to_string(&t).unwrap();
641        let back: Table = serde_json::from_str(&json).unwrap();
642        assert_eq!(t, back);
643    }
644
645    #[test]
646    fn serde_with_all_optional_fields() {
647        let mut t = simple_table()
648            .with_width(HwpUnit::from_mm(150.0).unwrap())
649            .with_caption(crate::caption::Caption::default())
650            .with_page_break(TablePageBreak::None)
651            .with_repeat_header(false)
652            .with_cell_spacing(HwpUnit::from_mm(2.0).unwrap())
653            .with_border_fill_id(7);
654        t.rows[0].height = Some(HwpUnit::from_mm(20.0).unwrap());
655        t.rows[0].cells[0] = t.rows[0].cells[0]
656            .clone()
657            .with_background(Color::from_rgb(255, 0, 0))
658            .with_height(HwpUnit::from_mm(8.0).unwrap())
659            .with_border_fill_id(9)
660            .with_margin(TableMargin {
661                left: HwpUnit::from_mm(1.0).unwrap(),
662                right: HwpUnit::from_mm(2.0).unwrap(),
663                top: HwpUnit::from_mm(0.5).unwrap(),
664                bottom: HwpUnit::from_mm(0.25).unwrap(),
665            })
666            .with_vertical_align(TableVerticalAlign::Bottom);
667
668        let json = serde_json::to_string(&t).unwrap();
669        let back: Table = serde_json::from_str(&json).unwrap();
670        assert_eq!(t, back);
671    }
672
673    #[test]
674    fn serde_defaults_missing_new_fields() {
675        let json = r#"{"rows":[],"width":null,"caption":null}"#;
676        let back: Table = serde_json::from_str(json).unwrap();
677        assert_eq!(back.page_break, TablePageBreak::Cell);
678        assert!(back.repeat_header);
679        assert!(back.cell_spacing.is_none());
680        assert!(back.border_fill_id.is_none());
681    }
682
683    #[test]
684    fn table_margin_defaults_to_zero() {
685        let margin = TableMargin::default();
686        assert_eq!(margin.left, HwpUnit::ZERO);
687        assert_eq!(margin.right, HwpUnit::ZERO);
688        assert_eq!(margin.top, HwpUnit::ZERO);
689        assert_eq!(margin.bottom, HwpUnit::ZERO);
690    }
691
692    #[test]
693    fn cell_zero_span_allowed_at_construction() {
694        // Zero spans are allowed during construction; validation catches them
695        let cell = TableCell::with_span(
696            vec![simple_paragraph()],
697            HwpUnit::from_mm(50.0).unwrap(),
698            0, // invalid, but construction doesn't prevent it
699            0,
700        );
701        assert_eq!(cell.col_span, 0);
702        assert_eq!(cell.row_span, 0);
703    }
704
705    #[test]
706    fn row_new_sets_expected_defaults() {
707        let cells = vec![simple_cell()];
708        let row = TableRow::new(cells.clone());
709        assert_eq!(row.cells, cells);
710        assert!(row.height.is_none());
711        assert!(!row.is_header);
712    }
713}