use unicode_width::UnicodeWidthStr;
const GRID_MAX_WIDTH: usize = 96;
struct Table {
header: Vec<String>,
rows: Vec<Vec<String>>,
}
fn cells(line: &str) -> Vec<String> {
let t = line.trim();
let t = t.strip_prefix('|').unwrap_or(t);
let t = t.strip_suffix('|').unwrap_or(t);
t.split('|').map(|c| strip_emphasis(c.trim())).collect()
}
fn strip_emphasis(cell: &str) -> String {
cell.replace("**", "").replace('`', "")
}
fn is_separator(line: &str) -> bool {
let c = cells(line);
!c.is_empty()
&& c.iter().all(|cell| {
!cell.is_empty()
&& cell.chars().all(|ch| ch == '-' || ch == ':' || ch == ' ')
&& cell.contains('-')
})
}
fn looks_like_row(line: &str) -> bool {
line.trim().starts_with('|') && line.trim().len() > 1
}
fn col_widths(table: &Table) -> Vec<usize> {
let mut w: Vec<usize> = table.header.iter().map(|c| c.width()).collect();
for row in &table.rows {
for (i, cell) in row.iter().enumerate() {
if let Some(slot) = w.get_mut(i) {
*slot = (*slot).max(cell.width());
}
}
}
w
}
fn render_grid(table: &Table) -> String {
let w = col_widths(table);
let pad = |row: &[String]| -> String {
row.iter()
.enumerate()
.map(|(i, cell)| {
let col = w.get(i).copied().unwrap_or(0);
let gap = (col + 2).saturating_sub(cell.width());
format!("{cell}{}", " ".repeat(gap))
})
.collect::<Vec<_>>()
.join(" ")
};
let rule = w
.iter()
.map(|x| "─".repeat(x + 2))
.collect::<Vec<_>>()
.join(" ");
let mut out = vec!["```text".to_string(), pad(&table.header), rule];
for row in &table.rows {
out.push(pad(row));
}
out.push("```".to_string());
out.join("\n")
}
fn render_key_value(table: &Table) -> String {
let mut out = Vec::new();
for row in &table.rows {
let label = row.first().map(|s| s.as_str()).unwrap_or("");
if label.is_empty() && row.iter().all(|c| c.is_empty()) {
continue;
}
out.push(format!("**{label}**"));
for (i, cell) in row.iter().enumerate().skip(1) {
if cell.is_empty() {
continue;
}
let head = table.header.get(i).map(|s| s.as_str()).unwrap_or("");
if head.is_empty() {
out.push(format!("└ {cell}"));
} else {
out.push(format!("└ {head}: {cell}"));
}
}
}
out.join("\n")
}
fn render(table: &Table) -> String {
let grid_width: usize = col_widths(table)
.iter()
.map(|x| x + 3)
.sum::<usize>()
.saturating_sub(1);
if grid_width <= GRID_MAX_WIDTH {
render_grid(table)
} else {
render_key_value(table)
}
}
pub(crate) fn tables_to_discord(text: &str) -> String {
if !text.contains('|') {
return text.to_string();
}
let lines: Vec<&str> = text.lines().collect();
let mut out: Vec<String> = Vec::new();
let mut i = 0usize;
let mut in_fence = false;
while i < lines.len() {
let line = lines[i];
if line.trim_start().starts_with("```") {
in_fence = !in_fence;
out.push(line.to_string());
i += 1;
continue;
}
if in_fence {
out.push(line.to_string());
i += 1;
continue;
}
let has_table = looks_like_row(line)
&& i + 1 < lines.len()
&& looks_like_row(lines[i + 1])
&& is_separator(lines[i + 1]);
if !has_table {
out.push(line.to_string());
i += 1;
continue;
}
let header = cells(line);
let mut rows = Vec::new();
let mut j = i + 2;
while j < lines.len() && looks_like_row(lines[j]) && !is_separator(lines[j]) {
rows.push(cells(lines[j]));
j += 1;
}
let rendered = render(&Table { header, rows });
if !rendered.is_empty() {
out.push(rendered);
}
i = j;
}
out.join("\n")
}