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| c.trim().to_string()).collect()
}
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 render(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;
}
if table.header.len() <= 2 {
let value = row.get(1).map(|s| s.as_str()).unwrap_or("");
if value.is_empty() {
out.push(format!("*{label}*"));
} else {
out.push(format!("*{label}* — {value}"));
}
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 headings_to_slack(text: &str) -> String {
let mut out: Vec<String> = Vec::new();
let mut in_fence = false;
let mut seen_heading = false;
for line in text.lines() {
if line.trim_start().starts_with("```") {
in_fence = !in_fence;
out.push(line.to_string());
continue;
}
let trimmed = line.trim_start();
let is_heading = !in_fence && trimmed.starts_with('#');
if !is_heading {
out.push(line.to_string());
continue;
}
let title = trimmed.trim_start_matches('#').trim();
if title.is_empty() {
out.push(line.to_string());
continue;
}
if seen_heading {
out.push("---".to_string());
}
seen_heading = true;
out.push(format!("*{}*", title.to_uppercase()));
}
out.join("\n")
}
pub(crate) fn structure_to_slack(text: &str) -> String {
headings_to_slack(&tables_to_slack(text))
}
pub(crate) fn tables_to_slack(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")
}