fix(slack): tables as monospace blocks + ASCII dividers + strikethrough

Tables: Slack has no table syntax. Converter now detects markdown tables
and renders them as monospace code blocks with aligned columns.

Dividers: replaced unicode em-dash (caused encoding artifacts) with
plain ASCII dashes.

Strikethrough: ~~text~~ converts to ~text~ (Slack native).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
rabbitblood 2026-04-17 15:01:46 -07:00
parent 9b1139ee90
commit 9ef314fef5

View File

@ -177,6 +177,10 @@ func (s *SlackAdapter) sendWebhookMessage(ctx context.Context, config map[string
// MD `code` → mrkdwn `code` (same)
// MD ```block``` → mrkdwn ```block``` (same)
func markdownToMrkdwn(text string) string {
// First pass: convert markdown tables to aligned plain text.
// Slack has no table support — render as monospace columns.
text = convertTables(text)
lines := strings.Split(text, "\n")
for i, line := range lines {
trimmed := strings.TrimSpace(line)
@ -190,12 +194,24 @@ func markdownToMrkdwn(text string) string {
}
}
// Horizontal rules
// Horizontal rules → simple dashes (no unicode em-dash)
if trimmed == "---" || trimmed == "***" || trimmed == "___" {
lines[i] = "———"
lines[i] = "----------"
continue
}
// Strikethrough: ~~text~~ → ~text~ (Slack uses single tilde)
for strings.Contains(lines[i], "~~") {
first := strings.Index(lines[i], "~~")
second := strings.Index(lines[i][first+2:], "~~")
if second < 0 {
break
}
second += first + 2
inner := lines[i][first+2 : second]
lines[i] = lines[i][:first] + "~" + inner + "~" + lines[i][second+2:]
}
// Links: [text](url) → <url|text>
for {
start := strings.Index(lines[i], "[")
@ -232,6 +248,102 @@ func markdownToMrkdwn(text string) string {
return strings.Join(lines, "\n")
}
// convertTables finds markdown tables and renders them as monospace blocks.
// Input: | Col A | Col B |
// |-------|-------|
// | val1 | val2 |
// Output: ```
// Col A Col B
// val1 val2
// ```
func convertTables(text string) string {
lines := strings.Split(text, "\n")
var result []string
i := 0
for i < len(lines) {
// Detect table start: line with | and next line is separator |---|
if strings.Contains(lines[i], "|") && i+1 < len(lines) && isTableSeparator(lines[i+1]) {
// Collect all table rows
var headers []string
var rows [][]string
headers = parseTableRow(lines[i])
i += 2 // skip header + separator
for i < len(lines) && strings.Contains(lines[i], "|") && !isTableSeparator(lines[i]) {
rows = append(rows, parseTableRow(lines[i]))
i++
}
// Calculate column widths
colWidths := make([]int, len(headers))
for j, h := range headers {
if len(h) > colWidths[j] {
colWidths[j] = len(h)
}
}
for _, row := range rows {
for j, cell := range row {
if j < len(colWidths) && len(cell) > colWidths[j] {
colWidths[j] = len(cell)
}
}
}
// Render as monospace block
result = append(result, "```")
headerLine := ""
for j, h := range headers {
headerLine += padRight(h, colWidths[j]) + " "
}
result = append(result, strings.TrimRight(headerLine, " "))
// Separator
sepLine := ""
for j := range headers {
sepLine += strings.Repeat("-", colWidths[j]) + " "
}
result = append(result, strings.TrimRight(sepLine, " "))
for _, row := range rows {
rowLine := ""
for j, cell := range row {
if j < len(colWidths) {
rowLine += padRight(cell, colWidths[j]) + " "
}
}
result = append(result, strings.TrimRight(rowLine, " "))
}
result = append(result, "```")
} else {
result = append(result, lines[i])
i++
}
}
return strings.Join(result, "\n")
}
func isTableSeparator(line string) bool {
trimmed := strings.TrimSpace(line)
return strings.Contains(trimmed, "|") && strings.Contains(trimmed, "---")
}
func parseTableRow(line string) []string {
line = strings.TrimSpace(line)
line = strings.Trim(line, "|")
parts := strings.Split(line, "|")
var cells []string
for _, p := range parts {
cells = append(cells, strings.TrimSpace(p))
}
return cells
}
func padRight(s string, width int) string {
if len(s) >= width {
return s
}
return s + strings.Repeat(" ", width-len(s))
}
func slackSplitMessage(text string, maxLen int) []string {
if len(text) <= maxLen {
return []string{text}