Matrix: suppress auto-linked file refs

This commit is contained in:
Gustavo Madeira Santana
2026-03-12 08:35:05 +00:00
parent de2464d50f
commit cbc7cc00fe
2 changed files with 66 additions and 0 deletions

View File

@@ -14,6 +14,19 @@ describe("markdownToMatrixHtml", () => {
expect(html).toContain('<a href="https://example.com">docs</a>');
});
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("<b>nope</b>");
expect(html).toContain("&lt;b&gt;nope&lt;/b&gt;");

View File

@@ -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<NonNullable<typeof md.renderer.rules.link_open>>[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 ?? "");