组合记号:叠在字母上的点和圈
é 可以是单个预组合码点 U+00E9,也可以是 e 加组合急音符 U+0301 两个码点。后一种写法里,那个符号自己不占位,而是叠到前一个字符上——这类字符叫 combining mark(组合记号,gc 为 \p{M})。它们能无限叠加,是 Zalgo
文字的原理;多个记号并存时的次序由 canonical combining class 规定;而且容易与长得像、却独立占一格的 Sk 修饰符号混淆。本页与 归一化、符号四子类 互为补充。
1 · 三个子类
\p{M} 切成三个子类,每个码点恰属一个。逐码点实测(Unicode 17.0):\p{M} 共 2543 个,其中 Mn 2059 个、Mc 471 个、Me 只有 13 个。日常见到的变音符、阿拉伯与希伯来的元音点、各种声调几乎全是 Mn。
| 子类 | 含义 | 占不占位 | 例 |
|---|---|---|---|
Mn |
Nonspacing,非占位,叠在基字符上 | 不占位 | ◌́ ◌̈ ◌̣ |
Mc |
Spacing Combining,占位组合,多见于印度系文字的元音符 | 占位 | ा ी |
Me |
Enclosing,把基字符圈起来 | 包围 | ◌⃝ ◌⃣ |
警示 · ^(U+005E)、´(U+00B4)这类是 Sk(Modifier Symbol,全 Unicode 125 个),自己独立占一格,不会叠到别的字符上;而 ◌̂(U+0302)、◌́(U+0301)是
Mn,一定叠上去。两者字形几乎一样,归类完全不同,键盘上打出来的多半是前者。详见 符号四子类。
2 · 叠加与字素簇
一个基字符后面可以跟任意多个组合记号,不管叠几个,它们与基字符始终是一个字素簇,也就是用户眼里的一个字符,但 code point 数一直在涨。这正是 字素簇 那页说的「一个字符等于一串码点」在拉丁字母上的形态,与 emoji 那几页的 ZWJ 序列同构。
3 · Zalgo
组合记号没有数量上限。往每个字符上随机堆几十个 Mn,字形就会向上下溢出、糊成一团,这就是 Zalgo 文字。它本身无害,仍是普通文本,但能撑爆行高,也能绕过只统计可见字符的长度校验:一段 5 个字素簇的 Zalgo 串,码点数可以是几百。
4 · Canonical combining class
一个基字符挂多个记号时,书写次序可以不同:先打上点再打下点,与先打下点再打上点,是两串不同的码点序列。Unicode 给每个记号一个 canonical combining class(ccc)数值,规定一个标准次序,归一化时按它重排——上点 U+0307 的 ccc 是 230,下点 U+0323 是
220,数值小的排前面,于是两种写法归一化后都变成 q + U+0323 + U+0307,判等随即成立。
重排之后才轮到合成,而合成是逐个记号试的,不是把整串塞进一个码点。e + 急音符 + 下点经 normalize('NFC') 得到的是 U+1EB9 加 U+0301 两个码点:下点与 e 合成了预组合的 ẹ,急音符则因为没有对应的预组合码点而留在外面。
建议 · 要剥掉所有变音记号,先 normalize('NFD') 把预组合码点拆开,再 replace(/\p{M}/gu, '') 删掉记号;只做后一步会漏掉写成单码点的那些。限长与截断一律按字素簇而非码点,否则会把记号与基字符切散,留下一串孤儿记号——这与
受控输入 页按 grapheme 限长是同一套思路。
5 · 参考文献
- Unicode. UAX #15: Unicode Normalization Forms. canonical ordering 与四种归一化形式的规范,本页第 4 节的重排与合成行为照它。unicode.org
- Unicode. UAX #44: Unicode Character Database.
General_Category的Mn/Mc/Me定义与Canonical_Combining_Class的取值表(上点 230、下点 220 即出自此)。unicode.org - MDN. String.prototype.normalize(). 四种形式的调用方式与常见用法。developer.mozilla.org
- Unicode. Combining Diacritical Marks(U+0300–U+036F). 本页记号池取自这一区,图表给出每个记号的名称与叠放位置。unicode.org