From 6ac4049bab3e952cfd71ef72fe651073a9568aea Mon Sep 17 00:00:00 2001 From: 7w1 Date: Wed, 6 May 2026 17:32:47 -0500 Subject: [PATCH 1/3] add underline with __ --- .../plugins/markdown/bidirectional.test.ts | 8 ++++ .../markdown/extensions/matrix-underline.ts | 38 +++++++++++++++++++ src/app/plugins/markdown/htmlToMarkdown.ts | 6 ++- src/app/plugins/markdown/injectDataMd.test.ts | 2 +- src/app/plugins/markdown/injectDataMd.ts | 2 +- .../plugins/markdown/markdownToHtml.test.ts | 8 ++++ src/app/plugins/markdown/markdownToHtml.ts | 2 + 7 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 src/app/plugins/markdown/extensions/matrix-underline.ts diff --git a/src/app/plugins/markdown/bidirectional.test.ts b/src/app/plugins/markdown/bidirectional.test.ts index dff56737c..56b8384f9 100644 --- a/src/app/plugins/markdown/bidirectional.test.ts +++ b/src/app/plugins/markdown/bidirectional.test.ts @@ -29,6 +29,14 @@ describe('bidirectional round-trip', () => { expect(result).toContain('*italic text*'); }); + it('round-trips underline', () => { + const markdown = '__underlined__'; + const html = markdownToHtml(markdown); + const injected = injectDataMd(html); + const result = htmlToMarkdown(injected); + expect(result).toContain('__underlined__'); + }); + it('round-trips inline code', () => { const markdown = '`inline code`'; const html = markdownToHtml(markdown); diff --git a/src/app/plugins/markdown/extensions/matrix-underline.ts b/src/app/plugins/markdown/extensions/matrix-underline.ts new file mode 100644 index 000000000..8f7a0b2ed --- /dev/null +++ b/src/app/plugins/markdown/extensions/matrix-underline.ts @@ -0,0 +1,38 @@ +import type { TokenizerExtension, RendererExtension, Tokens } from 'marked'; + +// Underline extension: __text__ +export const matrixUnderlineExtension = { + name: 'matrixUnderline', + level: 'inline', + start(src: string) { + return src.indexOf('__'); + }, + tokenizer( + this: { + lexer: { inlineTokens: (t: string, tokens: Tokens.Generic[]) => void }; + }, + src: string + ) { + if (!src.startsWith('__')) return undefined; + const rule = /^__(.+?)__/; + const match = rule.exec(src); + if (match) { + const token = { + type: 'matrixUnderline', + raw: match[0], + text: match[1], + tokens: [] as Tokens.Generic[], + }; + this.lexer.inlineTokens(token.text!, token.tokens); + return token; + } + return undefined; + }, + renderer( + this: { parser: { parseInline: (tokens: Tokens.Generic[]) => string } }, + token: Tokens.Generic + ) { + const tokens = (token as { tokens: Tokens.Generic[] }).tokens || []; + return `${this.parser.parseInline(tokens)}`; + }, +} satisfies TokenizerExtension & RendererExtension; diff --git a/src/app/plugins/markdown/htmlToMarkdown.ts b/src/app/plugins/markdown/htmlToMarkdown.ts index dfdb16fca..26283f514 100644 --- a/src/app/plugins/markdown/htmlToMarkdown.ts +++ b/src/app/plugins/markdown/htmlToMarkdown.ts @@ -117,8 +117,10 @@ function processNode(node: ChildNode, listDepth: number = 0): string { case 'i': return processInlineWrapper(node, '*'); - case 'u': - return processInlineWrapper(node, '_'); + case 'u': { + const md = node.attribs['data-md']; + return processInlineWrapper(node, md ?? '__'); + } case 's': case 'del': diff --git a/src/app/plugins/markdown/injectDataMd.test.ts b/src/app/plugins/markdown/injectDataMd.test.ts index 2572c9e20..c29f30196 100644 --- a/src/app/plugins/markdown/injectDataMd.test.ts +++ b/src/app/plugins/markdown/injectDataMd.test.ts @@ -56,7 +56,7 @@ describe('injectDataMd', () => { it('injects data-md into u tags', () => { const result = injectDataMd('underline'); - expect(result).toContain('data-md="_"'); + expect(result).toContain('data-md="__"'); }); it('injects data-md into s tags', () => { diff --git a/src/app/plugins/markdown/injectDataMd.ts b/src/app/plugins/markdown/injectDataMd.ts index e27cfdb7b..ca1403650 100644 --- a/src/app/plugins/markdown/injectDataMd.ts +++ b/src/app/plugins/markdown/injectDataMd.ts @@ -75,7 +75,7 @@ export function injectDataMd(html: string): string { // Inject inline markdown markers for underline html = html.replace(/]*)>([^<]*)<\/u>/g, (_, attrs, content) => { if (attrs.includes('data-md')) return `${content}`; - return `${content}`; + return `${content}`; }); // Inject inline markdown markers for strikethrough diff --git a/src/app/plugins/markdown/markdownToHtml.test.ts b/src/app/plugins/markdown/markdownToHtml.test.ts index 24a7592ae..670d27277 100644 --- a/src/app/plugins/markdown/markdownToHtml.test.ts +++ b/src/app/plugins/markdown/markdownToHtml.test.ts @@ -13,6 +13,14 @@ describe('markdownToHtml', () => { expect(result).toContain('bold'); }); + it('converts __ to underline, not bold', () => { + const result = markdownToHtml('__underlined__'); + expect(result).toContain('underlined<'); + expect(result).not.toContain('underlined'); + }); + it('converts italic text', () => { const result = markdownToHtml('*italic*'); expect(result).toContain('italic'); diff --git a/src/app/plugins/markdown/markdownToHtml.ts b/src/app/plugins/markdown/markdownToHtml.ts index 02338da9e..697623fb1 100644 --- a/src/app/plugins/markdown/markdownToHtml.ts +++ b/src/app/plugins/markdown/markdownToHtml.ts @@ -4,12 +4,14 @@ import { matrixSpoilerExtension } from './extensions/matrix-spoiler'; import { matrixMathExtension, matrixMathBlockExtension } from './extensions/matrix-math'; import { matrixSubscriptExtension } from './extensions/matrix-subscript'; import { matrixEmoticonExtension, preprocessEmoticon } from './extensions/matrix-emoticon'; +import { matrixUnderlineExtension } from './extensions/matrix-underline'; import { unescapeMarkdownBlockSequences, unescapeMarkdownInlineSequences } from './utils'; // Configure marked with Matrix extensions const processor = marked.use({ breaks: true, extensions: [ + matrixUnderlineExtension, matrixSpoilerExtension, matrixMathExtension, matrixMathBlockExtension, From 8cd927ae37c0e0e127dc2eb72f9555415a248cf6 Mon Sep 17 00:00:00 2001 From: 7w1 Date: Wed, 6 May 2026 17:38:59 -0500 Subject: [PATCH 2/3] changeset and fix unrelated typo in pins text --- .changeset/fix-underline-markdown.md | 5 +++++ src/app/components/message/Reply.tsx | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-underline-markdown.md diff --git a/.changeset/fix-underline-markdown.md b/.changeset/fix-underline-markdown.md new file mode 100644 index 000000000..7a3b3d3c6 --- /dev/null +++ b/.changeset/fix-underline-markdown.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +Added the ability to **underline** using `__underscores__`. diff --git a/src/app/components/message/Reply.tsx b/src/app/components/message/Reply.tsx index 46537cd4d..1c2fd8ddb 100644 --- a/src/app/components/message/Reply.tsx +++ b/src/app/components/message/Reply.tsx @@ -231,7 +231,7 @@ export const Reply = as<'div', ReplyProps>( {(pinsAdded?.length > 0 && `pinned ${pinsAdded.length} message${pinsAdded.length > 1 ? 's' : ''}`) || ''} - {(pinsAdded?.length > 0 && pinsRemoved?.length > 0 && `and`) || ''} + {(pinsAdded?.length > 0 && pinsRemoved?.length > 0 && ` and `) || ''} {(pinsRemoved?.length > 0 && `unpinned ${pinsRemoved.length} message${pinsRemoved.length > 1 ? 's' : ''}`) || ''} From 099ca05f4ce1fe2a45bb42d869684a5ae06e9108 Mon Sep 17 00:00:00 2001 From: 7w1 Date: Wed, 6 May 2026 17:39:54 -0500 Subject: [PATCH 3/3] other typo spot --- src/app/hooks/timeline/useTimelineEventRenderer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/hooks/timeline/useTimelineEventRenderer.tsx b/src/app/hooks/timeline/useTimelineEventRenderer.tsx index 3ceba3c9f..131f7f457 100644 --- a/src/app/hooks/timeline/useTimelineEventRenderer.tsx +++ b/src/app/hooks/timeline/useTimelineEventRenderer.tsx @@ -1074,7 +1074,7 @@ export function useTimelineEventRenderer({ {(pinsAdded?.length > 0 && `pinned ${pinsAdded.length} message${pinsAdded.length > 1 ? 's' : ''}`) || ''} - {(pinsAdded?.length > 0 && pinsRemoved?.length > 0 && ` and`) || ''} + {(pinsAdded?.length > 0 && pinsRemoved?.length > 0 && ` and `) || ''} {(pinsRemoved?.length > 0 && `unpinned ${pinsRemoved.length} message${pinsRemoved.length > 1 ? 's' : ''}`) || ''}