wowlab_common/markdown/
table.rs1use std::fmt::Display;
4
5const DEFAULT_TABLE_PADDING: usize = 2;
6const HEADER_ROW_COUNT: usize = 2;
7
8#[must_use]
10pub fn escape_table_cell(text: &str) -> String {
11 text.replace('|', r"\|")
12}
13
14#[derive(Debug)]
16pub struct Table {
17 headers: Vec<Box<str>>,
18 rows: Vec<Box<[Box<str>]>>,
19 padding: usize,
20}
21
22impl Table {
23 #[must_use]
24 pub fn new() -> Self {
25 Self {
26 headers: Vec::new(),
27 rows: Vec::new(),
28 padding: DEFAULT_TABLE_PADDING,
29 }
30 }
31
32 #[must_use]
33 pub fn headers<I, T>(mut self, headers: I) -> Self
34 where
35 I: IntoIterator<Item = T>,
36 T: Display,
37 {
38 self.headers = headers
39 .into_iter()
40 .map(|header| header.to_string().into_boxed_str())
41 .collect();
42
43 self
44 }
45
46 #[must_use]
47 pub fn row<I, T>(mut self, values: I) -> Self
48 where
49 I: IntoIterator<Item = T>,
50 T: Display,
51 {
52 self.rows.push(
53 values
54 .into_iter()
55 .map(|value| value.to_string().into_boxed_str())
56 .collect::<Vec<_>>()
57 .into_boxed_slice(),
58 );
59
60 self
61 }
62
63 #[must_use]
64 pub fn rows<I, R, T>(mut self, rows: I) -> Self
65 where
66 I: IntoIterator<Item = R>,
67 R: IntoIterator<Item = T>,
68 T: Display,
69 {
70 for row in rows {
71 self.rows.push(
72 row.into_iter()
73 .map(|value| value.to_string().into_boxed_str())
74 .collect::<Vec<_>>()
75 .into_boxed_slice(),
76 );
77 }
78
79 self
80 }
81
82 #[must_use]
83 pub fn padding(mut self, padding: usize) -> Self {
84 self.padding = padding;
85
86 self
87 }
88
89 #[must_use]
91 pub fn build_plain(self) -> String {
92 let widths = self.column_widths();
93 let mut lines = Vec::with_capacity(
94 self.rows.len() + usize::from(!self.headers.is_empty()) * HEADER_ROW_COUNT,
95 );
96 let pad = " ".repeat(self.padding);
97
98 if !self.headers.is_empty() {
99 let header_line: Vec<String> = self
100 .headers
101 .iter()
102 .enumerate()
103 .map(|(i, h)| format!("{:width$}", h, width = widths.get(i).copied().unwrap_or(0)))
104 .collect();
105
106 lines.push(header_line.join(&pad).trim_end().to_string());
107
108 let sep: Vec<String> = widths.iter().map(|&w| "─".repeat(w)).collect();
109
110 lines.push(sep.join(&pad));
111 }
112
113 for row in &self.rows {
114 let row_line: Vec<String> = row
115 .iter()
116 .enumerate()
117 .map(|(i, cell)| {
118 format!(
119 "{:width$}",
120 cell,
121 width = widths.get(i).copied().unwrap_or(0)
122 )
123 })
124 .collect();
125
126 lines.push(row_line.join(&pad).trim_end().to_string());
127 }
128
129 lines.join("\n")
130 }
131
132 #[must_use]
134 pub fn build_markdown(self) -> String {
135 let widths = self.column_widths();
136 let mut lines = Vec::with_capacity(
137 self.rows.len() + usize::from(!self.headers.is_empty()) * HEADER_ROW_COUNT,
138 );
139
140 if !self.headers.is_empty() {
141 let header_line: Vec<String> = self
142 .headers
143 .iter()
144 .enumerate()
145 .map(|(i, h)| format!("{:width$}", h, width = widths.get(i).copied().unwrap_or(0)))
146 .collect();
147
148 lines.push(format!("| {} |", header_line.join(" | ")));
149
150 let sep: Vec<String> = widths.iter().map(|&w| "-".repeat(w)).collect();
151
152 lines.push(format!("| {} |", sep.join(" | ")));
153 }
154
155 for row in &self.rows {
156 let row_line: Vec<String> = row
157 .iter()
158 .enumerate()
159 .map(|(i, cell)| {
160 let escaped = escape_table_cell(cell);
161
162 format!(
163 "{:width$}",
164 escaped,
165 width = widths.get(i).copied().unwrap_or(0)
166 )
167 })
168 .collect();
169
170 lines.push(format!("| {} |", row_line.join(" | ")));
171 }
172
173 lines.join("\n")
174 }
175
176 #[must_use]
178 pub fn build(self) -> String {
179 format!("```\n{}\n```", self.build_plain())
180 }
181
182 fn column_widths(&self) -> Vec<usize> {
183 let col_count = self
184 .headers
185 .len()
186 .max(self.rows.first().map_or(0, |r| r.len()));
187
188 let mut widths = vec![0; col_count];
189
190 for (i, header) in self.headers.iter().enumerate() {
191 widths[i] = widths[i].max(header.chars().count());
193 }
194
195 for row in &self.rows {
196 for (i, cell) in row.iter().enumerate() {
197 if i < widths.len() {
198 let pipe_extra = cell.chars().filter(|&c| c == '|').count();
199 widths[i] = widths[i].max(cell.chars().count() + pipe_extra);
202 }
203 }
204 }
205
206 widths
207 }
208}
209
210impl Default for Table {
211 fn default() -> Self {
212 Self::new()
213 }
214}
215
216pub fn render_table_row(cells: &[impl AsRef<str>]) -> String {
218 let escaped: Vec<String> = cells
219 .iter()
220 .map(|cell| escape_table_cell(cell.as_ref()))
221 .collect();
222
223 format!("| {} |", escaped.join(" | "))
224}