regex 总览待审核
字符的真面目 · 组合记号 \p{M}

组合记号:叠在字母上的那些点和圈,以及 Zalgo

é 可以是一个预组合码点,也可以是 e + 组合急音符 U+0301 两个码点 —— 后者那个符号自己不占位,而是到前一个字符上。这类字符就是 combining mark(gc \p{M}),分三个子类 Mn / Mc / Me。它们能无限叠加(这就是 Zalgo 文字的原理),排序受 canonical combining class 约束,还容易和长得像、却独立占位Sk 修饰符号混淆。本页和 归一化符号四子类互为补充。

三个子类:Mn / Mc / Me

gc 大类 \p{M}(Mark)切成三个子类,每个码点恰属一个。输入一个字符,看它命中哪个(组合记号在前面补一个 占位才看得见):

子类含义占不占位
MnNonspacing,非占位(叠在基字符上)不占位◌́ ◌̈ ◌̣(声调 / 变音)
McSpacing Combining,占位组合(多见于印度系文字的元音符)占位ा ी(Devanagari 元音符)
MeEnclosing,包围(把基字符圈起来)包围◌⃝ ◌⃣(圈 / keycap)
非占位的 Mn 最常见(变音符、阿拉伯 / 希伯来元音点、各种声调);Me 极少。

叠加:基字符 + 任意多个记号 = 一个字素簇

选一个基字符,往上面叠组合记号 —— 不管叠几个,它们和基字符一起仍是一个字素簇(用户眼里的一个字符),但 code point 数一直在涨。这正是 字素簇那页说的「一个字符 = 一串码点」:

code point 链

叠到失控:Zalgo

组合记号没有数量上限,往一个字符上随机堆几十个 Mn,字形就会向上下溢出、糊成一团 —— 这就是 Zalgo 文字。它本质无害(还是普通文本),但能撑爆行高、绕过只看「可见字符」的过滤。拖动强度看它怎么长出来:

8

顺序也要归一:canonical combining class

一个基字符挂多个记号时,记号的书写先后可以不同,但 Unicode 用每个记号的 canonical combining class(ccc)规定一个标准次序。同一个「上点 + 下点的 q」,先打上点还是先打下点,码点序列不同,normalize('NFC') 会按 ccc 把它们重排到同一序,于是判等成立 —— 这是 归一化那页的 canonical ordering:

Sk 不是组合记号:^(U+005E)、´(U+00B4)这类是 Sk(Modifier Symbol),自己独立占一格、不叠到别的字符上;而 ◌̂(U+0302)、◌́(U+0301)是 Mn,会叠上去。两者字形像、归类全不同,详见 符号四子类那页 防御与清理:要剥掉所有变音 / 记号,先 normalize('NFD') 拆出组合记号,再 replace(/\p{M}/gu, '') 删掉;限长、截断一律按字素簇而非码点(否则会把记号和基字符切散)。这和 受控输入那页按 grapheme 限长是同一套思路。