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' : ''}`) ||
''}
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' : ''}`) ||
''}
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 2a08e9c5b..b405fd37b 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 b0bad2cdc..22ac5cd48 100644
--- a/src/app/plugins/markdown/markdownToHtml.ts
+++ b/src/app/plugins/markdown/markdownToHtml.ts
@@ -9,6 +9,7 @@ import {
} from './extensions/matrix-math';
import { matrixSubscriptExtension } from './extensions/matrix-subscript';
import { matrixEmoticonExtension, preprocessEmoticon } from './extensions/matrix-emoticon';
+import { matrixUnderlineExtension } from './extensions/matrix-underline';
import {
escapeLineStartBlockquoteWithoutFollowingSpace,
unescapeMarkdownInlineSequences,
@@ -18,6 +19,7 @@ import {
const processor = marked.use({
breaks: true,
extensions: [
+ matrixUnderlineExtension,
matrixSpoilerExtension,
matrixMathExtension,
matrixMathBlockExtension,