fix(editor): render CJK-adjacent emphasis in Markdown preview

This commit is contained in:
Neil
2026-09-18 19:15:45 -07:00
committed by GitHub
parent d8cfe374a3
commit 56e5cd5e34
6 changed files with 164 additions and 2 deletions
+1
View File
@@ -289,6 +289,7 @@
"rehype-sanitize": "^6.0.0",
"rehype-slug": "^6.0.0",
"remark-breaks": "^4.0.0",
"remark-cjk-friendly": "^2.3.1",
"remark-frontmatter": "^5.0.0",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
+81
View File
@@ -472,6 +472,9 @@ importers:
remark-breaks:
specifier: ^4.0.0
version: 4.0.0
remark-cjk-friendly:
specifier: ^2.3.1
version: 2.3.1(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(unified@11.0.5)
remark-frontmatter:
specifier: ^5.0.0
version: 5.0.0(supports-color@7.2.0)
@@ -5724,6 +5727,15 @@ packages:
mdast-util-to-hast@13.2.1:
resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==}
mdast-util-to-markdown-cjk-friendly@1.0.0:
resolution: {integrity: sha512-BoaAm8mlJ+LAYz0Qs532Y3ciTuQYgBUPZcSFbvC/ZKmEMAKgulw84YvQK1gI34t/vL2euSfuaWlqczkTBgamkw==}
engines: {node: '>=18'}
peerDependencies:
'@types/mdast': '*'
peerDependenciesMeta:
'@types/mdast':
optional: true
mdast-util-to-markdown@2.1.2:
resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==}
@@ -5751,6 +5763,25 @@ packages:
micromark-core-commonmark@2.0.3:
resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==}
micromark-extension-cjk-friendly-util@3.0.1:
resolution: {integrity: sha512-GcbXqTTHOsiZHyF753oIddP/J2eH8j9zpyQPhkof6B2JNxfEJabnQqxbCgzJNuNes0Y2jTNJ3LiYPSXr6eJA8w==}
engines: {node: '>=18'}
peerDependencies:
micromark-util-types: '*'
peerDependenciesMeta:
micromark-util-types:
optional: true
micromark-extension-cjk-friendly@2.0.1:
resolution: {integrity: sha512-OkzoYVTL1ChbvQ8Cc1ayTIz7paFQz8iS9oIYmewncweUSwmWR+hkJF9spJ1lxB90XldJl26A1F4IkPOKS3bDXw==}
engines: {node: '>=18'}
peerDependencies:
micromark: ^4.0.0
micromark-util-types: ^2.0.0
peerDependenciesMeta:
micromark-util-types:
optional: true
micromark-extension-frontmatter@2.0.0:
resolution: {integrity: sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==}
@@ -6534,6 +6565,16 @@ packages:
remark-breaks@4.0.0:
resolution: {integrity: sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==}
remark-cjk-friendly@2.3.1:
resolution: {integrity: sha512-f+pKZRxCRwNEGFBKNRAZAqU91GIK1SAo3ZyFHWRUgC9zcxRR0BXKd6YwqgSsxtW0rNpUDtONj7H5nje2WL3fcA==}
engines: {node: '>=18'}
peerDependencies:
'@types/mdast': ^4.0.0
unified: ^11.0.0
peerDependenciesMeta:
'@types/mdast':
optional: true
remark-frontmatter@5.0.0:
resolution: {integrity: sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==}
@@ -12632,6 +12673,16 @@ snapshots:
unist-util-visit: 5.1.0
vfile: 6.0.3
mdast-util-to-markdown-cjk-friendly@1.0.0(@types/mdast@4.0.4)(micromark-util-types@2.0.2):
dependencies:
mdast-util-to-markdown: 2.1.2
micromark-extension-cjk-friendly-util: 3.0.1(micromark-util-types@2.0.2)
micromark-util-symbol: 2.0.1
optionalDependencies:
'@types/mdast': 4.0.4
transitivePeerDependencies:
- micromark-util-types
mdast-util-to-markdown@2.1.2:
dependencies:
'@types/mdast': 4.0.4
@@ -12700,6 +12751,25 @@ snapshots:
micromark-util-symbol: 2.0.1
micromark-util-types: 2.0.2
micromark-extension-cjk-friendly-util@3.0.1(micromark-util-types@2.0.2):
dependencies:
get-east-asian-width: 1.5.0
micromark-util-character: 2.1.1
micromark-util-symbol: 2.0.1
optionalDependencies:
micromark-util-types: 2.0.2
micromark-extension-cjk-friendly@2.0.1(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0)):
dependencies:
devlop: 1.1.0
micromark: 4.0.2(supports-color@7.2.0)
micromark-extension-cjk-friendly-util: 3.0.1(micromark-util-types@2.0.2)
micromark-util-chunked: 2.0.1
micromark-util-resolve-all: 2.0.1
micromark-util-symbol: 2.0.1
optionalDependencies:
micromark-util-types: 2.0.2
micromark-extension-frontmatter@2.0.0:
dependencies:
fault: 2.0.1
@@ -13768,6 +13838,17 @@ snapshots:
mdast-util-newline-to-break: 2.0.0
unified: 11.0.5
remark-cjk-friendly@2.3.1(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(unified@11.0.5):
dependencies:
mdast-util-to-markdown-cjk-friendly: 1.0.0(@types/mdast@4.0.4)(micromark-util-types@2.0.2)
micromark-extension-cjk-friendly: 2.0.1(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))
unified: 11.0.5
optionalDependencies:
'@types/mdast': 4.0.4
transitivePeerDependencies:
- micromark
- micromark-util-types
remark-frontmatter@5.0.0(supports-color@7.2.0):
dependencies:
'@types/mdast': 4.0.4
@@ -0,0 +1,67 @@
import { renderToStaticMarkup } from 'react-dom/server'
import remarkCjkFriendly from 'remark-cjk-friendly/parseOnly'
import remarkGfm from 'remark-gfm'
import remarkParse from 'remark-parse'
import { unified } from 'unified'
import { describe, expect, it } from 'vitest'
import { MarkdownPreviewBody } from './MarkdownPreviewBody'
const cjkEmphasisExamples = [
{
name: 'Korean text after a quoted phrase',
markdown: '**"이런"**것은 강조됩니다.',
expectedHtml: '<strong>&quot;이런&quot;</strong>것은 강조됩니다.'
},
{
name: 'Korean text after parentheses',
markdown: '**(강조)**입니다.',
expectedHtml: '<strong>(강조)</strong>입니다.'
},
{
name: 'Korean text on both delimiter boundaries',
markdown: '문장은**"여기"**에서 이어집니다.',
expectedHtml: '문장은<strong>&quot;여기&quot;</strong>에서 이어집니다.'
},
{
name: 'Japanese text after corner brackets',
markdown: '**「強調」**です。',
expectedHtml: '<strong>「強調」</strong>です。'
},
{
name: 'Chinese text after quotation marks',
markdown: '**“强调”**文本',
expectedHtml: '<strong>“强调”</strong>文本'
}
]
const unchangedMarkdownExamples = [
'# Heading',
'**bold** text and *italic* text',
'foo_bar_baz',
'`**literal**`',
'[link](https://example.com)',
'- [x] completed task',
'| left | right |\n| --- | --- |\n| a | b |',
'~~deleted~~ text',
'~~"삭제"~~문장'
]
function parseMarkdown(markdown: string) {
return unified().use(remarkParse).use(remarkGfm).parse(markdown)
}
function parseCjkFriendlyMarkdown(markdown: string) {
return unified().use(remarkParse).use(remarkGfm).use(remarkCjkFriendly).parse(markdown)
}
describe('MarkdownPreviewBody', () => {
it.each(cjkEmphasisExamples)('renders emphasis for $name', ({ markdown, expectedHtml }) => {
const html = renderToStaticMarkup(<MarkdownPreviewBody content={markdown} components={{}} />)
expect(html).toContain(expectedHtml)
})
it.each(unchangedMarkdownExamples)('keeps existing parsing for %s', (markdown) => {
expect(parseCjkFriendlyMarkdown(markdown)).toEqual(parseMarkdown(markdown))
})
})
@@ -7,6 +7,7 @@ import rehypeRaw from 'rehype-raw'
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize'
import rehypeSlug from 'rehype-slug'
import remarkBreaks from 'remark-breaks'
import remarkCjkFriendly from 'remark-cjk-friendly/parseOnly'
import remarkFrontmatter from 'remark-frontmatter'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
@@ -55,6 +56,7 @@ const markdownPreviewSanitizeSchema = {
type MarkdownPluginList = NonNullable<ReactMarkdownOptions['remarkPlugins']>
const MARKDOWN_REMARK_PLUGINS: MarkdownPluginList = [
remarkGfm,
remarkCjkFriendly,
remarkBreaks,
remarkFrontmatter,
remarkMath,
@@ -98,6 +98,15 @@ describe('markdown table of contents', () => {
])
})
it('extracts emphasized headings next to Korean text', () => {
const toc = buildMarkdownTableOfContents('# **"이런"**것은 강조됩니다')
expect(toc[0]).toMatchObject({
id: '이런것은-강조됩니다',
title: '"이런"것은 강조됩니다'
})
})
it('uses GitHub-compatible duplicate slugs', () => {
const toc = buildMarkdownTableOfContents('# Repeat\n# Repeat')
@@ -1,3 +1,4 @@
import remarkCjkFriendly from 'remark-cjk-friendly/parseOnly'
import remarkFrontmatter from 'remark-frontmatter'
import remarkGfm from 'remark-gfm'
import remarkParse from 'remark-parse'
@@ -112,7 +113,7 @@ function appendTocItem(stack: MarkdownTocItem[], item: MarkdownTocItem): void {
}
type MarkdownAstNode = {
alt?: string
alt?: string | null
children?: MarkdownAstNode[]
depth?: number
type?: string
@@ -142,8 +143,9 @@ export function buildMarkdownTableOfContents(markdown: string): MarkdownTocItem[
const tree = unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkCjkFriendly)
.use(remarkFrontmatter, ['yaml', 'toml'])
.parse(markdown) as MarkdownAstNode
.parse(markdown)
function visit(node: MarkdownAstNode): void {
if (