Merge pull request #851 from Molecule-AI/fix/slack-mrkdwn-formatting
fix(slack): convert Markdown → mrkdwn before posting
This commit is contained in:
commit
2d9083b155
@ -81,6 +81,9 @@ func (s *SlackAdapter) sendBotMessage(ctx context.Context, config map[string]int
|
||||
username, _ := config["username"].(string)
|
||||
iconEmoji, _ := config["icon_emoji"].(string)
|
||||
|
||||
// Convert Markdown → Slack mrkdwn before sending
|
||||
text = markdownToMrkdwn(text)
|
||||
|
||||
// Split long messages at newline boundaries
|
||||
chunks := slackSplitMessage(text, 3000)
|
||||
for _, chunk := range chunks {
|
||||
@ -152,6 +155,73 @@ func (s *SlackAdapter) sendWebhookMessage(ctx context.Context, config map[string
|
||||
return nil
|
||||
}
|
||||
|
||||
// markdownToMrkdwn converts standard Markdown to Slack's mrkdwn format.
|
||||
// Agents output standard MD (Claude Code default); Slack renders mrkdwn.
|
||||
//
|
||||
// MD **bold** → mrkdwn *bold*
|
||||
// MD __italic__ or *italic* (standalone) → mrkdwn _italic_
|
||||
// MD ### heading → mrkdwn *heading* (bold, no heading syntax in Slack)
|
||||
// MD [text](url) → mrkdwn <url|text>
|
||||
// MD --- → mrkdwn ———
|
||||
// MD > quote → mrkdwn > quote (same, works as-is)
|
||||
// MD `code` → mrkdwn `code` (same)
|
||||
// MD ```block``` → mrkdwn ```block``` (same)
|
||||
func markdownToMrkdwn(text string) string {
|
||||
lines := strings.Split(text, "\n")
|
||||
for i, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
|
||||
// Headings: ### Text → *Text*
|
||||
if strings.HasPrefix(trimmed, "#") {
|
||||
heading := strings.TrimLeft(trimmed, "# ")
|
||||
if heading != "" {
|
||||
lines[i] = "*" + heading + "*"
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Horizontal rules
|
||||
if trimmed == "---" || trimmed == "***" || trimmed == "___" {
|
||||
lines[i] = "———"
|
||||
continue
|
||||
}
|
||||
|
||||
// Links: [text](url) → <url|text>
|
||||
for {
|
||||
start := strings.Index(lines[i], "[")
|
||||
if start < 0 {
|
||||
break
|
||||
}
|
||||
mid := strings.Index(lines[i][start:], "](")
|
||||
if mid < 0 {
|
||||
break
|
||||
}
|
||||
mid += start
|
||||
end := strings.Index(lines[i][mid+2:], ")")
|
||||
if end < 0 {
|
||||
break
|
||||
}
|
||||
end += mid + 2
|
||||
linkText := lines[i][start+1 : mid]
|
||||
url := lines[i][mid+2 : end]
|
||||
lines[i] = lines[i][:start] + "<" + url + "|" + linkText + ">" + lines[i][end+1:]
|
||||
}
|
||||
|
||||
// Bold: **text** → *text* (Slack bold is single asterisk)
|
||||
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:]
|
||||
}
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func slackSplitMessage(text string, maxLen int) []string {
|
||||
if len(text) <= maxLen {
|
||||
return []string{text}
|
||||
|
||||
@ -113,3 +113,56 @@ func TestSlackAdapter_DisplayName(t *testing.T) {
|
||||
t.Errorf("expected 'Slack', got %q", a.DisplayName())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdownToMrkdwn_Bold(t *testing.T) {
|
||||
got := markdownToMrkdwn("This is **bold** text")
|
||||
if got != "This is *bold* text" {
|
||||
t.Errorf("expected *bold*, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdownToMrkdwn_Heading(t *testing.T) {
|
||||
got := markdownToMrkdwn("### Security Findings")
|
||||
if got != "*Security Findings*" {
|
||||
t.Errorf("expected *Security Findings*, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdownToMrkdwn_Link(t *testing.T) {
|
||||
got := markdownToMrkdwn("See [PR #800](https://github.com/org/repo/pull/800)")
|
||||
if got != "See <https://github.com/org/repo/pull/800|PR #800>" {
|
||||
t.Errorf("expected Slack link, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdownToMrkdwn_HorizontalRule(t *testing.T) {
|
||||
got := markdownToMrkdwn("above\n---\nbelow")
|
||||
if got != "above\n———\nbelow" {
|
||||
t.Errorf("expected ———, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdownToMrkdwn_CodeBlockUntouched(t *testing.T) {
|
||||
input := "```go\nfunc main() {}\n```"
|
||||
got := markdownToMrkdwn(input)
|
||||
if got != input {
|
||||
t.Errorf("code block should be untouched, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdownToMrkdwn_Mixed(t *testing.T) {
|
||||
input := "## Summary\n\n**3 PRs** merged. See [details](https://example.com).\n\n---\n\nDone."
|
||||
got := markdownToMrkdwn(input)
|
||||
if !strings.Contains(got, "*Summary*") {
|
||||
t.Error("heading not converted")
|
||||
}
|
||||
if !strings.Contains(got, "*3 PRs*") {
|
||||
t.Error("bold not converted")
|
||||
}
|
||||
if !strings.Contains(got, "<https://example.com|details>") {
|
||||
t.Error("link not converted")
|
||||
}
|
||||
if !strings.Contains(got, "———") {
|
||||
t.Error("hr not converted")
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user