Skip to content

Commit 497f8e5

Browse files
authored
Add underline to markdown (#761)
<!-- Please read https://github.com/SableClient/Sable/blob/dev/CONTRIBUTING.md before submitting your pull request --> ### Description <!-- Please include a summary of the change. Please also include relevant motivation and context. List any dependencies that are required for this change. --> Adds underline using `__` also fixed a typo in the pins that resulted in text like `pinned ... andunpinned ...`, now there's spaces around and. #### Type of change - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] This change requires a documentation update ### Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings ### AI disclosure: - [x] Partially AI assisted (clarify which code was AI assisted and briefly explain what it does). - [ ] Fully AI generated (explain what all the generated code does in moderate detail). <!-- Write any explanation required here, but do not generate the explanation using AI!! You must prove you understand what the code in this PR does. --> Tests were updated with AI.
2 parents 000e9ba + 0996def commit 497f8e5

10 files changed

Lines changed: 69 additions & 6 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
default: patch
3+
---
4+
5+
Added the ability to **underline** using `__underscores__`.

src/app/components/message/Reply.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ export const Reply = as<'div', ReplyProps>(
231231
{(pinsAdded?.length > 0 &&
232232
`pinned ${pinsAdded.length} message${pinsAdded.length > 1 ? 's' : ''}`) ||
233233
''}
234-
{(pinsAdded?.length > 0 && pinsRemoved?.length > 0 && `and`) || ''}
234+
{(pinsAdded?.length > 0 && pinsRemoved?.length > 0 && ` and `) || ''}
235235
{(pinsRemoved?.length > 0 &&
236236
`unpinned ${pinsRemoved.length} message${pinsRemoved.length > 1 ? 's' : ''}`) ||
237237
''}

src/app/hooks/timeline/useTimelineEventRenderer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1074,7 +1074,7 @@ export function useTimelineEventRenderer({
10741074
{(pinsAdded?.length > 0 &&
10751075
`pinned ${pinsAdded.length} message${pinsAdded.length > 1 ? 's' : ''}`) ||
10761076
''}
1077-
{(pinsAdded?.length > 0 && pinsRemoved?.length > 0 && ` and`) || ''}
1077+
{(pinsAdded?.length > 0 && pinsRemoved?.length > 0 && ` and `) || ''}
10781078
{(pinsRemoved?.length > 0 &&
10791079
`unpinned ${pinsRemoved.length} message${pinsRemoved.length > 1 ? 's' : ''}`) ||
10801080
''}

src/app/plugins/markdown/bidirectional.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ describe('bidirectional round-trip', () => {
2929
expect(result).toContain('*italic text*');
3030
});
3131

32+
it('round-trips underline', () => {
33+
const markdown = '__underlined__';
34+
const html = markdownToHtml(markdown);
35+
const injected = injectDataMd(html);
36+
const result = htmlToMarkdown(injected);
37+
expect(result).toContain('__underlined__');
38+
});
39+
3240
it('round-trips inline code', () => {
3341
const markdown = '`inline code`';
3442
const html = markdownToHtml(markdown);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { TokenizerExtension, RendererExtension, Tokens } from 'marked';
2+
3+
// Underline extension: __text__
4+
export const matrixUnderlineExtension = {
5+
name: 'matrixUnderline',
6+
level: 'inline',
7+
start(src: string) {
8+
return src.indexOf('__');
9+
},
10+
tokenizer(
11+
this: {
12+
lexer: { inlineTokens: (t: string, tokens: Tokens.Generic[]) => void };
13+
},
14+
src: string
15+
) {
16+
if (!src.startsWith('__')) return undefined;
17+
const rule = /^__(.+?)__/;
18+
const match = rule.exec(src);
19+
if (match) {
20+
const token = {
21+
type: 'matrixUnderline',
22+
raw: match[0],
23+
text: match[1],
24+
tokens: [] as Tokens.Generic[],
25+
};
26+
this.lexer.inlineTokens(token.text!, token.tokens);
27+
return token;
28+
}
29+
return undefined;
30+
},
31+
renderer(
32+
this: { parser: { parseInline: (tokens: Tokens.Generic[]) => string } },
33+
token: Tokens.Generic
34+
) {
35+
const tokens = (token as { tokens: Tokens.Generic[] }).tokens || [];
36+
return `<u data-md="__">${this.parser.parseInline(tokens)}</u>`;
37+
},
38+
} satisfies TokenizerExtension & RendererExtension;

src/app/plugins/markdown/htmlToMarkdown.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,10 @@ function processNode(node: ChildNode, listDepth: number = 0): string {
117117
case 'i':
118118
return processInlineWrapper(node, '*');
119119

120-
case 'u':
121-
return processInlineWrapper(node, '_');
120+
case 'u': {
121+
const md = node.attribs['data-md'];
122+
return processInlineWrapper(node, md ?? '__');
123+
}
122124

123125
case 's':
124126
case 'del':

src/app/plugins/markdown/injectDataMd.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ describe('injectDataMd', () => {
5656

5757
it('injects data-md into u tags', () => {
5858
const result = injectDataMd('<u>underline</u>');
59-
expect(result).toContain('data-md="_"');
59+
expect(result).toContain('data-md="__"');
6060
});
6161

6262
it('injects data-md into s tags', () => {

src/app/plugins/markdown/injectDataMd.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export function injectDataMd(html: string): string {
7575
// Inject inline markdown markers for underline
7676
html = html.replace(/<u([^>]*)>([^<]*)<\/u>/g, (_, attrs, content) => {
7777
if (attrs.includes('data-md')) return `<u${attrs}>${content}</u>`;
78-
return `<u data-md="_"${attrs}>${content}</u>`;
78+
return `<u data-md="__"${attrs}>${content}</u>`;
7979
});
8080

8181
// Inject inline markdown markers for strikethrough

src/app/plugins/markdown/markdownToHtml.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ describe('markdownToHtml', () => {
1313
expect(result).toContain('<strong>bold</strong>');
1414
});
1515

16+
it('converts __ to underline, not bold', () => {
17+
const result = markdownToHtml('__underlined__');
18+
expect(result).toContain('<u');
19+
expect(result).toContain('data-md="__"');
20+
expect(result).toContain('>underlined<');
21+
expect(result).not.toContain('<strong>underlined</strong>');
22+
});
23+
1624
it('converts italic text', () => {
1725
const result = markdownToHtml('*italic*');
1826
expect(result).toContain('<em>italic</em>');

src/app/plugins/markdown/markdownToHtml.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from './extensions/matrix-math';
1010
import { matrixSubscriptExtension } from './extensions/matrix-subscript';
1111
import { matrixEmoticonExtension, preprocessEmoticon } from './extensions/matrix-emoticon';
12+
import { matrixUnderlineExtension } from './extensions/matrix-underline';
1213
import {
1314
escapeLineStartBlockquoteWithoutFollowingSpace,
1415
unescapeMarkdownInlineSequences,
@@ -18,6 +19,7 @@ import {
1819
const processor = marked.use({
1920
breaks: true,
2021
extensions: [
22+
matrixUnderlineExtension,
2123
matrixSpoilerExtension,
2224
matrixMathExtension,
2325
matrixMathBlockExtension,

0 commit comments

Comments
 (0)