/** * Converts markdown text to Telegram-compatible HTML. * * Handles: * - Fenced code blocks →
...
* - Inline code → ...
* - Bold **text** → text
* - Italic *text* → text
* - Strikethrough ~~text~~ → `;
} else {
html += "";
}
html += escapeHtml(codeContent);
html += "
\n";
codeLang = "";
}
continue;
}
if (inCodeBlock) {
if (codeContent) codeContent += "\n";
codeContent += line;
continue;
}
// Normal line — convert inline markdown
html += convertInlineMarkdown(line) + "\n";
}
// If code block was never closed, dump it anyway
if (inCodeBlock) {
html += "";
html += escapeHtml(codeContent);
html += "
\n";
}
return html.trimEnd();
}
function convertInlineMarkdown(line: string): string {
// Escape HTML first in non-code parts
// We need to be careful: process inline code first, then escape the rest
// Extract inline code spans to protect them
const codeSpans: string[] = [];
let processed = line.replace(/`([^`]+)`/g, (_match, code) => {
const idx = codeSpans.length;
codeSpans.push(`${escapeHtml(code)}`);
return `\x00CODE${idx}\x00`;
});
// Now escape HTML in the remaining text
processed = escapeHtml(processed);
// Bold **text** or __text__
processed = processed.replace(/\*\*(.+?)\*\*/g, "$1");
processed = processed.replace(/__(.+?)__/g, "$1");
// Italic *text* or _text_ (but not inside words for underscore)
processed = processed.replace(/\*(.+?)\*/g, "$1");
// Strikethrough ~~text~~
processed = processed.replace(/~~(.+?)~~/g, "$1");
// Links [text](url)
processed = processed.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1');
// Restore inline code spans
processed = processed.replace(/\x00CODE(\d+)\x00/g, (_match, idx) => {
return codeSpans[parseInt(idx, 10)];
});
return processed;
}
/**
* Split a message into chunks that fit Telegram's 4096 char limit.
* Tries to split at newlines when possible.
*/
export function splitForTelegram(html: string): string[] {
if (html.length <= TELEGRAM_MAX_LENGTH) {
return [html];
}
const chunks: string[] = [];
let remaining = html;
while (remaining.length > 0) {
if (remaining.length <= TELEGRAM_MAX_LENGTH) {
chunks.push(remaining);
break;
}
// Reserve space for truncation notice on the first chunk if needed
const maxChunk = TELEGRAM_MAX_LENGTH - TRUNCATION_NOTICE.length;
// Try to find a newline to split at
let splitAt = remaining.lastIndexOf("\n", maxChunk);
if (splitAt <= 0) {
// No good newline, just split at max
splitAt = maxChunk;
}
const chunk = remaining.slice(0, splitAt);
chunks.push(chunk);
remaining = remaining.slice(splitAt).trimStart();
}
// Add truncation notice to last chunk if we split
if (chunks.length > 1) {
const last = chunks[chunks.length - 1];
if (last.length + TRUNCATION_NOTICE.length > TELEGRAM_MAX_LENGTH) {
// Trim last chunk to fit the notice
chunks[chunks.length - 1] = last.slice(0, TELEGRAM_MAX_LENGTH - TRUNCATION_NOTICE.length) + TRUNCATION_NOTICE;
} else {
chunks[chunks.length - 1] = last + TRUNCATION_NOTICE;
}
}
return chunks;
}