From 814155c91924a77fac26ada6299927f08aeb1895 Mon Sep 17 00:00:00 2001 From: henkedk Date: Thu, 9 Apr 2026 18:48:05 +0200 Subject: [PATCH] fix(telegram): protect URLs with underscores from italic parsing (#785) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Extract bare URLs into placeholders before italic conversion to prevent `_text_` inside URLs from being wrapped in `` tags - Follows existing placeholder pattern (code blocks, inline code, mentions, tables) - Fixes #784 ## Tests - Bare URL with underscores — no italic tags - Clean URL without underscores — unchanged - Markdown link `[text](url_with_underscores)` — href preserved --- internal/channels/telegram/format.go | 17 ++++++++++ internal/channels/telegram/format_test.go | 39 +++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/internal/channels/telegram/format.go b/internal/channels/telegram/format.go index 5db344b1..e81bf411 100644 --- a/internal/channels/telegram/format.go +++ b/internal/channels/telegram/format.go @@ -61,6 +61,17 @@ func markdownToTelegramHTML(text string) string { inlineCodes := extractInlineCodes(text) text = inlineCodes.text + + // Extract and protect bare URLs from italic parsing. + // URLs with underscores (e.g. syngas_dailymail_2026_ai) get broken by + // the italic regex which matches _text_ patterns inside URLs. + var urlPlaceholders []string + reURL := regexp.MustCompile(`https?://[^\s<>\)\]]+`) + text = reURL.ReplaceAllStringFunc(text, func(s string) string { + idx := len(urlPlaceholders) + urlPlaceholders = append(urlPlaceholders, s) + return fmt.Sprintf("\x00URL%d\x00", idx) + }) // Strip markdown headers text = regexp.MustCompile(`(?m)^#{1,6}\s+(.+)$`).ReplaceAllString(text, "$1") @@ -115,6 +126,12 @@ func markdownToTelegramHTML(text string) string { // List items text = regexp.MustCompile(`(?m)^[-*]\s+`).ReplaceAllString(text, "• ") + // Restore bare URLs (protected from italic parsing above). + for i, u := range urlPlaceholders { + escaped := escapeHTML(u) + text = strings.ReplaceAll(text, fmt.Sprintf("\x00URL%d\x00", i), escaped) + } + // Restore inline code for i, code := range inlineCodes.codes { escaped := escapeHTML(code) diff --git a/internal/channels/telegram/format_test.go b/internal/channels/telegram/format_test.go index ed82395c..876074d8 100644 --- a/internal/channels/telegram/format_test.go +++ b/internal/channels/telegram/format_test.go @@ -182,3 +182,42 @@ func TestChunkHTML(t *testing.T) { }) } } + +func TestMarkdownToTelegramHTML_URLsWithUnderscores(t *testing.T) { + tests := []struct { + name string + input string + want string + deny string + }{ + { + name: "bare URL with underscores not broken by italic", + input: "Check https://pre.glomotra.dev/uk/syngas_dailymail_2026_ai/?fname=James here", + want: "https://pre.glomotra.dev/uk/syngas_dailymail_2026_ai/?fname=James", + deny: "", + }, + { + name: "URL without underscores unchanged", + input: "Visit https://example.com/path today", + want: "https://example.com/path", + }, + { + name: "markdown link with underscored URL preserved", + input: "[Click](https://example.com/a_b_c)", + want: `href="https://example.com/a_b_c"`, + deny: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := markdownToTelegramHTML(tt.input) + if !strings.Contains(got, tt.want) { + t.Errorf("expected %q in output, got: %s", tt.want, got) + } + if tt.deny != "" && strings.Contains(got, tt.deny) { + t.Errorf("unexpected %q in output, got: %s", tt.deny, got) + } + }) + } +}