markdown_table/
lib.rs

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                        // TODO using tostring,  may be not good as mutliline string will break it
79                        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}