fix(markdown): keep CJK punctuation out of bare-URL autolinks

marked's GFM url tokenizer swallows any non-space text after a bare URL,
including fullwidth punctuation and Chinese annotations written directly
after it ("https://x.com/docs(说明)了解更多"). Switch the autolink
tokenizer to marked-linkify-it, which treats Unicode punctuation as a URL
boundary, so fullwidth parens and CJK punctuation are trimmed naturally.
This commit is contained in:
gaojunran
2026-08-21 13:10:42 +08:00
parent 0d70a631f6
commit 19c5cc1f1e
4 changed files with 47 additions and 6 deletions
@@ -187,3 +187,30 @@ describe('Markdown images', () => {
expect(html).not.toContain('data-openchamber-markdown-image');
});
});
describe('CJK-aware link parsing', () => {
const hrefOf = (html: string): string | null => /<a\b[^>]*href="([^"]*)"/.exec(html)?.[1] ?? null;
test('bare URL followed by a CJK annotation trims the annotation from the href', () => {
const html = renderMarkdownSync('访问 https://example.com/docs(中文说明)了解更多');
expect(hrefOf(html)).toBe('https://example.com/docs');
});
test('bare URL followed by CJK punctuation trims the punctuation', () => {
expect(hrefOf(renderMarkdownSync('地址 https://example.com/guide,详见'))).toBe(
'https://example.com/guide',
);
expect(hrefOf(renderMarkdownSync('官网 https://example.com。'))).toBe('https://example.com');
});
test('correct links are unaffected', () => {
expect(hrefOf(renderMarkdownSync('官方文档见 [这里](https://docs.example.com)(中文说明)'))).toBe(
'https://docs.example.com',
);
expect(hrefOf(renderMarkdownSync('[下载](https://dl.example.com/安装包(正式版))'))).toBe(
'https://dl.example.com/安装包(正式版)',
);
expect(hrefOf(renderMarkdownSync('[a](url(1))'))).toBe('url(1)');
expect(hrefOf(renderMarkdownSync('[a](url "title")'))).toBe('url');
});
});
@@ -1,4 +1,5 @@
import { Marked, marked, type Tokens } from 'marked';
import markedLinkifyIt from 'marked-linkify-it';
import remend from 'remend';
import katex from 'katex';
import DOMPurify from 'dompurify';
@@ -321,10 +322,15 @@ const blockMathExtension = {
},
};
const createParser = (imageMode: MarkdownImageMode) => new Marked().use({
gfm: true,
breaks: false,
extensions: [inlineMathExtension, blockMathExtension],
// marked's GFM autolink swallows CJK punctuation after a bare URL, so switch
// to marked-linkify-it, which treats Unicode punctuation as a URL boundary.
// Plain CJK characters right after a URL are still consumed, matching GitHub.
const createParser = (imageMode: MarkdownImageMode) => new Marked().use(
markedLinkifyIt({ fuzzyLink: false }),
{
gfm: true,
breaks: false,
extensions: [inlineMathExtension, blockMathExtension],
renderer: {
// Assistant output is untrusted. Markdown constructs still render as HTML,
// but raw HTML must remain visible text so it cannot introduce active DOM