Skip to content

Commit 8255be4

Browse files
committed
feat(channel): render Markdown as HTML in Telegram messages
Claude Code responses use Markdown formatting (bold, italic, code blocks, inline code) which Telegram displays as raw text by default. Add markdownToHtml() converter in TelegramAdapter that transforms standard Markdown to Telegram HTML, and send messages with parse_mode: 'HTML'. Code blocks are extracted first to prevent formatting replacements inside them.
1 parent 657fbf7 commit 8255be4

1 file changed

Lines changed: 43 additions & 1 deletion

File tree

packages/channel-connector/src/adapters/TelegramAdapter.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ export class TelegramAdapter implements ChannelAdapter {
5959
async sendMessage(chatId: string, text: string): Promise<void> {
6060
const chunks = chunkMessage(text, TELEGRAM_MAX_MESSAGE_LENGTH);
6161
for (const chunk of chunks) {
62-
await this.bot.telegram.sendMessage(chatId, chunk);
62+
const html = markdownToHtml(chunk);
63+
await this.bot.telegram.sendMessage(chatId, html, { parse_mode: 'HTML' });
6364
}
6465
}
6566

@@ -108,3 +109,44 @@ function chunkMessage(text: string, maxLen: number): string[] {
108109

109110
return chunks;
110111
}
112+
113+
function escapeHtml(text: string): string {
114+
return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
115+
}
116+
117+
/**
118+
* Convert standard Markdown to Telegram HTML.
119+
* Handles code blocks first to prevent formatting inside them.
120+
*/
121+
function markdownToHtml(text: string): string {
122+
const codeBlocks: string[] = [];
123+
const inlineCodes: string[] = [];
124+
125+
let result = text.replace(/```(\w*)\n?([\s\S]*?)```/g, (_, lang, code) => {
126+
const escaped = escapeHtml(code.trimEnd());
127+
const block = lang
128+
? `<pre><code class="language-${lang}">${escaped}</code></pre>`
129+
: `<pre><code>${escaped}</code></pre>`;
130+
codeBlocks.push(block);
131+
return `\x00CODE${codeBlocks.length - 1}\x00`;
132+
});
133+
134+
result = result.replace(/`([^`]+)`/g, (_, code) => {
135+
inlineCodes.push(`<code>${escapeHtml(code)}</code>`);
136+
return `\x00INLINE${inlineCodes.length - 1}\x00`;
137+
});
138+
139+
result = escapeHtml(result);
140+
141+
result = result
142+
.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>')
143+
.replace(/__(.+?)__/g, '<b>$1</b>')
144+
.replace(/\*(.+?)\*/g, '<i>$1</i>')
145+
.replace(/_(.+?)_/g, '<i>$1</i>')
146+
.replace(/~~(.+?)~~/g, '<s>$1</s>');
147+
148+
result = result.replace(/\x00CODE(\d+)\x00/g, (_, i) => codeBlocks[parseInt(i)]);
149+
result = result.replace(/\x00INLINE(\d+)\x00/g, (_, i) => inlineCodes[parseInt(i)]);
150+
151+
return result;
152+
}

0 commit comments

Comments
 (0)