1use crate::align::{AlignMethod, VerticalAlignMethod};
46use crate::box_drawing::{get_safe_box, BoxStyle, BOX_HEAVY_HEAD};
47use crate::console::{ConsoleOptions, OverflowMethod, RenderResult, Renderable};
48use crate::segment::Segment;
49use crate::style::Style;
50use std::collections::HashSet;
51use unicode_width::UnicodeWidthStr;
52
53#[derive(Debug, Clone)]
59pub struct Cell {
60 pub content: String,
62 pub style: Option<Style>,
64 pub colspan: usize,
66 pub rowspan: usize,
68}
69
70impl Cell {
71 pub fn new(content: impl Into<String>) -> Self {
73 Cell {
74 content: content.into(),
75 style: None,
76 colspan: 1,
77 rowspan: 1,
78 }
79 }
80
81 pub fn style(mut self, s: Style) -> Self { self.style = Some(s); self }
83 pub fn colspan(mut self, c: usize) -> Self { self.colspan = c; self }
85 pub fn rowspan(mut self, r: usize) -> Self { self.rowspan = r; self }
87}
88
89impl From<String> for Cell {
90 fn from(s: String) -> Self { Cell::new(s) }
91}
92
93impl From<&str> for Cell {
94 fn from(s: &str) -> Self { Cell::new(s) }
95}
96
97#[derive(Debug, Clone)]
103pub struct Column {
104 pub header: String,
106 pub footer: String,
108 pub header_style: Style,
110 pub footer_style: Style,
112 pub style: Style,
114 pub justify: AlignMethod,
116 pub vertical: VerticalAlignMethod,
118 pub overflow: OverflowMethod,
120 pub width: Option<usize>,
122 pub min_width: Option<usize>,
124 pub max_width: Option<usize>,
126 pub ratio: Option<usize>,
128 pub colspan: usize,
130}
131
132impl Column {
133 pub fn new(header: impl Into<String>) -> Self {
135 Self {
136 header: header.into(),
137 footer: String::new(),
138 header_style: Style::new().bold(true),
139 footer_style: Style::new(),
140 style: Style::new(),
141 justify: AlignMethod::Left,
142 vertical: VerticalAlignMethod::Top,
143 overflow: OverflowMethod::Ellipsis,
144 width: None,
145 min_width: None,
146 max_width: None,
147 ratio: None,
148 colspan: 1,
149 }
150 }
151
152 pub fn justify(mut self, j: AlignMethod) -> Self { self.justify = j; self }
154 pub fn width(mut self, w: usize) -> Self { self.width = Some(w); self }
156 pub fn min_width(mut self, w: usize) -> Self { self.min_width = Some(w); self }
158 pub fn max_width(mut self, w: usize) -> Self { self.max_width = Some(w); self }
160 pub fn style(mut self, s: Style) -> Self { self.style = s; self }
162 pub fn header_style(mut self, s: Style) -> Self { self.header_style = s; self }
164 pub fn ratio(mut self, r: usize) -> Self { self.ratio = Some(r); self }
166 pub fn overflow(mut self, o: OverflowMethod) -> Self { self.overflow = o; self }
168}
169
170#[derive(Debug, Clone)]
176pub struct Row {
177 pub cells: Vec<Cell>,
178 pub style: Option<Style>,
179 pub end_section: bool,
180}
181
182impl Row {
183 pub fn new(cells: Vec<Cell>) -> Self {
185 Self { cells, style: None, end_section: false }
186 }
187
188 pub fn style(mut self, style: Style) -> Self {
190 self.style = Some(style);
191 self
192 }
193
194 pub fn end_section(mut self, value: bool) -> Self {
197 self.end_section = value;
198 self
199 }
200}
201
202#[derive(Debug, Clone)]
208pub struct Table {
209 columns: Vec<Column>,
210 rows: Vec<Vec<Cell>>,
211 pub title: Option<String>,
213 pub caption: Option<String>,
215 pub box_style: BoxStyle,
217 pub show_header: bool,
219 pub show_footer: bool,
221 pub show_edge: bool,
223 pub show_lines: bool,
225 pub padding: (usize, usize, usize, usize),
227 pub collapse_padding: bool,
229 pub style: Style,
231 pub border_style: Style,
233 pub title_style: Style,
235 pub caption_style: Style,
237 pub title_justify: AlignMethod,
239 pub caption_justify: AlignMethod,
241 pub highlight: bool,
243 pub width: Option<usize>,
245 pub row_styles: Vec<Style>,
247 pub leading: usize,
249 pub rowspans: Vec<usize>,
251 pub section_rows: HashSet<usize>,
253 pub pad_edge: bool,
255 pub sections: Vec<usize>,
257}
258
259impl Table {
260 pub fn new() -> Self {
262 Self {
263 columns: Vec::new(),
264 rows: Vec::new(),
265 title: None,
266 caption: None,
267 box_style: BOX_HEAVY_HEAD.clone(),
268 show_header: true,
269 show_footer: false,
270 show_edge: true,
271 show_lines: false,
272 padding: (0, 1, 0, 1),
273 collapse_padding: false,
274 style: Style::new(),
275 border_style: Style::new(),
276 title_style: Style::new().bold(true),
277 caption_style: Style::new().dim(true),
278 title_justify: AlignMethod::Center,
279 caption_justify: AlignMethod::Center,
280 highlight: false,
281 width: None,
282 row_styles: Vec::new(),
283 leading: 0,
284 rowspans: Vec::new(),
285 section_rows: HashSet::new(),
286 pad_edge: true,
287 sections: Vec::new(),
288 }
289 }
290
291 pub fn add_column(&mut self, column: Column) {
305 self.columns.push(column);
306 }
307
308 pub fn add_row(&mut self, row: Vec<Cell>) {
321 self.rows.push(row);
322 }
323
324 pub fn add_row_explicit(&mut self, row: Row) -> &mut Self {
330 if row.end_section {
331 self.section_rows.insert(self.rows.len());
332 self.sections.push(self.rows.len());
333 }
334 self.rows.push(row.cells);
335 self
336 }
337
338 pub fn add_row_str(&mut self, row: Vec<String>) {
351 let cells: Vec<Cell> = row.into_iter().map(Cell::new).collect();
352 self.rows.push(cells);
353 }
354
355 pub fn column(mut self, col: Column) -> Self { self.add_column(col); self }
357
358 pub fn row(mut self, row: Vec<Cell>) -> Self { self.add_row(row); self }
360
361 pub fn row_str(mut self, row: Vec<String>) -> Self { self.add_row_str(row); self }
363
364 pub fn row_explicit(mut self, row: Row) -> Self { self.add_row_explicit(row); self }
366
367 pub fn title(mut self, t: impl Into<String>) -> Self { self.title = Some(t.into()); self }
369
370 pub fn caption(mut self, t: impl Into<String>) -> Self { self.caption = Some(t.into()); self }
372
373 pub fn box_style(mut self, bs: BoxStyle) -> Self { self.box_style = bs; self }
375
376 pub fn border_style(mut self, s: Style) -> Self { self.border_style = s; self }
378
379 pub fn hide_header(mut self) -> Self { self.show_header = false; self }
381
382 pub fn show_lines(mut self) -> Self { self.show_lines = true; self }
384
385 pub fn leading(mut self, l: usize) -> Self { self.leading = l; self }
387
388 pub fn highlight(mut self, value: bool) -> Self { self.highlight = value; self }
390
391 pub fn title_justify(mut self, justify: AlignMethod) -> Self { self.title_justify = justify; self }
393
394 pub fn caption_justify(mut self, justify: AlignMethod) -> Self { self.caption_justify = justify; self }
396
397 pub fn row_styles(mut self, styles: Vec<Style>) -> Self { self.row_styles = styles; self }
399
400 pub fn show_edge(mut self, value: bool) -> Self { self.show_edge = value; self }
402
403 pub fn collapse_padding(mut self, value: bool) -> Self { self.collapse_padding = value; self }
405
406 pub fn pad_edge(mut self, value: bool) -> Self { self.pad_edge = value; self }
408
409 pub fn get_row_style(&self, row_index: usize) -> Option<Style> {
411 if self.row_styles.is_empty() {
412 None
413 } else {
414 Some(self.row_styles[row_index % self.row_styles.len()].clone())
415 }
416 }
417
418 pub fn grid() -> Self {
421 Self {
422 columns: Vec::new(),
423 rows: Vec::new(),
424 title: None,
425 caption: None,
426 box_style: crate::box_drawing::BOX_SIMPLE.clone(),
427 show_header: false,
428 show_footer: false,
429 show_edge: false,
430 show_lines: false,
431 padding: (0, 1, 0, 1),
432 collapse_padding: false,
433 style: Style::new(),
434 border_style: Style::new(),
435 title_style: Style::new().bold(true),
436 caption_style: Style::new().dim(true),
437 title_justify: AlignMethod::Center,
438 caption_justify: AlignMethod::Center,
439 highlight: false,
440 width: None,
441 row_styles: Vec::new(),
442 leading: 0,
443 rowspans: Vec::new(),
444 section_rows: HashSet::new(),
445 pad_edge: true,
446 sections: Vec::new(),
447 }
448 }
449
450 pub fn add_section(&mut self) -> &mut Self {
454 self.section_rows.insert(self.rows.len());
455 self.sections.push(self.rows.len());
456 self
457 }
458
459 pub fn row_count(&self) -> usize { self.rows.len() }
461}
462
463impl Renderable for Table {
464 fn render(&self, options: &ConsoleOptions) -> RenderResult {
465 if self.columns.is_empty() {
466 return RenderResult::new();
467 }
468
469 let box_style = get_safe_box(&self.box_style, options.ascii_only);
470 let available_width = self.width.unwrap_or(options.max_width);
471 let col_count = self.columns.len();
472
473 let col_widths = self.calculate_column_widths(available_width);
475
476 let mut lines: Vec<Vec<Segment>> = Vec::new();
477 let b = &box_style;
478
479 let bs = |ch: char| -> Segment {
481 let ansi = self.border_style.to_ansi();
482 let reset = if ansi.is_empty() { "" } else { "\x1b[0m" };
483 Segment::new(format!("{ansi}{ch}{reset}"))
484 };
485
486 if let Some(ref title) = self.title {
488 let _tw = UnicodeWidthStr::width(title.as_str());
489 let centered = self.title_justify.align_text(title, available_width.saturating_sub(2));
490 lines.push(vec![bs(b.top_left), Segment::new(¢ered[1..centered.len()-1]), bs(b.top_right), Segment::line()]);
491 }
492
493 if self.show_edge {
495 let mut top_line = vec![bs(b.top_left)];
496 for (i, w) in col_widths.iter().enumerate() {
497 top_line.push(Segment::new(b.top.to_string().repeat(*w)));
498 if i < col_count - 1 {
499 top_line.push(bs(b.top_divider));
500 }
501 }
502 top_line.push(bs(b.top_right));
503 top_line.push(Segment::line());
504 lines.push(top_line);
505 }
506
507 if self.show_header && self.columns.iter().any(|c| !c.header.is_empty()) {
509 let (pt, _pr, _pb, _pl) = self.padding;
511 for _ in 0..pt {
512 lines.push(self.render_row_line(&col_widths, &[], &b, available_width, false));
513 }
514
515 let header_cells: Vec<String> = self.columns.iter()
516 .map(|c| c.header.clone())
517 .collect();
518 lines.push(self.render_cell_line(&col_widths, &header_cells, &b, true));
519
520 let mut sep = vec![bs(b.head_row_left)];
522 for (i, w) in col_widths.iter().enumerate() {
523 sep.push(Segment::new(b.head_row_horizontal.to_string().repeat(*w)));
524 if i < col_count - 1 {
525 sep.push(bs(b.head_row_cross));
526 }
527 }
528 sep.push(bs(b.head_row_right));
529 sep.push(Segment::line());
530 lines.push(sep);
531 }
532
533 let mut rowspan_remaining: Vec<usize> = vec![0; col_count];
535 for (row_idx, row) in self.rows.iter().enumerate() {
536 if self.section_rows.contains(&row_idx) {
538 let mut sep = vec![bs(b.head_row_left)];
539 for (i, w) in col_widths.iter().enumerate() {
540 sep.push(Segment::new(b.head_row_horizontal.to_string().repeat(*w)));
541 if i < col_count - 1 {
542 sep.push(bs(b.head_row_cross));
543 }
544 }
545 sep.push(bs(b.head_row_right));
546 sep.push(Segment::line());
547 lines.push(sep);
548 }
549
550 if row_idx > 0 {
552 for _ in 0..self.leading {
553 lines.push(self.render_row_line(&col_widths, &[], &b, available_width, false));
554 }
555 }
556
557 let (pt, _pr, _pb, _pl) = self.padding;
558 for _ in 0..pt {
559 lines.push(self.render_row_line(&col_widths, &[], &b, available_width, false));
560 }
561
562 let _style = if row_idx < self.row_styles.len() {
563 Some(&self.row_styles[row_idx])
564 } else if self.row_styles.len() == 2 {
565 Some(&self.row_styles[row_idx % 2])
566 } else {
567 None
568 };
569
570 lines.push(self.render_cell_line_with_rowspan(
571 &col_widths, row, &b, false, &mut rowspan_remaining,
572 ));
573
574 if self.show_lines && row_idx < self.rows.len() - 1 {
576 let mut sep = vec![bs(b.row_left)];
577 for (i, w) in col_widths.iter().enumerate() {
578 sep.push(Segment::new(b.row_horizontal.to_string().repeat(*w)));
579 if i < col_count - 1 {
580 sep.push(bs(b.row_cross));
581 }
582 }
583 sep.push(bs(b.row_right));
584 sep.push(Segment::line());
585 lines.push(sep);
586 }
587 }
588
589 if self.show_footer && self.columns.iter().any(|c| !c.footer.is_empty()) {
591 let mut sep = vec![bs(b.foot_row_left)];
592 for (i, w) in col_widths.iter().enumerate() {
593 sep.push(Segment::new(b.foot_row_horizontal.to_string().repeat(*w)));
594 if i < col_count - 1 {
595 sep.push(bs(b.foot_row_cross));
596 }
597 }
598 sep.push(bs(b.foot_row_right));
599 sep.push(Segment::line());
600 lines.push(sep);
601
602 let footer_cells: Vec<String> = self.columns.iter()
603 .map(|c| c.footer.clone())
604 .collect();
605 lines.push(self.render_cell_line(&col_widths, &footer_cells, &b, false));
606 }
607
608 if self.show_edge {
610 let mut bot_line = vec![bs(b.bottom_left)];
611 for (i, w) in col_widths.iter().enumerate() {
612 bot_line.push(Segment::new(b.bottom.to_string().repeat(*w)));
613 if i < col_count - 1 {
614 bot_line.push(bs(b.bottom_divider));
615 }
616 }
617 bot_line.push(bs(b.bottom_right));
618 bot_line.push(Segment::line());
619 lines.push(bot_line);
620 }
621
622 if let Some(ref caption) = self.caption {
624 let centered = self.caption_justify.align_text(caption, available_width.saturating_sub(2));
625 lines.push(vec![Segment::new(¢ered), Segment::line()]);
626 }
627
628 RenderResult { lines, items: Vec::new() }
629 }
630}
631
632impl Table {
633 fn calculate_column_widths(&self, available: usize) -> Vec<usize> {
634 let col_count = self.columns.len();
635 let total_pad = col_count.saturating_sub(1) + 2; let content_width = available.saturating_sub(total_pad);
637
638 let mut widths: Vec<usize> = vec![0; col_count];
640 let mut flex_cols: Vec<usize> = Vec::new();
641 let mut used = 0usize;
642
643 for (i, col) in self.columns.iter().enumerate() {
644 if let Some(w) = col.width {
645 widths[i] = w;
646 used += w;
647 } else {
648 flex_cols.push(i);
649 }
650 }
651
652 if flex_cols.is_empty() {
653 return widths;
654 }
655
656 let remaining = content_width.saturating_sub(used);
657 let _flex_count = flex_cols.len();
658
659 let total_ratio: usize = flex_cols.iter()
661 .map(|&i| self.columns[i].ratio.unwrap_or(1))
662 .sum();
663
664 for &i in &flex_cols {
665 let col = &self.columns[i];
666 let ratio = col.ratio.unwrap_or(1);
667 let mut w = (remaining * ratio) / total_ratio;
668 if let Some(min_w) = col.min_width {
669 w = w.max(min_w);
670 }
671 if let Some(max_w) = col.max_width {
672 w = w.min(max_w);
673 }
674 w = w.max(3); widths[i] = w;
676 }
677
678 let total: usize = widths.iter().sum();
680 if total < content_width && !flex_cols.is_empty() {
681 let extra = content_width - total;
682 widths[flex_cols[flex_cols.len() - 1]] += extra;
683 }
684
685 widths
686 }
687
688 fn render_cell_line(
689 &self,
690 widths: &[usize],
691 values: &[String],
692 b: &BoxStyle,
693 is_header: bool,
694 ) -> Vec<Segment> {
695 let mut line = Vec::new();
696 let col_count = widths.len();
697 let bs = |ch: char| -> Segment {
698 let ansi = self.border_style.to_ansi();
699 let reset = if ansi.is_empty() { "" } else { "\x1b[0m" };
700 Segment::new(format!("{ansi}{ch}{reset}"))
701 };
702
703 line.push(bs(b.mid_vertical));
704
705 for (i, w) in widths.iter().enumerate() {
706 let val = values.get(i).map(|s| s.as_str()).unwrap_or("");
707 let col = self.columns.get(i);
708 let justify = col.map(|c| c.justify).unwrap_or(AlignMethod::Left);
709 let (_pt, pr, _pb, pl) = self.padding;
710
711 let left_pad = if i == 0 && !self.pad_edge { 0 } else { pl };
713 let right_pad = if i == col_count - 1 && !self.pad_edge { 0 } else { pr };
714
715 line.push(Segment::new(" ".repeat(left_pad)));
717
718 let content_w = w.saturating_sub(left_pad + right_pad);
720 let disp = justify.align_text(val, content_w);
721 let disp_trunc = if UnicodeWidthStr::width(disp.as_str()) > content_w {
723 let mut truncated = disp.chars().take(
724 content_w.saturating_sub(1) ).collect::<String>();
726 truncated.push('…');
727 truncated
728 } else {
729 disp
730 };
731
732 if is_header {
734 let header_style = col.map(|c| &c.header_style);
735 if let Some(hs) = header_style {
736 let ansi = hs.to_ansi();
737 let reset = hs.reset_ansi();
738 line.push(Segment::new(format!("{ansi}{disp_trunc}{reset}")));
739 } else {
740 line.push(Segment::new(disp_trunc));
741 }
742 } else {
743 line.push(Segment::new(disp_trunc));
744 }
745
746 line.push(Segment::new(" ".repeat(right_pad)));
748
749 if i < col_count - 1 {
750 line.push(bs(b.mid_vertical));
751 }
752 }
753
754 line.push(bs(b.mid_right));
755 line.push(Segment::line());
756 line
757 }
758
759 fn render_cell_line_with_rowspan(
762 &self,
763 widths: &[usize],
764 cells: &[Cell],
765 b: &BoxStyle,
766 is_header: bool,
767 rowspan_remaining: &mut [usize],
768 ) -> Vec<Segment> {
769 let mut line = Vec::new();
770 let col_count = widths.len();
771 let bs = |ch: char| -> Segment {
772 let ansi = self.border_style.to_ansi();
773 let reset = if ansi.is_empty() { "" } else { "\x1b[0m" };
774 Segment::new(format!("{ansi}{ch}{reset}"))
775 };
776
777 line.push(bs(b.mid_vertical));
778
779 let mut cell_idx = 0;
780 let mut col: usize = 0;
781
782 while col < col_count {
783 if rowspan_remaining[col] > 0 {
785 rowspan_remaining[col] -= 1;
786 let w = widths[col];
788 let (_pt, pr, _pb, pl) = self.padding;
789 let left_pad = if col == 0 && !self.pad_edge { 0 } else { pl };
790 let right_pad = if col == col_count - 1 && !self.pad_edge { 0 } else { pr };
791 line.push(Segment::new(" ".repeat(left_pad + w + right_pad)));
792 if col < col_count - 1 {
793 line.push(bs(b.mid_vertical));
794 }
795 col += 1;
796 continue;
797 }
798
799 if cell_idx >= cells.len() {
801 let w = widths[col];
802 let (_pt, pr, _pb, pl) = self.padding;
803 let left_pad = if col == 0 && !self.pad_edge { 0 } else { pl };
804 let right_pad = if col == col_count - 1 && !self.pad_edge { 0 } else { pr };
805 line.push(Segment::new(" ".repeat(left_pad + w + right_pad)));
806 if col < col_count - 1 {
807 line.push(bs(b.mid_vertical));
808 }
809 col += 1;
810 continue;
811 }
812
813 let cell = &cells[cell_idx];
814 cell_idx += 1;
815
816 let span_end = (col + cell.colspan).min(col_count);
817 let span_width: usize = widths[col..span_end].iter().sum();
818 let (_pt, pr, _pb, pl) = self.padding;
819 let left_pad = if col == 0 && !self.pad_edge { 0 } else { pl };
820 let right_pad = if span_end >= col_count && !self.pad_edge { 0 } else { pr };
821 let content_width = span_width.saturating_sub(left_pad + right_pad);
822
823 let col_def = self.columns.get(col);
824 let justify = col_def.map(|c| c.justify).unwrap_or(AlignMethod::Left);
825
826 let disp_text = justify.align_text(&cell.content, content_width);
828 let disp_trunc = if UnicodeWidthStr::width(disp_text.as_str()) > content_width {
829 let mut truncated: String = disp_text.chars()
830 .take(content_width.saturating_sub(1))
831 .collect();
832 truncated.push('…');
833 truncated
834 } else {
835 disp_text
836 };
837
838 line.push(Segment::new(" ".repeat(left_pad)));
840
841 if let Some(ref cell_style) = cell.style {
843 let ansi = cell_style.to_ansi();
844 let reset = if ansi.is_empty() { "" } else { "\x1b[0m" };
845 line.push(Segment::new(format!("{ansi}{disp_trunc}{reset}")));
846 } else if is_header {
847 if let Some(hs) = col_def.map(|c| &c.header_style) {
848 let ansi = hs.to_ansi();
849 let reset = hs.reset_ansi();
850 line.push(Segment::new(format!("{ansi}{disp_trunc}{reset}")));
851 } else {
852 line.push(Segment::new(disp_trunc));
853 }
854 } else {
855 let col_ansi = col_def.map(|c| c.style.to_ansi()).unwrap_or_default();
857 if col_ansi.is_empty() {
858 line.push(Segment::new(disp_trunc));
859 } else {
860 line.push(Segment::new(format!("{col_ansi}{disp_trunc}\x1b[0m")));
861 }
862 }
863
864 line.push(Segment::new(" ".repeat(right_pad)));
866
867 if cell.rowspan > 1 {
869 for rc in col..span_end {
870 rowspan_remaining[rc] = cell.rowspan - 1;
871 }
872 }
873
874 col = span_end;
875
876 if col < col_count {
878 line.push(bs(b.mid_vertical));
879 }
880 }
881
882 line.push(bs(b.mid_right));
883 line.push(Segment::line());
884 line
885 }
886
887 fn render_row_line(
888 &self,
889 widths: &[usize],
890 _values: &[String],
891 b: &BoxStyle,
892 _available_width: usize,
893 _is_header: bool,
894 ) -> Vec<Segment> {
895 let mut line = Vec::new();
896 let col_count = widths.len();
897 let bs = |ch: char| -> Segment {
898 let ansi = self.border_style.to_ansi();
899 let reset = if ansi.is_empty() { "" } else { "\x1b[0m" };
900 Segment::new(format!("{ansi}{ch}{reset}"))
901 };
902
903 line.push(bs(b.mid_vertical));
904 for (i, w) in widths.iter().enumerate() {
905 let (_pt, pr, _pb, pl) = self.padding;
906 let left_pad = if i == 0 && !self.pad_edge { 0 } else { pl };
907 let right_pad = if i == col_count - 1 && !self.pad_edge { 0 } else { pr };
908 line.push(Segment::new(" ".repeat(left_pad + w + right_pad)));
909 if i < col_count - 1 {
910 line.push(bs(b.mid_vertical));
911 }
912 }
913 line.push(bs(b.mid_right));
914 line.push(Segment::line());
915 line
916 }
917}
918
919impl Default for Table {
920 fn default() -> Self {
921 Self::new()
922 }
923}
924
925#[cfg(test)]
926mod tests {
927 use super::*;
928
929 #[test]
930 fn test_empty_table() {
931 let table = Table::new();
932 let opts = ConsoleOptions::default();
933 let result = table.render(&opts);
934 assert!(result.lines.is_empty());
935 }
936
937 #[test]
938 fn test_table_with_one_column() {
939 let mut table = Table::new();
940 table.add_column(Column::new("Name"));
941 table.add_row_str(vec!["Alice".into()]);
942 table.add_row_str(vec!["Bob".into()]);
943
944 let opts = ConsoleOptions::default();
945 let result = table.render(&opts);
946 let ansi = result.to_ansi();
947 assert!(ansi.contains("Name"));
948 assert!(ansi.contains("Alice"));
949 }
950
951 #[test]
952 fn test_cell_creation() {
953 let cell = Cell::new("hello");
954 assert_eq!(cell.content, "hello");
955 assert_eq!(cell.colspan, 1);
956 assert_eq!(cell.rowspan, 1);
957 assert!(cell.style.is_none());
958
959 let cell2 = Cell::new("world").colspan(2).rowspan(3);
960 assert_eq!(cell2.content, "world");
961 assert_eq!(cell2.colspan, 2);
962 assert_eq!(cell2.rowspan, 3);
963 }
964
965 #[test]
966 fn test_cell_from_string() {
967 let cell: Cell = "test".into();
968 assert_eq!(cell.content, "test");
969 }
970
971 #[test]
972 fn test_column_colspan() {
973 let col = Column::new("Header");
974 assert_eq!(col.colspan, 1);
975 }
976
977 #[test]
978 fn test_add_row_str() {
979 let mut table = Table::new();
980 table.add_column(Column::new("A"));
981 table.add_column(Column::new("B"));
982 table.add_row_str(vec!["x".into(), "y".into()]);
983 assert_eq!(table.row_count(), 1);
984 }
985
986 #[test]
987 fn test_add_section() {
988 let mut table = Table::new();
989 table.add_column(Column::new("A"));
990 table.add_row_str(vec!["r1".into()]);
991 table.add_section();
992 table.add_row_str(vec!["r2".into()]);
993 assert_eq!(table.row_count(), 2);
994 assert!(table.section_rows.contains(&1));
995
996 let opts = ConsoleOptions::default();
997 let result = table.render(&opts);
998 let ansi = result.to_ansi();
999 assert!(ansi.contains("r1"));
1000 assert!(ansi.contains("r2"));
1001 }
1002
1003 #[test]
1004 fn test_leading() {
1005 let table = Table::new()
1006 .column(Column::new("X"))
1007 .row_str(vec!["a".into()])
1008 .row_str(vec!["b".into()])
1009 .leading(1);
1010 assert_eq!(table.leading, 1);
1011 }
1012
1013 #[test]
1014 fn test_cell_rowspan() {
1015 let mut table = Table::new();
1016 table.add_column(Column::new("A"));
1017 table.add_column(Column::new("B"));
1018 let cell_a = Cell::new("span").rowspan(2);
1019 let cell_b = Cell::new("single");
1020 table.add_row(vec![cell_a, cell_b]);
1021 table.add_row_str(vec!["row2col2".into()]);
1022
1023 let opts = ConsoleOptions::default();
1024 let result = table.render(&opts);
1025 let ansi = result.to_ansi();
1026 assert!(ansi.contains("span"));
1027 }
1028
1029 #[test]
1030 fn test_cell_colspan() {
1031 let mut table = Table::new();
1032 table.add_column(Column::new("A"));
1033 table.add_column(Column::new("B"));
1034 table.add_column(Column::new("C"));
1035 let cell = Cell::new("wide").colspan(2);
1036 table.add_row(vec![cell, Cell::new("c")]);
1037 table.add_row_str(vec!["a".into(), "b".into(), "c".into()]);
1038
1039 let opts = ConsoleOptions::default();
1040 let result = table.render(&opts);
1041 let ansi = result.to_ansi();
1042 assert!(ansi.contains("wide"));
1043 }
1044
1045 #[test]
1048 fn test_row_struct() {
1049 let cells = vec![Cell::new("a"), Cell::new("b")];
1050 let row = Row::new(cells)
1051 .style(Style::new().bold(true))
1052 .end_section(true);
1053 assert_eq!(row.cells.len(), 2);
1054 assert!(row.style.is_some());
1055 assert!(row.end_section);
1056 }
1057
1058 #[test]
1059 fn test_add_row_explicit() {
1060 let mut table = Table::new();
1061 table.add_column(Column::new("A"));
1062 table.add_column(Column::new("B"));
1063 let row = Row::new(vec![Cell::new("x"), Cell::new("y")]);
1064 table.add_row_explicit(row);
1065 assert_eq!(table.row_count(), 1);
1066
1067 let opts = ConsoleOptions::default();
1068 let result = table.render(&opts);
1069 let ansi = result.to_ansi();
1070 assert!(ansi.contains("x"));
1071 assert!(ansi.contains("y"));
1072 }
1073
1074 #[test]
1075 fn test_add_row_explicit_with_section() {
1076 let mut table = Table::new();
1077 table.add_column(Column::new("A"));
1078 table.add_row_str(vec!["before".into()]);
1079 let row = Row::new(vec![Cell::new("after")]).end_section(true);
1080 table.add_row_explicit(row);
1081 assert!(table.section_rows.contains(&1));
1082 }
1083
1084 #[test]
1085 fn test_builder_highlight() {
1086 let table = Table::new().highlight(true);
1087 assert!(table.highlight);
1088 }
1089
1090 #[test]
1091 fn test_builder_title_justify() {
1092 let table = Table::new().title_justify(AlignMethod::Right);
1093 assert_eq!(table.title_justify, AlignMethod::Right);
1094 }
1095
1096 #[test]
1097 fn test_builder_caption_justify() {
1098 let table = Table::new().caption_justify(AlignMethod::Left);
1099 assert_eq!(table.caption_justify, AlignMethod::Left);
1100 }
1101
1102 #[test]
1103 fn test_builder_row_styles() {
1104 let s1 = Style::new().bold(true);
1105 let s2 = Style::new().dim(true);
1106 let table = Table::new().row_styles(vec![s1.clone(), s2.clone()]);
1107 assert_eq!(table.row_styles.len(), 2);
1108 }
1109
1110 #[test]
1111 fn test_builder_show_edge() {
1112 let table = Table::new().show_edge(false);
1113 assert!(!table.show_edge);
1114 }
1115
1116 #[test]
1117 fn test_builder_collapse_padding() {
1118 let table = Table::new().collapse_padding(true);
1119 assert!(table.collapse_padding);
1120 }
1121
1122 #[test]
1123 fn test_builder_pad_edge() {
1124 let table = Table::new().pad_edge(false);
1125 assert!(!table.pad_edge);
1126 }
1127
1128 #[test]
1129 fn test_get_row_style_empty() {
1130 let table = Table::new();
1131 assert_eq!(table.get_row_style(0), None);
1132 }
1133
1134 #[test]
1135 fn test_get_row_style_with_styles() {
1136 let s1 = Style::new().bold(true);
1137 let s2 = Style::new().dim(true);
1138 let table = Table::new().row_styles(vec![s1, s2]);
1139 assert!(table.get_row_style(0).is_some());
1140 assert!(table.get_row_style(1).is_some());
1141 assert!(table.get_row_style(2).is_some());
1143 assert!(table.get_row_style(3).is_some());
1144 }
1145
1146 #[test]
1147 fn test_add_section_returns_self() {
1148 let mut table = Table::new();
1149 table.add_column(Column::new("A"));
1150 table.add_row_str(vec!["r1".into()]);
1151 let ret = table.add_section();
1152 ret.add_row_str(vec!["r2".into()]);
1154 assert_eq!(table.row_count(), 2);
1155 }
1156
1157 #[test]
1158 fn test_sections_field() {
1159 let mut table = Table::new();
1160 table.add_column(Column::new("A"));
1161 table.add_row_str(vec!["r1".into()]);
1162 table.add_section();
1163 table.add_row_str(vec!["r2".into()]);
1164 assert_eq!(table.sections.len(), 1);
1165 assert_eq!(table.sections[0], 1);
1166 }
1167
1168 #[test]
1169 fn test_pad_edge_default() {
1170 let table = Table::new();
1171 assert!(table.pad_edge);
1172 }
1173
1174 #[test]
1175 fn test_grid_method() {
1176 let table = Table::grid();
1177 assert!(!table.show_edge);
1178 assert!(!table.show_header);
1179 }
1180}