1pub fn render(headers: &[&str], rows: &[Vec<String>]) -> String {
12 if headers.is_empty() {
13 return String::new();
14 }
15
16 let widths: Vec<usize> = headers
17 .iter()
18 .enumerate()
19 .map(|(index, header)| {
20 rows.iter()
21 .filter_map(|row| row.get(index))
22 .map(|cell| cell.chars().count())
23 .chain(std::iter::once(header.chars().count()))
24 .max()
25 .unwrap_or(0)
26 })
27 .collect();
28
29 let header_cells: Vec<String> = headers.iter().map(|h| (*h).to_string()).collect();
30
31 let mut out = String::new();
32 out.push_str(&rule('┌', '┬', '┐', &widths));
33 out.push_str(&line(&header_cells, &widths));
34 out.push_str(&rule('├', '┼', '┤', &widths));
35 for row in rows {
36 out.push_str(&line(row, &widths));
37 }
38 out.push_str(&rule('└', '┴', '┘', &widths));
39 out
40}
41
42fn rule(left: char, join: char, right: char, widths: &[usize]) -> String {
43 let segments: Vec<String> = widths.iter().map(|w| "─".repeat(w + 2)).collect();
44 format!("{left}{}{right}\n", segments.join(&join.to_string()))
45}
46
47fn line(cells: &[String], widths: &[usize]) -> String {
48 let mut out = String::from("│");
49 for (index, width) in widths.iter().enumerate() {
50 let empty = String::new();
51 let cell = cells.get(index).unwrap_or(&empty);
52 let pad = width.saturating_sub(cell.chars().count());
53 out.push(' ');
54 out.push_str(cell);
55 out.push_str(&" ".repeat(pad));
56 out.push_str(" │");
57 }
58 out.push('\n');
59 out
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65
66 fn row(cells: &[&str]) -> Vec<String> {
67 cells.iter().map(|c| (*c).to_string()).collect()
68 }
69
70 #[test]
71 fn every_line_is_the_same_width() {
72 let table = render(&["A", "B"], &[row(&["one", "two"]), row(&["longer", "x"])]);
73 let widths: Vec<usize> = table.lines().map(|l| l.chars().count()).collect();
74
75 assert!(widths.windows(2).all(|pair| pair[0] == pair[1]), "{table}");
76 }
77
78 #[test]
79 fn a_multibyte_cell_does_not_skew_the_border() {
80 let table = render(&["A", "B"], &[row(&["—", "✓"]), row(&["ascii", "y"])]);
81 let widths: Vec<usize> = table.lines().map(|l| l.chars().count()).collect();
82
83 assert!(widths.windows(2).all(|pair| pair[0] == pair[1]), "{table}");
84 }
85
86 #[test]
87 fn a_short_row_is_padded_out() {
88 let table = render(&["A", "B", "C"], &[row(&["only"])]);
89 let widths: Vec<usize> = table.lines().map(|l| l.chars().count()).collect();
90
91 assert!(widths.windows(2).all(|pair| pair[0] == pair[1]), "{table}");
92 }
93
94 #[test]
95 fn a_wide_cell_sets_the_column_width() {
96 let table = render(&["A"], &[row(&["a much longer cell"])]);
97
98 assert!(table.contains("a much longer cell │"));
99 }
100
101 #[test]
102 fn corners_are_drawn() {
103 let table = render(&["A"], &[row(&["x"])]);
104
105 assert!(table.starts_with('┌'));
106 assert!(table.contains('├'));
107 assert!(table.trim_end().ends_with('┘'));
108 }
109
110 #[test]
111 fn no_headers_renders_nothing() {
112 assert_eq!(render(&[], &[row(&["x"])]), "");
113 }
114}