#[must_use]
pub fn table<H: AsRef<str>, C: AsRef<str>>(headers: &[H], rows: &[Vec<C>]) -> String {
let headers: Vec<String> = headers.iter().map(|h| clean(h.as_ref())).collect();
let rows: Vec<Vec<String>> = rows
.iter()
.map(|row| row.iter().map(|cell| clean(cell.as_ref())).collect())
.collect();
render(&headers, &rows)
}
fn render(headers: &[String], rows: &[Vec<String>]) -> String {
let columns = headers.len();
if columns == 0 {
return String::new();
}
let cell = |row: &[String], column: usize| row.get(column).cloned().unwrap_or_default();
let widths: Vec<usize> = (0..columns)
.map(|column| {
rows.iter()
.map(|row| cell(row, column).chars().count())
.chain([headers[column].chars().count(), 3])
.max()
.unwrap_or(3)
})
.collect();
let line = |cells: Vec<String>| {
let padded: Vec<String> = cells
.iter()
.zip(&widths)
.map(|(text, width)| format!("{text:<width$}"))
.collect();
format!("| {} |\n", padded.join(" | "))
};
let mut out = line(headers.to_vec());
out.push_str(&line(widths.iter().map(|w| "-".repeat(*w)).collect()));
for row in rows {
out.push_str(&line(
(0..columns).map(|column| cell(row, column)).collect(),
));
}
out
}
fn clean(cell: &str) -> String {
cell.replace('|', "\\|").replace(['\r', '\n'], " ")
}
#[cfg(test)]
#[path = "table.test.rs"]
mod tests;
#[cfg(test)]
#[path = "table.spec.rs"]
mod spec;