From 2570d8fa5cf2df8a869ee7ba88d68822e03e4e6a Mon Sep 17 00:00:00 2001
From: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com>
Date: Fri, 23 Oct 2026 15:25:16 +0000
Subject: [PATCH] fix(markdown): render CJK bold that ends in punctuation in
desktop and phone chats (#32020)
---
mobile/package.json | 1 +
mobile/pnpm-lock.yaml | 26 +++++++++++++++++++
.../components/mobile-markdown-tree.test.ts | 22 ++++++++++++++++
mobile/src/components/mobile-markdown-tree.ts | 8 +++---
.../sidebar/CommentMarkdown.test.tsx | 17 ++++++++++++
.../components/sidebar/CommentMarkdown.tsx | 2 ++
6 files changed, 73 insertions(+), 3 deletions(-)
create mode 100644 mobile/src/components/mobile-markdown-tree.test.ts
diff --git a/mobile/package.json b/mobile/package.json
index 2753365cae6..675ff465a19 100644
--- a/mobile/package.json
+++ b/mobile/package.json
@@ -54,6 +54,7 @@
"lowlight": "^3.3.0",
"lucide-react-native": "^1.14.0",
"markdown-it": "^14.3.2",
+ "markdown-it-cjk-friendly": "^2.0.3",
"markdown-it-footnote": "^4.0.0",
"mermaid": "11.17.2",
"react": "19.2.8",
diff --git a/mobile/pnpm-lock.yaml b/mobile/pnpm-lock.yaml
index faf82cf389f..dfbb6b5115d 100644
--- a/mobile/pnpm-lock.yaml
+++ b/mobile/pnpm-lock.yaml
@@ -279,6 +279,9 @@ importers:
markdown-it:
specifier: 14.3.3
version: 14.3.3
+ markdown-it-cjk-friendly:
+ specifier: ^2.0.3
+ version: 2.0.3(@types/markdown-it@14.1.2)(markdown-it@14.3.3)
markdown-it-footnote:
specifier: ^4.0.0
version: 4.0.0
@@ -4650,6 +4653,10 @@ packages:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
+ get-east-asian-width@1.7.0:
+ resolution: {integrity: sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==}
+ engines: {node: '>=18'}
+
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
@@ -5558,6 +5565,16 @@ packages:
makeerror@1.0.12:
resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==}
+ markdown-it-cjk-friendly@2.0.3:
+ resolution: {integrity: sha512-dBAhqp7GZ6he15ug2t5JSfwTw2KvZX9tt0y7Up0reR1cZOBUogSq5V859hcIGwge7hjW1SkUaWkNr++0HszyZg==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@types/markdown-it': '*'
+ markdown-it: 14.3.3
+ peerDependenciesMeta:
+ '@types/markdown-it':
+ optional: true
+
markdown-it-footnote@4.0.0:
resolution: {integrity: sha512-WYJ7urf+khJYl3DqofQpYfEYkZKbmXmwxQV8c8mO/hGIhgZ1wOe7R4HLFNwqx7TjILbnC98fuyeSsin19JdFcQ==}
@@ -12448,6 +12465,8 @@ snapshots:
get-caller-file@2.0.5: {}
+ get-east-asian-width@1.7.0: {}
+
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -13550,6 +13569,13 @@ snapshots:
dependencies:
tmpl: 1.0.5
+ markdown-it-cjk-friendly@2.0.3(@types/markdown-it@14.1.2)(markdown-it@14.3.3):
+ dependencies:
+ get-east-asian-width: 1.7.0
+ markdown-it: 14.3.3
+ optionalDependencies:
+ '@types/markdown-it': 14.1.2
+
markdown-it-footnote@4.0.0: {}
markdown-it@14.3.3:
diff --git a/mobile/src/components/mobile-markdown-tree.test.ts b/mobile/src/components/mobile-markdown-tree.test.ts
new file mode 100644
index 00000000000..382cef6fbf8
--- /dev/null
+++ b/mobile/src/components/mobile-markdown-tree.test.ts
@@ -0,0 +1,22 @@
+import { describe, expect, it } from 'vitest'
+import { parseMobileMarkdownTree } from './mobile-markdown-tree'
+
+function inlineNodes(source: string) {
+ const [paragraph] = parseMobileMarkdownTree(source, false).children
+ return paragraph?.type === 'paragraph' ? paragraph.children : []
+}
+
+describe('CJK emphasis in mobile markdown', () => {
+ it('closes bold that ends in punctuation right before CJK text', () => {
+ expect(
+ inlineNodes('**单算 Runtime 执行引擎,平均每题 29.78 秒。**边界是开始执行到产出结果。')
+ ).toContainEqual({
+ type: 'strong',
+ children: [{ type: 'text', value: '单算 Runtime 执行引擎,平均每题 29.78 秒。' }]
+ })
+ })
+
+ it('keeps CommonMark flanking rules between non-CJK characters', () => {
+ expect(inlineNodes('**Note.**Next')).toEqual([{ type: 'text', value: '**Note.**Next' }])
+ })
+})
diff --git a/mobile/src/components/mobile-markdown-tree.ts b/mobile/src/components/mobile-markdown-tree.ts
index 54bc12f67fd..15aea826e31 100644
--- a/mobile/src/components/mobile-markdown-tree.ts
+++ b/mobile/src/components/mobile-markdown-tree.ts
@@ -1,4 +1,5 @@
import MarkdownIt from 'markdown-it'
+import cjkFriendly from 'markdown-it-cjk-friendly'
import footnote from 'markdown-it-footnote'
import type {
AlignType,
@@ -14,9 +15,10 @@ import remend from 'remend'
import { normalizeMobileMarkdownPreviewHtml } from './mobile-markdown-preview-html'
import { endLinkifyUrlsAtProsePunctuation } from './markdown-linkify-prose-boundary'
-// CommonMark with GitHub's tables, strikethrough, autolinks and footnotes: the rules desktop's
-// renderer follows, read by a parser fast enough for the phone's JavaScript engine.
-const markdown = new MarkdownIt({ html: true, linkify: true }).use(footnote)
+// CommonMark with GitHub's tables, strikethrough, autolinks, footnotes and CJK-friendly
+// emphasis: the rules desktop's renderer follows, read by a parser fast enough for the phone's
+// JavaScript engine.
+const markdown = new MarkdownIt({ html: true, linkify: true }).use(footnote).use(cjkFriendly)
// Every href parses as a link, file: included; routeMarkdownHref decides what a tap opens.
markdown.validateLink = () => true
endLinkifyUrlsAtProsePunctuation(markdown.linkify)
diff --git a/src/renderer/src/components/sidebar/CommentMarkdown.test.tsx b/src/renderer/src/components/sidebar/CommentMarkdown.test.tsx
index 364d4d93e5a..c7e613ddcaf 100644
--- a/src/renderer/src/components/sidebar/CommentMarkdown.test.tsx
+++ b/src/renderer/src/components/sidebar/CommentMarkdown.test.tsx
@@ -333,6 +333,23 @@ function renderComment(content: string, variant: 'compact' | 'document' = 'docum
return renderToStaticMarkup()
}
+describe('CJK emphasis', () => {
+ it('closes bold that ends in punctuation right before CJK text', () => {
+ expect(
+ renderComment('**单算 Runtime 执行引擎,平均每题 29.78 秒。**边界是开始执行到产出结果。')
+ ).toContain(
+ '单算 Runtime 执行引擎,平均每题 29.78 秒。边界是开始执行到产出结果。'
+ )
+ })
+
+ it('keeps CommonMark flanking rules between non-CJK characters', () => {
+ const markup = renderComment('**Note.**Next')
+
+ expect(markup).not.toContain('')
+ expect(markup).toContain('**Note.**Next')
+ })
+})
+
describe('GitHub callouts', () => {
it.each(['compact', 'document'] as const)(
'renders a %s quote that opens with a marker as a titled callout',
diff --git a/src/renderer/src/components/sidebar/CommentMarkdown.tsx b/src/renderer/src/components/sidebar/CommentMarkdown.tsx
index ba7a87c2f11..089f62e2580 100644
--- a/src/renderer/src/components/sidebar/CommentMarkdown.tsx
+++ b/src/renderer/src/components/sidebar/CommentMarkdown.tsx
@@ -6,6 +6,7 @@ import {
import Markdown, { defaultUrlTransform, type Components } from 'react-markdown'
import remarkGfm from 'remark-gfm'
import remarkBreaks from 'remark-breaks'
+import remarkCjkFriendly from 'remark-cjk-friendly/parseOnly'
import rehypeRaw from 'rehype-raw'
import rehypeSanitize, { defaultSchema, type Options as SanitizeSchema } from 'rehype-sanitize'
import remend from 'remend'
@@ -83,6 +84,7 @@ const commentMarkdownFileUriUrlTransform: UrlTransform = (value, key, node) => {
// with existing plain-text comments that rely on newline formatting.
const remarkPlugins = [
remarkGfm,
+ remarkCjkFriendly,
remarkAutolinkLiteralProseBoundary,
remarkGitHubCallouts,
remarkBreaks