diff --git a/extensions/matrix/src/matrix/format.test.ts b/extensions/matrix/src/matrix/format.test.ts index 4538c2792e2..c929514ee17 100644 --- a/extensions/matrix/src/matrix/format.test.ts +++ b/extensions/matrix/src/matrix/format.test.ts @@ -14,6 +14,19 @@ describe("markdownToMatrixHtml", () => { expect(html).toContain('docs'); }); + it("does not auto-link bare file references into external urls", () => { + const html = markdownToMatrixHtml("Check README.md and backup.sh"); + expect(html).toContain("README.md"); + expect(html).toContain("backup.sh"); + expect(html).not.toContain('href="http://README.md"'); + expect(html).not.toContain('href="http://backup.sh"'); + }); + + it("keeps real domains linked even when path segments look like filenames", () => { + const html = markdownToMatrixHtml("See https://docs.example.com/backup.sh"); + expect(html).toContain('href="https://docs.example.com/backup.sh"'); + }); + it("escapes raw HTML", () => { const html = markdownToMatrixHtml("nope"); expect(html).toContain("<b>nope</b>"); diff --git a/extensions/matrix/src/matrix/format.ts b/extensions/matrix/src/matrix/format.ts index 65ba822bd65..31bddcc5292 100644 --- a/extensions/matrix/src/matrix/format.ts +++ b/extensions/matrix/src/matrix/format.ts @@ -11,10 +11,63 @@ md.enable("strikethrough"); const { escapeHtml } = md.utils; +/** + * Keep bare file references like README.md from becoming external http:// links. + * Telegram already hardens this path; Matrix should not turn common code/docs + * filenames into clickable registrar-style URLs either. + */ +const FILE_EXTENSIONS_WITH_TLD = new Set(["md", "go", "py", "pl", "sh", "am", "at", "be", "cc"]); + +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; + } + const segments = label.split("/"); + if (segments.length > 1) { + for (let i = 0; i < segments.length - 1; i += 1) { + if (segments[i]?.includes(".")) { + return false; + } + } + } + return true; +} + +function shouldSuppressAutoLink( + tokens: Parameters>[0], + idx: number, +): boolean { + const token = tokens[idx]; + if (token?.type !== "link_open" || token.info !== "auto") { + return false; + } + const href = token.attrGet("href") ?? ""; + const label = tokens[idx + 1]?.type === "text" ? (tokens[idx + 1]?.content ?? "") : ""; + return Boolean(href && label && isAutoLinkedFileRef(href, label)); +} + md.renderer.rules.image = (tokens, idx) => escapeHtml(tokens[idx]?.content ?? ""); md.renderer.rules.html_block = (tokens, idx) => escapeHtml(tokens[idx]?.content ?? ""); md.renderer.rules.html_inline = (tokens, idx) => escapeHtml(tokens[idx]?.content ?? ""); +md.renderer.rules.link_open = (tokens, idx, _options, _env, self) => + shouldSuppressAutoLink(tokens, idx) ? "" : self.renderToken(tokens, idx, _options); +md.renderer.rules.link_close = (tokens, idx, _options, _env, self) => { + const openIdx = idx - 2; + if (openIdx >= 0 && shouldSuppressAutoLink(tokens, openIdx)) { + return ""; + } + return self.renderToken(tokens, idx, _options); +}; export function markdownToMatrixHtml(markdown: string): string { const rendered = md.render(markdown ?? "");