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; };