1use std::{cmp, fmt::Display};
2
3use anyhow::{bail, Result};
4
5use pad::{Alignment, PadStr};
6
7#[derive(Debug, Clone)]
8pub enum HeadingAlignment {
9 Left,
10 Center,
11 Right,
12 Default,
13}
14
15impl HeadingAlignment {
16 fn to_padded_string(&self, length: usize) -> String {
17 match self {
18 HeadingAlignment::Default => "---".pad(length, '-', Alignment::Left, false),
19 HeadingAlignment::Left => ":--".pad(length, '-', Alignment::Left, false),
20 HeadingAlignment::Center => {
21 format!("{}:", ":-".pad(length - 1, '-', Alignment::Left, false))
22 }
23 HeadingAlignment::Right => "--:".pad(length, '-', Alignment::Right, false),
24 }
25 }
26}
27
28#[derive(Debug, Clone)]
29pub struct Heading {
30 label: String,
31 alignment: HeadingAlignment,
32}
33
34impl Heading {
35 pub fn new(label: String, alignment: Option<HeadingAlignment>) -> Self {
36 Self {
37 label,
38 alignment: alignment.unwrap_or(HeadingAlignment::Default),
39 }
40 }
41}
42
43#[derive(Debug, Clone)]
44pub struct MarkdownTable<T>
45where
46 T: ToString + Display,
47{
48 headings: Option<Vec<Heading>>,
49 cells: Vec<Vec<T>>,
50}
51
52impl<T> MarkdownTable<T>
53where
54 T: ToString + Display,
55{
56 pub fn new(cells: Vec<Vec<T>>) -> Self {
57 Self {
58 cells,
59 headings: None,
60 }
61 }
62
63 pub fn with_headings(&mut self, headings: Vec<Heading>) -> &Self {
64 self.headings = Some(headings);
65 self
66 }
67
68 pub fn as_markdown(&self) -> Result<String> {
69 match &self.headings {
70 Some(headings) => {
71 let mut col_width: Vec<usize> = headings
72 .iter()
73 .map(|h| cmp::max(5, h.label.len()))
74 .collect();
75
76 for row in &self.cells {
77 for (col_index, value) in row.iter().enumerate() {
78 let value_len = value.to_string().len();
80 if value_len > col_width[col_index] {
81 col_width[col_index] = value_len;
82 }
83 }
84 }
85 let header_row = format!(
86 "|{}|",
87 headings
88 .iter()
89 .enumerate()
90 .map(|(i, h)| format!(" {} ", h.label).pad_to_width(col_width[i]))
91 .collect::<Vec<String>>()
92 .join("|")
93 );
94 let header_split_row = format!(
95 "|{}|",
96 headings
97 .iter()
98 .enumerate()
99 .map(|(i, h)| format!(
100 " {} ",
101 h.alignment.to_padded_string(col_width[i] - 2)
102 ))
103 .collect::<Vec<String>>()
104 .join("|")
105 );
106 let cells_row = self
107 .cells
108 .iter()
109 .map(|v| {
110 format!(
111 "|{}|",
112 v.iter()
113 .enumerate()
114 .map(|(i, v_inner)| format!(" {} ", v_inner)
115 .pad_to_width(col_width[i]))
116 .collect::<Vec<String>>()
117 .join("|")
118 )
119 })
120 .collect::<Vec<String>>()
121 .join("\n");
122 Ok(format!(
123 "{}\n{}\n{}\n",
124 header_row, header_split_row, cells_row
125 ))
126 }
127 None => {
128 if !self.cells.is_empty() {
129 Ok(format!(
130 "<table>{}</table>",
131 self.cells
132 .iter()
133 .map(|v| {
134 format!(
135 "<tr>{}",
136 v.iter()
137 .map(|v_inner| format!("<td>{}", v_inner))
138 .collect::<Vec<String>>()
139 .join("")
140 )
141 })
142 .collect::<Vec<String>>()
143 .join(""),
144 ))
145 } else {
146 bail!("Table must have at least 1 row.".to_string())
147 }
148 }
149 }
150 }
151}
152
153impl<T> ToString for MarkdownTable<T>
154where
155 T: ToString + Display,
156{
157 fn to_string(&self) -> String {
158 self.as_markdown().unwrap()
159 }
160}