diff --git a/src/telegram/format.ts b/src/telegram/format.ts index f82921fa5fb..777ac3aaec6 100644 --- a/src/telegram/format.ts +++ b/src/telegram/format.ts @@ -20,7 +20,57 @@ function escapeHtmlAttr(text: string): string { return escapeHtml(text).replace(/"/g, """); } -function buildTelegramLink(link: MarkdownLinkSpan, _text: string) { +/** + * File extensions that share TLDs and commonly appear in code/documentation. + * These are wrapped in tags to prevent Telegram from generating + * spurious domain registrar previews. + */ +const FILE_EXTENSIONS_WITH_TLD = new Set([ + // High priority - commonly referenced in messages + "md", // Markdown (Moldova) + "go", // Go language + "py", // Python (Paraguay) + "pl", // Perl (Poland) + "ai", // Adobe Illustrator (Anguilla) + "sh", // Shell (Saint Helena) + // Medium priority - sometimes referenced + "io", // Tuvalu (often used for tech projects) + "tv", // Tuvalu (video files) + "fm", // Federated States of Micronesia (audio) + "am", // Armenia + "at", // Austria + "be", // Belgium + "cc", // Cocos Islands + "co", // Colombia +]); + +/** Detects when markdown-it linkify auto-generated a link from a bare filename (e.g. README.md → http://README.md) */ +function isAutoLinkedFileRef(href: string, label: string): boolean { + const stripped = href.replace(/^https?:\/\//i, ""); + if (stripped !== label) { + return false; + } + const dotIndex = label.lastIndexOf("."); + if (dotIndex < 1) { + return false; + } + const ext = label.slice(dotIndex + 1).toLowerCase(); + if (!FILE_EXTENSIONS_WITH_TLD.has(ext)) { + return false; + } + // Reject if any path segment before the filename contains a dot (looks like a domain) + const segments = label.split("/"); + if (segments.length > 1) { + for (let i = 0; i < segments.length - 1; i++) { + if (segments[i].includes(".")) { + return false; + } + } + } + return true; +} + +function buildTelegramLink(link: MarkdownLinkSpan, text: string) { const href = link.href.trim(); if (!href) { return null; @@ -28,6 +78,11 @@ function buildTelegramLink(link: MarkdownLinkSpan, _text: string) { if (link.start === link.end) { return null; } + // Suppress auto-linkified file references (e.g. README.md → http://README.md) + const label = text.slice(link.start, link.end); + if (isAutoLinkedFileRef(href, label)) { + return null; + } const safeHref = escapeHtmlAttr(href); return { start: link.start, @@ -69,30 +124,6 @@ export function markdownToTelegramHtml( return html; } -/** - * File extensions that share TLDs and commonly appear in code/documentation. - * These are wrapped in tags to prevent Telegram from generating - * spurious domain registrar previews. - */ -const FILE_EXTENSIONS_WITH_TLD = new Set([ - // High priority - commonly referenced in messages - "md", // Markdown (Moldova) - "go", // Go language - "py", // Python (Paraguay) - "pl", // Perl (Poland) - "ai", // Adobe Illustrator (Anguilla) - "sh", // Shell (Saint Helena) - // Medium priority - sometimes referenced - "io", // Tuvalu (often used for tech projects) - "tv", // Tuvalu (video files) - "fm", // Federated States of Micronesia (audio) - "am", // Armenia - "at", // Austria - "be", // Belgium - "cc", // Cocos Islands - "co", // Colombia -]); - /** * Wraps standalone file references (with TLD extensions) in tags. * This prevents Telegram from treating them as URLs and generating @@ -104,6 +135,18 @@ const FILE_EXTENSIONS_WITH_TLD = new Set([ export function wrapFileReferencesInHtml(html: string): string { // Build regex pattern for all tracked extensions const extensionsPattern = Array.from(FILE_EXTENSIONS_WITH_TLD).join("|"); + + // Safety-net: de-linkify auto-generated anchors where href="http://