diff --git a/internal/channels/slack/format.go b/internal/channels/slack/format.go index 41591ffe..b95389cb 100644 --- a/internal/channels/slack/format.go +++ b/internal/channels/slack/format.go @@ -113,8 +113,17 @@ func extractCodeBlocks(text string) (blocks []string, result string) { return nil, text } + // An odd number of ``` fences makes Split return an even number of parts; + // the final segment then follows an unpaired opening fence and must stay + // literal (it is re-appended verbatim below), so exclude it from the + // paired-scan loop instead of misclassifying it as a closed code block. + scanEnd := len(parts) + if scanEnd%2 == 0 { + scanEnd-- + } var sb strings.Builder - for i, part := range parts { + for i := 0; i < scanEnd; i++ { + part := parts[i] if i%2 == 1 { blocks = append(blocks, part) sb.WriteString(fmt.Sprintf("\x00CB%d\x00", len(blocks)-1)) diff --git a/internal/channels/slack/format_test.go b/internal/channels/slack/format_test.go index 273b4bdc..85234c51 100644 --- a/internal/channels/slack/format_test.go +++ b/internal/channels/slack/format_test.go @@ -318,6 +318,14 @@ func TestExtractCodeBlocks(t *testing.T) { input: "text\n```\ncode (no closing)", expectedCount: 0, }, + { + // A closed block followed by an unpaired opening fence: 3 fences -> + // 4 parts. Only the middle part is a real code block; the trailing + // segment must stay literal, not be counted (and re-emitted) as one. + name: "closed block then unpaired trailing fence", + input: "a```b```c```d", + expectedCount: 1, + }, { name: "empty code block", input: "```\n\n```",