diff --git a/src/app/components/editor/output.test.ts b/src/app/components/editor/output.test.ts
index 72b5ac033..44a2dddb6 100644
--- a/src/app/components/editor/output.test.ts
+++ b/src/app/components/editor/output.test.ts
@@ -28,3 +28,52 @@ describe('toMatrixCustomHTML emoticons', () => {
expect(html).toContain('height="32"');
});
});
+
+describe('toMatrixCustomHTML matrix.to', () => {
+ it('serializes room mentions as raw matrix.to URL text, not an anchor', () => {
+ const html = trimCustomHtml(
+ toMatrixCustomHTML(
+ [
+ {
+ type: BlockType.Paragraph,
+ children: [
+ {
+ type: BlockType.Mention,
+ id: '!room:example.org',
+ name: 'My room',
+ children: [{ text: '' }],
+ } as never,
+ ],
+ } as never,
+ ],
+ {}
+ )
+ );
+
+ expect(html).toContain('https://matrix.to/#/!room:example.org');
+ expect(html).not.toMatch(/]*matrix\.to/i);
+ });
+
+ it('serializes matrix.to links as raw URL text, not an anchor', () => {
+ const html = trimCustomHtml(
+ toMatrixCustomHTML(
+ [
+ {
+ type: BlockType.Paragraph,
+ children: [
+ {
+ type: BlockType.Link,
+ href: 'https://matrix.to/#/@alice:example.org',
+ children: [{ text: 'Alice' }],
+ } as never,
+ ],
+ } as never,
+ ],
+ {}
+ )
+ );
+
+ expect(html).toContain('https://matrix.to/#/@alice:example.org');
+ expect(html).not.toMatch(/]*matrix\.to/i);
+ });
+});
diff --git a/src/app/components/editor/output.ts b/src/app/components/editor/output.ts
index 074edd6cd..513b66a00 100644
--- a/src/app/components/editor/output.ts
+++ b/src/app/components/editor/output.ts
@@ -8,7 +8,7 @@ import { isUserId } from '$utils/matrix';
import type { CustomElement } from './slate';
import { BlockType } from './types';
import { getMarkdownCodeSpanRanges, isInsideMarkdownCodeSpan } from './utils';
-import { MATRIX_TO_BASE } from '$plugins/matrix-to';
+import { MATRIX_TO_BASE, testMatrixTo } from '$plugins/matrix-to';
export type OutputOptions = {
/**
@@ -38,8 +38,8 @@ const elementToCustomHtml = (node: CustomElement, children: string): string => {
fragment += `?${node.viaServers.map((server) => `via=${server}`).join('&')}`;
}
- const matrixTo = `https://matrix.to/#/${fragment}`;
- return `${sanitizeText(node.name)}`;
+ const matrixTo = `${MATRIX_TO_BASE}#/${fragment}`;
+ return sanitizeText(matrixTo);
}
case BlockType.Emoticon:
return node.key.startsWith('mxc://')
@@ -48,7 +48,9 @@ const elementToCustomHtml = (node: CustomElement, children: string): string => {
)}" title="${sanitizeText(node.shortcode)}" height="32" />`
: sanitizeText(node.key);
case BlockType.Link:
- return `${children}`;
+ return testMatrixTo(node.href)
+ ? sanitizeText(node.href)
+ : `${children}`;
case BlockType.Command:
return `/${sanitizeText(node.command)}`;
default:
diff --git a/src/app/plugins/markdown/markdownToHtml.test.ts b/src/app/plugins/markdown/markdownToHtml.test.ts
index 20ea8186e..580c7ab19 100644
--- a/src/app/plugins/markdown/markdownToHtml.test.ts
+++ b/src/app/plugins/markdown/markdownToHtml.test.ts
@@ -223,4 +223,21 @@ describe('markdownToHtml', () => {
const result = markdownToHtml(html);
expect(result).not.toContain('?');
});
+
+ it('keeps normal markdown links valid when many bare matrix.to URLs are shielded', () => {
+ const matrixLines = Array.from(
+ { length: 12 },
+ (_, i) => `https://matrix.to/#/@u${i}:example.org`
+ ).join('\n');
+ const md = `${matrixLines}\n[docs](https://example.com/doc)`;
+ const result = markdownToHtml(md);
+ expect(result).toContain(' {
+ const result = markdownToHtml('join https://matrix.to/#/#room:example.org please');
+ expect(result).toContain('https://matrix.to/#/#room:example.org');
+ expect(result).not.toMatch(/]*matrix\.to/);
+ });
});
diff --git a/src/app/plugins/markdown/markdownToHtml.ts b/src/app/plugins/markdown/markdownToHtml.ts
index c58c0a22e..f18aae62a 100644
--- a/src/app/plugins/markdown/markdownToHtml.ts
+++ b/src/app/plugins/markdown/markdownToHtml.ts
@@ -77,8 +77,10 @@ const shieldBareMatrixToLinks = (
const unshieldBareMatrixToLinks = (html: string, placeholders: Map): string => {
let result = html;
- for (const [key, url] of placeholders.entries()) {
- result = result.split(key).join(escapeHtml(url));
+ const keys = [...placeholders.keys()].sort((a, b) => b.length - a.length);
+ for (const key of keys) {
+ const url = placeholders.get(key);
+ if (url) result = result.split(key).join(escapeHtml(url));
}
return result;
};