diff --git a/src/telegram/format.ts b/src/telegram/format.ts index 2972acb9845..501b6ac776f 100644 --- a/src/telegram/format.ts +++ b/src/telegram/format.ts @@ -222,14 +222,17 @@ export function wrapFileReferencesInHtml(html: string): string { `([^a-zA-Z0-9]|^)([A-Za-z]\\.(?:${extensionsPattern}))(?=[^a-zA-Z0-9/]|$)`, "g", ); - result = result.replace(orphanedTldPattern, (m, prefix, tld, offset) => { + // Snapshot for offset calculations (offset is relative to pre-replacement string) + // Note: replace() doesn't mutate, but snapshot makes intent explicit + const snapshot = result; + result = snapshot.replace(orphanedTldPattern, (m, prefix, tld, offset) => { // Skip if prefix is > (right after a tag close) if (prefix === ">") { return m; } // Skip if we're inside an HTML tag (between < and >) - const lastOpen = result.lastIndexOf("<", offset); - const lastClose = result.lastIndexOf(">", offset); + const lastOpen = snapshot.lastIndexOf("<", offset); + const lastClose = snapshot.lastIndexOf(">", offset); if (lastOpen > lastClose) { return m; // Inside a tag } diff --git a/src/telegram/format.wrap-md.test.ts b/src/telegram/format.wrap-md.test.ts index 413b91c1576..96b7628f71d 100644 --- a/src/telegram/format.wrap-md.test.ts +++ b/src/telegram/format.wrap-md.test.ts @@ -341,4 +341,22 @@ describe("edge cases", () => { const result = wrapFileReferencesInHtml(input); expect(result).toBe(input); }); + + it("handles multiple orphaned TLDs with HTML tags (offset stability)", () => { + // This tests the bug where offset is relative to pre-replacement string + // but we were checking against the mutating result string + const input = 'link B.md text D.py'; + const result = wrapFileReferencesInHtml(input); + // A.md in href should NOT be wrapped (inside attribute) + // B.md outside tags SHOULD be wrapped + // C.sh in title attribute should NOT be wrapped + // D.py outside tags SHOULD be wrapped + expect(result).toContain("B.md"); + expect(result).toContain("D.py"); + expect(result).not.toContain("A.md"); + expect(result).not.toContain("C.sh"); + // Attributes should be unchanged + expect(result).toContain('href="http://A.md"'); + expect(result).toContain('title="C.sh"'); + }); });