组合记号:叠在字母上的那些点和圈,以及 Zalgo
é 可以是一个预组合码点,也可以是 e + 组合急音符 U+0301 两个码点 —— 后者那个符号自己不占位,而是叠到前一个字符上。这类字符就是 combining mark(gc \p{M}),分三个子类 Mn / Mc / Me。它们能无限叠加(这就是 Zalgo 文字的原理),排序受 canonical combining class 约束,还容易和长得像、却独立占位的 Sk 修饰符号混淆。本页和
归一化、符号四子类互为补充。
三个子类:Mn / Mc / Me
gc 大类 \p{M}(Mark)切成三个子类,每个码点恰属一个。输入一个字符,看它命中哪个(组合记号在前面补一个 ◌ 占位才看得见):
| 子类 | 含义 | 占不占位 | 例 |
|---|---|---|---|
| Mn | Nonspacing,非占位(叠在基字符上) | 不占位 | ◌́ ◌̈ ◌̣(声调 / 变音) |
| Mc | Spacing Combining,占位组合(多见于印度系文字的元音符) | 占位 | ा ी(Devanagari 元音符) |
| Me | Enclosing,包围(把基字符圈起来) | 包围 | ◌⃝ ◌⃣(圈 / keycap) |
叠加:基字符 + 任意多个记号 = 一个字素簇
选一个基字符,往上面叠组合记号 —— 不管叠几个,它们和基字符一起仍是一个字素簇(用户眼里的一个字符),但 code point 数一直在涨。这正是 字素簇那页说的「一个字符 = 一串码点」:
code point 链
叠到失控:Zalgo
组合记号没有数量上限,往一个字符上随机堆几十个 Mn,字形就会向上下溢出、糊成一团 —— 这就是 Zalgo 文字。它本质无害(还是普通文本),但能撑爆行高、绕过只看「可见字符」的过滤。拖动强度看它怎么长出来:
8
顺序也要归一:canonical combining class
一个基字符挂多个记号时,记号的书写先后可以不同,但 Unicode 用每个记号的 canonical combining class(ccc)规定一个标准次序。同一个「上点 + 下点的 q」,先打上点还是先打下点,码点序列不同,normalize('NFC') 会按 ccc 把它们重排到同一序,于是判等成立 —— 这是
归一化那页的 canonical ordering:
^(U+005E)、´(U+00B4)这类是 Sk(Modifier Symbol),自己独立占一格、不叠到别的字符上;而 ◌̂(U+0302)、◌́(U+0301)是 Mn,会叠上去。两者字形像、归类全不同,详见
符号四子类那页。
normalize('NFD') 拆出组合记号,再 replace(/\p{M}/gu, '') 删掉;限长、截断一律按字素簇而非码点(否则会把记号和基字符切散)。这和
受控输入那页按 grapheme 限长是同一套思路。