1use hwpforge_foundation::{Color, HwpUnit};
39use schemars::JsonSchema;
40use serde::{Deserialize, Serialize};
41
42use crate::caption::Caption;
43use crate::paragraph::Paragraph;
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
47#[serde(rename_all = "snake_case")]
48pub enum TablePageBreak {
49 #[default]
51 Cell,
52 Table,
54 None,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
60#[serde(rename_all = "snake_case")]
61pub enum TableVerticalAlign {
62 Top,
64 #[default]
66 Center,
67 Bottom,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
73pub struct TableMargin {
74 pub left: HwpUnit,
76 pub right: HwpUnit,
78 pub top: HwpUnit,
80 pub bottom: HwpUnit,
82}
83
84fn default_repeat_header() -> bool {
85 true
86}
87
88#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
110#[non_exhaustive]
111pub struct Table {
112 pub rows: Vec<TableRow>,
114 pub width: Option<HwpUnit>,
116 pub caption: Option<Caption>,
118 #[serde(default)]
120 pub page_break: TablePageBreak,
121 #[serde(default = "default_repeat_header")]
123 pub repeat_header: bool,
124 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub cell_spacing: Option<HwpUnit>,
127 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub border_fill_id: Option<u32>,
130}
131
132impl Table {
133 #[must_use]
144 pub fn new(rows: Vec<TableRow>) -> Self {
145 Self {
146 rows,
147 width: None,
148 caption: None,
149 page_break: TablePageBreak::Cell,
150 repeat_header: true,
151 cell_spacing: None,
152 border_fill_id: None,
153 }
154 }
155
156 #[must_use]
158 pub fn with_width(mut self, width: HwpUnit) -> Self {
159 self.width = Some(width);
160 self
161 }
162
163 #[must_use]
165 pub fn with_caption(mut self, caption: Caption) -> Self {
166 self.caption = Some(caption);
167 self
168 }
169
170 #[must_use]
172 pub fn with_page_break(mut self, page_break: TablePageBreak) -> Self {
173 self.page_break = page_break;
174 self
175 }
176
177 #[must_use]
179 pub fn with_repeat_header(mut self, repeat_header: bool) -> Self {
180 self.repeat_header = repeat_header;
181 self
182 }
183
184 #[must_use]
186 pub fn with_cell_spacing(mut self, cell_spacing: HwpUnit) -> Self {
187 self.cell_spacing = Some(cell_spacing);
188 self
189 }
190
191 #[must_use]
193 pub fn with_border_fill_id(mut self, border_fill_id: u32) -> Self {
194 self.border_fill_id = Some(border_fill_id);
195 self
196 }
197
198 pub fn row_count(&self) -> usize {
200 self.rows.len()
201 }
202
203 pub fn col_count(&self) -> usize {
207 self.rows.first().map_or(0, |r| r.cells.len())
208 }
209
210 pub fn is_empty(&self) -> bool {
212 self.rows.is_empty()
213 }
214}
215
216impl std::fmt::Display for Table {
217 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218 write!(f, "Table({}x{})", self.row_count(), self.col_count())
219 }
220}
221
222#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
238#[non_exhaustive]
239pub struct TableRow {
240 pub cells: Vec<TableCell>,
242 pub height: Option<HwpUnit>,
244 #[serde(default)]
246 pub is_header: bool,
247}
248
249impl TableRow {
250 #[must_use]
267 pub fn new(cells: Vec<TableCell>) -> Self {
268 Self { cells, height: None, is_header: false }
269 }
270
271 #[must_use]
288 pub fn with_height(cells: Vec<TableCell>, height: HwpUnit) -> Self {
289 Self { cells, height: Some(height), is_header: false }
290 }
291
292 #[must_use]
294 pub fn with_header(mut self, is_header: bool) -> Self {
295 self.is_header = is_header;
296 self
297 }
298}
299
300#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
320#[non_exhaustive]
321pub struct TableCell {
322 pub paragraphs: Vec<Paragraph>,
324 pub col_span: u16,
326 pub row_span: u16,
328 pub width: HwpUnit,
330 #[serde(default, skip_serializing_if = "Option::is_none")]
332 pub height: Option<HwpUnit>,
333 pub background: Option<Color>,
335 #[serde(default, skip_serializing_if = "Option::is_none")]
337 pub border_fill_id: Option<u32>,
338 #[serde(default, skip_serializing_if = "Option::is_none")]
340 pub margin: Option<TableMargin>,
341 #[serde(default, skip_serializing_if = "Option::is_none")]
343 pub vertical_align: Option<TableVerticalAlign>,
344}
345
346impl TableCell {
347 #[must_use]
365 pub fn new(paragraphs: Vec<Paragraph>, width: HwpUnit) -> Self {
366 Self {
367 paragraphs,
368 col_span: 1,
369 row_span: 1,
370 width,
371 height: None,
372 background: None,
373 border_fill_id: None,
374 margin: None,
375 vertical_align: None,
376 }
377 }
378
379 #[must_use]
398 pub fn with_span(
399 paragraphs: Vec<Paragraph>,
400 width: HwpUnit,
401 col_span: u16,
402 row_span: u16,
403 ) -> Self {
404 Self {
405 paragraphs,
406 col_span,
407 row_span,
408 width,
409 height: None,
410 background: None,
411 border_fill_id: None,
412 margin: None,
413 vertical_align: None,
414 }
415 }
416
417 #[must_use]
419 pub fn with_height(mut self, height: HwpUnit) -> Self {
420 self.height = Some(height);
421 self
422 }
423
424 #[must_use]
426 pub fn with_background(mut self, background: Color) -> Self {
427 self.background = Some(background);
428 self
429 }
430
431 #[must_use]
433 pub fn with_border_fill_id(mut self, border_fill_id: u32) -> Self {
434 self.border_fill_id = Some(border_fill_id);
435 self
436 }
437
438 #[must_use]
440 pub fn with_margin(mut self, margin: TableMargin) -> Self {
441 self.margin = Some(margin);
442 self
443 }
444
445 #[must_use]
447 pub fn with_vertical_align(mut self, vertical_align: TableVerticalAlign) -> Self {
448 self.vertical_align = Some(vertical_align);
449 self
450 }
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456 use crate::run::Run;
457 use hwpforge_foundation::{CharShapeIndex, ParaShapeIndex};
458
459 fn simple_paragraph() -> Paragraph {
460 Paragraph::with_runs(
461 vec![Run::text("cell", CharShapeIndex::new(0))],
462 ParaShapeIndex::new(0),
463 )
464 }
465
466 fn simple_cell() -> TableCell {
467 TableCell::new(vec![simple_paragraph()], HwpUnit::from_mm(50.0).unwrap())
468 }
469
470 fn simple_row() -> TableRow {
471 TableRow::new(vec![simple_cell(), simple_cell()])
472 }
473
474 fn simple_table() -> Table {
475 Table::new(vec![simple_row(), simple_row()])
476 }
477
478 #[test]
479 fn table_new() {
480 let t = simple_table();
481 assert_eq!(t.row_count(), 2);
482 assert_eq!(t.col_count(), 2);
483 assert!(!t.is_empty());
484 assert!(t.width.is_none());
485 assert!(t.caption.is_none());
486 assert_eq!(t.page_break, TablePageBreak::Cell);
487 assert!(t.repeat_header);
488 assert!(t.cell_spacing.is_none());
489 assert!(t.border_fill_id.is_none());
490 }
491
492 #[test]
493 fn empty_table() {
494 let t = Table::new(vec![]);
495 assert_eq!(t.row_count(), 0);
496 assert_eq!(t.col_count(), 0);
497 assert!(t.is_empty());
498 }
499
500 #[test]
501 fn table_with_caption() {
502 let t = simple_table().with_caption(crate::caption::Caption::default());
503 assert!(t.caption.is_some());
504 }
505
506 #[test]
507 fn table_with_width() {
508 let t = simple_table().with_width(HwpUnit::from_mm(150.0).unwrap());
509 assert!(t.width.is_some());
510 }
511
512 #[test]
513 fn table_with_page_break() {
514 let t = simple_table().with_page_break(TablePageBreak::Table);
515 assert_eq!(t.page_break, TablePageBreak::Table);
516 }
517
518 #[test]
519 fn table_with_repeat_header_disabled() {
520 let t = simple_table().with_repeat_header(false);
521 assert!(!t.repeat_header);
522 }
523
524 #[test]
525 fn cell_new_defaults() {
526 let cell = simple_cell();
527 assert_eq!(cell.col_span, 1);
528 assert_eq!(cell.row_span, 1);
529 assert!(cell.height.is_none());
530 assert!(cell.background.is_none());
531 assert!(cell.border_fill_id.is_none());
532 assert!(cell.margin.is_none());
533 assert!(cell.vertical_align.is_none());
534 assert_eq!(cell.paragraphs.len(), 1);
535 }
536
537 #[test]
538 fn cell_with_span() {
539 let cell =
540 TableCell::with_span(vec![simple_paragraph()], HwpUnit::from_mm(100.0).unwrap(), 3, 2);
541 assert_eq!(cell.col_span, 3);
542 assert_eq!(cell.row_span, 2);
543 }
544
545 #[test]
546 fn cell_with_background() {
547 let cell = simple_cell().with_background(Color::from_rgb(200, 200, 200));
548 assert!(cell.background.is_some());
549 }
550
551 #[test]
552 fn table_display() {
553 let t = simple_table();
554 assert_eq!(t.to_string(), "Table(2x2)");
555 }
556
557 #[test]
558 fn single_cell_table() {
559 let table = Table::new(vec![TableRow::with_height(
560 vec![simple_cell()],
561 HwpUnit::from_mm(10.0).unwrap(),
562 )]);
563 assert_eq!(table.row_count(), 1);
564 assert_eq!(table.col_count(), 1);
565 }
566
567 #[test]
568 fn row_with_fixed_height() {
569 let row = TableRow::with_height(vec![simple_cell()], HwpUnit::from_mm(25.0).unwrap());
570 assert!(row.height.is_some());
571 }
572
573 #[test]
574 fn row_new_auto_height() {
575 let row = TableRow::new(vec![simple_cell(), simple_cell()]);
576 assert_eq!(row.cells.len(), 2);
577 assert!(row.height.is_none());
578 }
579
580 #[test]
581 fn row_new_empty_cells() {
582 let row = TableRow::new(vec![]);
583 assert!(row.cells.is_empty());
584 assert!(row.height.is_none());
585 }
586
587 #[test]
588 fn row_with_height_constructor() {
589 let h = HwpUnit::from_mm(20.0).unwrap();
590 let row = TableRow::with_height(vec![simple_cell()], h);
591 assert_eq!(row.cells.len(), 1);
592 assert_eq!(row.height, Some(h));
593 }
594
595 #[test]
596 fn equality() {
597 let a = simple_table();
598 let b = simple_table();
599 assert_eq!(a, b);
600 }
601
602 #[test]
603 fn clone_independence() {
604 let t = simple_table();
605 let mut cloned = t.clone();
606 cloned.caption = Some(crate::caption::Caption::default());
607 assert!(t.caption.is_none());
608 }
609
610 #[test]
611 fn serde_roundtrip() {
612 let t = simple_table();
613 let json = serde_json::to_string(&t).unwrap();
614 let back: Table = serde_json::from_str(&json).unwrap();
615 assert_eq!(t, back);
616 }
617
618 #[test]
619 fn serde_with_all_optional_fields() {
620 let mut t = simple_table()
621 .with_width(HwpUnit::from_mm(150.0).unwrap())
622 .with_caption(crate::caption::Caption::default())
623 .with_page_break(TablePageBreak::None)
624 .with_repeat_header(false)
625 .with_cell_spacing(HwpUnit::from_mm(2.0).unwrap())
626 .with_border_fill_id(7);
627 t.rows[0].height = Some(HwpUnit::from_mm(20.0).unwrap());
628 t.rows[0].cells[0] = t.rows[0].cells[0]
629 .clone()
630 .with_background(Color::from_rgb(255, 0, 0))
631 .with_height(HwpUnit::from_mm(8.0).unwrap())
632 .with_border_fill_id(9)
633 .with_margin(TableMargin {
634 left: HwpUnit::from_mm(1.0).unwrap(),
635 right: HwpUnit::from_mm(2.0).unwrap(),
636 top: HwpUnit::from_mm(0.5).unwrap(),
637 bottom: HwpUnit::from_mm(0.25).unwrap(),
638 })
639 .with_vertical_align(TableVerticalAlign::Bottom);
640
641 let json = serde_json::to_string(&t).unwrap();
642 let back: Table = serde_json::from_str(&json).unwrap();
643 assert_eq!(t, back);
644 }
645
646 #[test]
647 fn serde_defaults_missing_new_fields() {
648 let json = r#"{"rows":[],"width":null,"caption":null}"#;
649 let back: Table = serde_json::from_str(json).unwrap();
650 assert_eq!(back.page_break, TablePageBreak::Cell);
651 assert!(back.repeat_header);
652 assert!(back.cell_spacing.is_none());
653 assert!(back.border_fill_id.is_none());
654 }
655
656 #[test]
657 fn table_margin_defaults_to_zero() {
658 let margin = TableMargin::default();
659 assert_eq!(margin.left, HwpUnit::ZERO);
660 assert_eq!(margin.right, HwpUnit::ZERO);
661 assert_eq!(margin.top, HwpUnit::ZERO);
662 assert_eq!(margin.bottom, HwpUnit::ZERO);
663 }
664
665 #[test]
666 fn cell_zero_span_allowed_at_construction() {
667 let cell = TableCell::with_span(
669 vec![simple_paragraph()],
670 HwpUnit::from_mm(50.0).unwrap(),
671 0, 0,
673 );
674 assert_eq!(cell.col_span, 0);
675 assert_eq!(cell.row_span, 0);
676 }
677
678 #[test]
679 fn row_new_sets_expected_defaults() {
680 let cells = vec![simple_cell()];
681 let row = TableRow::new(cells.clone());
682 assert_eq!(row.cells, cells);
683 assert!(row.height.is_none());
684 assert!(!row.is_header);
685 }
686}