1use hwpforge_foundation::{Color, HwpUnit};
39use schemars::JsonSchema;
40use serde::{Deserialize, Serialize};
41
42use crate::caption::Caption;
43use crate::object_id::ObjectId;
44use crate::paragraph::Paragraph;
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
48#[serde(rename_all = "snake_case")]
49pub enum TablePageBreak {
50 #[default]
52 Cell,
53 Table,
55 None,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
61#[serde(rename_all = "snake_case")]
62pub enum TableVerticalAlign {
63 Top,
65 #[default]
67 Center,
68 Bottom,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
74pub struct TableMargin {
75 pub left: HwpUnit,
77 pub right: HwpUnit,
79 pub top: HwpUnit,
81 pub bottom: HwpUnit,
83}
84
85fn default_repeat_header() -> bool {
86 true
87}
88
89#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
111#[non_exhaustive]
112pub struct Table {
113 pub rows: Vec<TableRow>,
115 pub width: Option<HwpUnit>,
117 pub caption: Option<Caption>,
119 #[serde(default)]
121 pub page_break: TablePageBreak,
122 #[serde(default = "default_repeat_header")]
124 pub repeat_header: bool,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub cell_spacing: Option<HwpUnit>,
128 #[serde(default, skip_serializing_if = "Option::is_none")]
130 pub border_fill_id: Option<u32>,
131 #[serde(default, skip_serializing_if = "Option::is_none")]
136 pub inst_id: Option<ObjectId>,
137}
138
139impl Table {
140 #[must_use]
151 pub fn new(rows: Vec<TableRow>) -> Self {
152 Self {
153 rows,
154 width: None,
155 caption: None,
156 page_break: TablePageBreak::Cell,
157 repeat_header: true,
158 cell_spacing: None,
159 border_fill_id: None,
160 inst_id: None,
161 }
162 }
163
164 #[must_use]
166 pub fn with_width(mut self, width: HwpUnit) -> Self {
167 self.width = Some(width);
168 self
169 }
170
171 #[must_use]
173 pub fn with_caption(mut self, caption: Caption) -> Self {
174 self.caption = Some(caption);
175 self
176 }
177
178 #[must_use]
180 pub fn with_page_break(mut self, page_break: TablePageBreak) -> Self {
181 self.page_break = page_break;
182 self
183 }
184
185 #[must_use]
187 pub fn with_repeat_header(mut self, repeat_header: bool) -> Self {
188 self.repeat_header = repeat_header;
189 self
190 }
191
192 #[must_use]
194 pub fn with_cell_spacing(mut self, cell_spacing: HwpUnit) -> Self {
195 self.cell_spacing = Some(cell_spacing);
196 self
197 }
198
199 #[must_use]
201 pub fn with_border_fill_id(mut self, border_fill_id: u32) -> Self {
202 self.border_fill_id = Some(border_fill_id);
203 self
204 }
205
206 pub fn row_count(&self) -> usize {
208 self.rows.len()
209 }
210
211 pub fn col_count(&self) -> usize {
215 self.rows.first().map_or(0, |r| r.cells.len())
216 }
217
218 pub fn is_empty(&self) -> bool {
220 self.rows.is_empty()
221 }
222}
223
224impl std::fmt::Display for Table {
225 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226 write!(f, "Table({}x{})", self.row_count(), self.col_count())
227 }
228}
229
230#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
246#[non_exhaustive]
247pub struct TableRow {
248 pub cells: Vec<TableCell>,
250 pub height: Option<HwpUnit>,
252 #[serde(default)]
254 pub is_header: bool,
255}
256
257impl TableRow {
258 #[must_use]
275 pub fn new(cells: Vec<TableCell>) -> Self {
276 Self { cells, height: None, is_header: false }
277 }
278
279 #[must_use]
296 pub fn with_height(cells: Vec<TableCell>, height: HwpUnit) -> Self {
297 Self { cells, height: Some(height), is_header: false }
298 }
299
300 #[must_use]
302 pub fn with_header(mut self, is_header: bool) -> Self {
303 self.is_header = is_header;
304 self
305 }
306}
307
308#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
328#[non_exhaustive]
329pub struct TableCell {
330 pub paragraphs: Vec<Paragraph>,
332 pub col_span: u16,
334 pub row_span: u16,
336 pub width: HwpUnit,
338 #[serde(default, skip_serializing_if = "Option::is_none")]
340 pub height: Option<HwpUnit>,
341 pub background: Option<Color>,
343 #[serde(default, skip_serializing_if = "Option::is_none")]
345 pub border_fill_id: Option<u32>,
346 #[serde(default, skip_serializing_if = "Option::is_none")]
348 pub margin: Option<TableMargin>,
349 #[serde(default, skip_serializing_if = "Option::is_none")]
351 pub vertical_align: Option<TableVerticalAlign>,
352}
353
354impl TableCell {
355 #[must_use]
373 pub fn new(paragraphs: Vec<Paragraph>, width: HwpUnit) -> Self {
374 Self {
375 paragraphs,
376 col_span: 1,
377 row_span: 1,
378 width,
379 height: None,
380 background: None,
381 border_fill_id: None,
382 margin: None,
383 vertical_align: None,
384 }
385 }
386
387 #[must_use]
406 pub fn with_span(
407 paragraphs: Vec<Paragraph>,
408 width: HwpUnit,
409 col_span: u16,
410 row_span: u16,
411 ) -> Self {
412 Self {
413 paragraphs,
414 col_span,
415 row_span,
416 width,
417 height: None,
418 background: None,
419 border_fill_id: None,
420 margin: None,
421 vertical_align: None,
422 }
423 }
424
425 #[must_use]
427 pub fn with_height(mut self, height: HwpUnit) -> Self {
428 self.height = Some(height);
429 self
430 }
431
432 #[must_use]
434 pub fn with_background(mut self, background: Color) -> Self {
435 self.background = Some(background);
436 self
437 }
438
439 #[must_use]
441 pub fn with_border_fill_id(mut self, border_fill_id: u32) -> Self {
442 self.border_fill_id = Some(border_fill_id);
443 self
444 }
445
446 #[must_use]
448 pub fn with_margin(mut self, margin: TableMargin) -> Self {
449 self.margin = Some(margin);
450 self
451 }
452
453 #[must_use]
455 pub fn with_vertical_align(mut self, vertical_align: TableVerticalAlign) -> Self {
456 self.vertical_align = Some(vertical_align);
457 self
458 }
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464 use crate::run::Run;
465 use hwpforge_foundation::{CharShapeIndex, ParaShapeIndex};
466
467 fn simple_paragraph() -> Paragraph {
468 Paragraph::with_runs(
469 vec![Run::text("cell", CharShapeIndex::new(0))],
470 ParaShapeIndex::new(0),
471 )
472 }
473
474 fn simple_cell() -> TableCell {
475 TableCell::new(vec![simple_paragraph()], HwpUnit::from_mm(50.0).unwrap())
476 }
477
478 fn simple_row() -> TableRow {
479 TableRow::new(vec![simple_cell(), simple_cell()])
480 }
481
482 fn simple_table() -> Table {
483 Table::new(vec![simple_row(), simple_row()])
484 }
485
486 #[test]
487 fn table_new() {
488 let t = simple_table();
489 assert_eq!(t.row_count(), 2);
490 assert_eq!(t.col_count(), 2);
491 assert!(!t.is_empty());
492 assert!(t.width.is_none());
493 assert!(t.caption.is_none());
494 assert_eq!(t.page_break, TablePageBreak::Cell);
495 assert!(t.repeat_header);
496 assert!(t.cell_spacing.is_none());
497 assert!(t.border_fill_id.is_none());
498 }
499
500 #[test]
501 fn empty_table() {
502 let t = Table::new(vec![]);
503 assert_eq!(t.row_count(), 0);
504 assert_eq!(t.col_count(), 0);
505 assert!(t.is_empty());
506 }
507
508 #[test]
509 fn table_with_caption() {
510 let t = simple_table().with_caption(crate::caption::Caption::default());
511 assert!(t.caption.is_some());
512 }
513
514 #[test]
515 fn table_with_width() {
516 let t = simple_table().with_width(HwpUnit::from_mm(150.0).unwrap());
517 assert!(t.width.is_some());
518 }
519
520 #[test]
521 fn table_with_page_break() {
522 let t = simple_table().with_page_break(TablePageBreak::Table);
523 assert_eq!(t.page_break, TablePageBreak::Table);
524 }
525
526 #[test]
527 fn table_with_repeat_header_disabled() {
528 let t = simple_table().with_repeat_header(false);
529 assert!(!t.repeat_header);
530 }
531
532 #[test]
533 fn cell_new_defaults() {
534 let cell = simple_cell();
535 assert_eq!(cell.col_span, 1);
536 assert_eq!(cell.row_span, 1);
537 assert!(cell.height.is_none());
538 assert!(cell.background.is_none());
539 assert!(cell.border_fill_id.is_none());
540 assert!(cell.margin.is_none());
541 assert!(cell.vertical_align.is_none());
542 assert_eq!(cell.paragraphs.len(), 1);
543 }
544
545 #[test]
546 fn cell_with_span() {
547 let cell =
548 TableCell::with_span(vec![simple_paragraph()], HwpUnit::from_mm(100.0).unwrap(), 3, 2);
549 assert_eq!(cell.col_span, 3);
550 assert_eq!(cell.row_span, 2);
551 }
552
553 #[test]
554 fn cell_with_background() {
555 let cell = simple_cell().with_background(Color::from_rgb(200, 200, 200));
556 assert!(cell.background.is_some());
557 }
558
559 #[test]
560 fn table_display() {
561 let t = simple_table();
562 assert_eq!(t.to_string(), "Table(2x2)");
563 }
564
565 #[test]
566 fn single_cell_table() {
567 let table = Table::new(vec![TableRow::with_height(
568 vec![simple_cell()],
569 HwpUnit::from_mm(10.0).unwrap(),
570 )]);
571 assert_eq!(table.row_count(), 1);
572 assert_eq!(table.col_count(), 1);
573 }
574
575 #[test]
576 fn row_with_fixed_height() {
577 let row = TableRow::with_height(vec![simple_cell()], HwpUnit::from_mm(25.0).unwrap());
578 assert!(row.height.is_some());
579 }
580
581 #[test]
582 fn row_new_auto_height() {
583 let row = TableRow::new(vec![simple_cell(), simple_cell()]);
584 assert_eq!(row.cells.len(), 2);
585 assert!(row.height.is_none());
586 }
587
588 #[test]
589 fn row_new_empty_cells() {
590 let row = TableRow::new(vec![]);
591 assert!(row.cells.is_empty());
592 assert!(row.height.is_none());
593 }
594
595 #[test]
596 fn row_with_height_constructor() {
597 let h = HwpUnit::from_mm(20.0).unwrap();
598 let row = TableRow::with_height(vec![simple_cell()], h);
599 assert_eq!(row.cells.len(), 1);
600 assert_eq!(row.height, Some(h));
601 }
602
603 #[test]
604 fn equality() {
605 let a = simple_table();
606 let b = simple_table();
607 assert_eq!(a, b);
608 }
609
610 #[test]
611 fn clone_independence() {
612 let t = simple_table();
613 let mut cloned = t.clone();
614 cloned.caption = Some(crate::caption::Caption::default());
615 assert!(t.caption.is_none());
616 }
617
618 #[test]
619 fn serde_roundtrip() {
620 let t = simple_table();
621 let json = serde_json::to_string(&t).unwrap();
622 let back: Table = serde_json::from_str(&json).unwrap();
623 assert_eq!(t, back);
624 }
625
626 #[test]
627 fn serde_with_all_optional_fields() {
628 let mut t = simple_table()
629 .with_width(HwpUnit::from_mm(150.0).unwrap())
630 .with_caption(crate::caption::Caption::default())
631 .with_page_break(TablePageBreak::None)
632 .with_repeat_header(false)
633 .with_cell_spacing(HwpUnit::from_mm(2.0).unwrap())
634 .with_border_fill_id(7);
635 t.rows[0].height = Some(HwpUnit::from_mm(20.0).unwrap());
636 t.rows[0].cells[0] = t.rows[0].cells[0]
637 .clone()
638 .with_background(Color::from_rgb(255, 0, 0))
639 .with_height(HwpUnit::from_mm(8.0).unwrap())
640 .with_border_fill_id(9)
641 .with_margin(TableMargin {
642 left: HwpUnit::from_mm(1.0).unwrap(),
643 right: HwpUnit::from_mm(2.0).unwrap(),
644 top: HwpUnit::from_mm(0.5).unwrap(),
645 bottom: HwpUnit::from_mm(0.25).unwrap(),
646 })
647 .with_vertical_align(TableVerticalAlign::Bottom);
648
649 let json = serde_json::to_string(&t).unwrap();
650 let back: Table = serde_json::from_str(&json).unwrap();
651 assert_eq!(t, back);
652 }
653
654 #[test]
655 fn serde_defaults_missing_new_fields() {
656 let json = r#"{"rows":[],"width":null,"caption":null}"#;
657 let back: Table = serde_json::from_str(json).unwrap();
658 assert_eq!(back.page_break, TablePageBreak::Cell);
659 assert!(back.repeat_header);
660 assert!(back.cell_spacing.is_none());
661 assert!(back.border_fill_id.is_none());
662 }
663
664 #[test]
665 fn table_margin_defaults_to_zero() {
666 let margin = TableMargin::default();
667 assert_eq!(margin.left, HwpUnit::ZERO);
668 assert_eq!(margin.right, HwpUnit::ZERO);
669 assert_eq!(margin.top, HwpUnit::ZERO);
670 assert_eq!(margin.bottom, HwpUnit::ZERO);
671 }
672
673 #[test]
674 fn cell_zero_span_allowed_at_construction() {
675 let cell = TableCell::with_span(
677 vec![simple_paragraph()],
678 HwpUnit::from_mm(50.0).unwrap(),
679 0, 0,
681 );
682 assert_eq!(cell.col_span, 0);
683 assert_eq!(cell.row_span, 0);
684 }
685
686 #[test]
687 fn row_new_sets_expected_defaults() {
688 let cells = vec![simple_cell()];
689 let row = TableRow::new(cells.clone());
690 assert_eq!(row.cells, cells);
691 assert!(row.height.is_none());
692 assert!(!row.is_header);
693 }
694}