在 markdown 中使用中文符号

本文最后更新于:2024年2月16日 上午

问题

最近再次碰到 markdown 对中文的支持问题,由于这个问题已经长期存在,所以想谈一下现状和解决方法。当同时使用中文符号和粗体/斜体时,就会出现问题。

例如

**真没想到我这么快就要死了。**她有些自暴自弃地想着。

会被渲染为

**真没想到我这么快就要死了。**她有些自暴自弃地想着。

实际上,这种情况还有很多,几乎所有中文符号都无法被正常渲染。

示例 渲染
**真,**她 **真,**她
**真。**她 **真。**她
**真、**她 **真、**她
**真;**她 **真;**她
**真:**她 **真:**她
**真?**她 **真?**她
**真!**她 **真!**她
**真“**她 **真“**她
**真”**她 **真”**她
**真‘**她 **真‘**她
**真’**她 **真’**她
**真(**她 **真(**她
**真)**她 **真)**她
**真【**她 **真【**她
**真】**她 **真】**她
**真《**她 **真《**她
**真》**她 **真》**她
**真—**她 **真—**她
**真~**她 **真~**她
**真…**她 **真…**她
**真·**她 **真·**她
**真〃**她 **真〃**她
**真-**她 **真-**她
**真々**她 真々

这种情况有多常见呢?

根据吾辈之前维护同人小说的经验,在一本 80w 字的小说中出现了 2700 次以上,可以在《魔法少女小圆 飞向星空》的项目中搜索 /\*\*.*?[,。、;:?!“”‘’()【】《》—~…·〃-々]\*\* / 找到真实的用例。

解决方法

那么,如何解决呢?现在可以通过加空格、零宽度字符或者将符号移动到粗体外面。

示例 渲染
**真,** 她 真,
**真,**​她 真,​她
**真**,她 ,她

第一种方法会呈现出非预期的渲染,第二种方法输入很困难,最后一种方法则是必须改变输入,和第一种有类似的问题。

例如 docusaurus 就推荐了第一种方法,并结合 markdown 解析插件来自动清理粗体后面的多余空格。原本 **真,** 她 会渲染为 <p><strong>真,</strong> 她</p>,但通过插件处理后会变成 <p><strong>真,</strong>她</p>

吾辈也为 mdast(remark 底层库) 实现过一个类似的插件,参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { Transform } from 'mdast-util-from-markdown'

export function clearStrongAfterSpace(): Transform {
return (root) => {
visit(root, (it) => {
if (it.type === 'paragraph') {
const children = (it as Paragraph).children
children.forEach((it, i) => {
if (it.type === 'strong') {
const next = children[i + 1]
const s = (it.children[0] as Text).value
if (s) {
const last = s.slice(s.length - 1)
if (
next &&
next.type === 'text' &&
',。、;:?!“”‘’()【】《》—~…·〃-々'
.split('')
.includes(last) &&
next.value.startsWith(' ')
) {
next.value = next.value.trim()
}
}
}
})
}
})
return root
}
}

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { toHast } from 'mdast-util-to-hast'
import { toHtml } from 'hast-util-to-html'
import { fromMarkdown } from 'mdast-util-from-markdown'
import { clearStrongAfterSpace } from '../cjk'

const render = (s: string) =>
toHtml(
toHast(
fromMarkdown(s, {
mdastExtensions: [
{
transforms: [clearStrongAfterSpace()],
},
],
}),
)!,
)

console.log(render('**真,** 她')) // `<p><strong>真,</strong>她</p>`

吾辈也为 markdown-it 实现了这个插件,参考:https://github.com/mark-magic/mark-magic/blob/77f9b1571a7d96847fc39e0aa8504ed994a64b71/packages/plugin-docs/src/assets/config.ts#L13-L42

进展

尽管上面的解决方法还算不错,但对于使用 markdown 编写大量内容的人而言,它仍然不够直观。尤其是不检查渲染结果就不可能知道是否忘记添加了额外的空格,这在上面提到的小说中非常明显,当内容增加到一定程度时,这种检查就变得非常烦人。尤其是从外部来源(转换)得到一个 markdown 时,这可能会非常常见。

commonmark 官方目前又开始在推进这个问题了,距离问题最初提出已经过去了 4 年,想要关注进展可以关注 issue commonmark/commonmark-spec#650

尽管官方规范如果定义,那么问题将会极大的解决。但考虑到规范的定义和实现是漫长的,所以目前吾辈也在尝试编写 mdast 插件自行处理解析部分,以处理上面提到的那个中文符号示例列表。相关代码参考: https://github.com/rxliuli/liuli-tools/blob/82d81eeb0d661ad1338d04d1a75f173764dea9bc/packages/markdown-util/src/cjk.ts#L46-L77

总结

尽管 markdown 对 cjk 的支持有诸多不顺,但 markdown 作为一种开放的文本格式仍然很棒,所有文本数据采用 markdown 并根据需要分发为不同的格式似乎是最好的。


在 markdown 中使用中文符号
https://blog.rxliuli.com/p/2029b35ae4094a48a3073f998f10af9c/
作者
rxliuli
发布于
2024年2月16日
许可协议