diff --git a/src/telegram/format.ts b/src/telegram/format.ts index 1a0b9c0a583..aec5f9f2257 100644 --- a/src/telegram/format.ts +++ b/src/telegram/format.ts @@ -216,6 +216,20 @@ export function wrapFileReferencesInHtml(html: string): string { return `${prefix}${escapeHtml(filename)}`; }); + // Second pass: catch orphaned single-letter TLD patterns (e.g., 'D.md' in 'R&D.md') + // These can be auto-linked by Telegram as domains + const orphanedTldPattern = new RegExp( + `([^a-zA-Z0-9]|^)([A-Za-z]\\.(?:${extensionsPattern}))(?=[^a-zA-Z0-9/]|$)`, + "g", + ); + result = result.replace(orphanedTldPattern, (m, prefix, tld) => { + // Skip if already wrapped in a tag (check for < before or > after in context) + if (prefix === ">") { + return m; + } + return `${prefix}${escapeHtml(tld)}`; + }); + return result; } diff --git a/src/telegram/format.wrap-md.test.ts b/src/telegram/format.wrap-md.test.ts index 0b2b61ac9fd..5cc41e4c414 100644 --- a/src/telegram/format.wrap-md.test.ts +++ b/src/telegram/format.wrap-md.test.ts @@ -278,12 +278,20 @@ describe("edge cases", () => { expect(wrapFileReferencesInHtml(input)).toBe(input); }); - it("does not match filenames with special characters", () => { - // The regex only matches [a-zA-Z0-9_.\\-./] so & breaks the pattern + it("wraps orphaned TLD pattern after special character", () => { + // R&D.md - the & breaks the main pattern, but D.md could be auto-linked + // So we wrap the orphaned D.md part to prevent Telegram linking it const input = "R&D.md"; const result = wrapFileReferencesInHtml(input); - // Not wrapped because & is not in the allowed character class - expect(result).toBe(input); + expect(result).toBe("R&D.md"); + }); + + it("wraps orphaned single-letter TLD patterns", () => { + const result1 = wrapFileReferencesInHtml("X.ai is cool"); + expect(result1).toContain("X.ai"); + + const result2 = wrapFileReferencesInHtml("Check R.io"); + expect(result2).toContain("R.io"); }); it("does not match filenames containing angle brackets", () => {