算法与数据结构 / regex · Unicode 字符模型与一个自研引擎 / 组合记号:叠在字母上的点和圈 待审核 17 / 36
字符的真面目 · 组合记号 \p{M}

组合记号:叠在字母上的点和圈

é 可以是单个预组合码点 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

表 1-1 · Mark 的三个子类、占位行为与典型字符。
子类 含义 占不占位
Mn Nonspacing,非占位,叠在基字符上 不占位 ◌́ ◌̈ ◌̣
Mc Spacing Combining,占位组合,多见于印度系文字的元音符 占位 ा ी
Me Enclosing,把基字符圈起来 包围 ◌⃝ ◌⃣
图 1-1 · 单个字符的 Mark 子类实测,同时测 Sk 以作对照。组合记号前补一个 ◌ 占位才看得见。可换输入或点预设。

警示 · ^U+005E)、´U+00B4)这类是 Sk(Modifier Symbol,全 Unicode 125 个),自己独立占一格,不会叠到别的字符上;而 ◌̂U+0302)、◌́U+0301)是 Mn,一定叠上去。两者字形几乎一样,归类完全不同,键盘上打出来的多半是前者。详见 符号四子类

2 · 叠加与字素簇

一个基字符后面可以跟任意多个组合记号,不管叠几个,它们与基字符始终是一个字素簇,也就是用户眼里的一个字符,但 code point 数一直在涨。这正是 字素簇 那页说的「一个字符等于一串码点」在拉丁字母上的形态,与 emoji 那几页的 ZWJ 序列同构。

图 2-1 · 往基字符上叠组合记号,同时给出码元、码点与字素簇三个计数。可换基字符、从记号池里挑上标、下标或穿透记号。

3 · Zalgo

组合记号没有数量上限。往每个字符上随机堆几十个 Mn,字形就会向上下溢出、糊成一团,这就是 Zalgo 文字。它本身无害,仍是普通文本,但能撑爆行高,也能绕过只统计可见字符的长度校验:一段 5 个字素簇的 Zalgo 串,码点数可以是几百。

图 3-1 · 按强度往文本上随机叠加组合记号,并给出字素簇、码点与记号三个计数。可改文本、拖强度或重新生成。

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+1EB9U+0301 两个码点:下点与 e 合成了预组合的 ,急音符则因为没有对应的预组合码点而留在外面。

图 4-1 · 三组记号次序不同的输入,逐串给出原始码点序列与 NFC 结果,并标出 NFC 是否重排过。可点预设切换。

建议 · 要剥掉所有变音记号,先 normalize('NFD') 把预组合码点拆开,再 replace(/\p{M}/gu, '') 删掉记号;只做后一步会漏掉写成单码点的那些。限长与截断一律按字素簇而非码点,否则会把记号与基字符切散,留下一串孤儿记号——这与 受控输入 页按 grapheme 限长是同一套思路。

5 · 参考文献

  1. Unicode. UAX #15: Unicode Normalization Forms. canonical ordering 与四种归一化形式的规范,本页第 4 节的重排与合成行为照它。unicode.org
  2. Unicode. UAX #44: Unicode Character Database. General_CategoryMn / Mc / Me 定义与 Canonical_Combining_Class 的取值表(上点 230、下点 220 即出自此)。unicode.org
  3. MDN. String.prototype.normalize(). 四种形式的调用方式与常见用法。developer.mozilla.org
  4. Unicode. Combining Diacritical Marks(U+0300–U+036F). 本页记号池取自这一区,图表给出每个记号的名称与叠放位置。unicode.org