regex · Unicode 字符模型与一个自研引擎
一个 😀 在 JS 里 .length === 2,一面 🇨🇳 是两个区域码拼的,一个 👨👩👧 由五个 code point 用 ZWJ 粘成——emoji 正则难,是因为「一个用户眼里的字符」常常是一串 code point。本系列从 code point ⇄ UTF-16 / UTF-8 两套存储层讲起,认识 \p{…} 属性类、emoji 序列家族、grapheme cluster(字素簇)与归一化 / 大小写 / Han 统一 / 排序等通用 Unicode 概念,最后用本仓库 @vega/parsing/regex 自研引擎拆开 AST 与字节码。本系列从零讲起,不需要前置知识;「语法速览」页会把正则编译成 NFA/DFA 状态机,这部分的背景概念(Thompson 构造 / 子集构造)在 automata 系列有完整讲解。
编码 · 码点怎么存与怎么写
先把逻辑层的 code point 与两套存储层(UTF-16 / UTF-8)分开,回看 Unicode 之前的中文字节编码(GB / Big5),再看同一个码位在源码里怎么写出来(HTML / CSS / JS 三套转义)。
字符的编码:code point、UTF-16、UTF-8
分清 code point(逻辑层 U+0000~U+10FFFF)与两套存储层:UTF-16 code unit(内存,16 bit,补充平面用代理对——'😀'.length===2)与 UTF-8(网络 / 磁盘,变长 1~4 字节,前缀位标长度、续字节 10xxxxxx 自同步抗错)。一个拆解器并排摊出三层,亲手把 U+1F600 拆成代理对、摊进字节;顺带看 UTF-16 的字节序与 BOM、\u{...} vs \uXXXX、以及 emoji 正则为什么离不开 u/v 标志。
中文编码:GB2312 / GBK / GB18030 与 Big5
Unicode 之前,中文靠国家 / 地区编码落进字节:大陆 GB2312(1980)→ GBK(1995)→ GB18030 一脉相承、层层超集,台 / 港 Big5 另起一套收繁体。检视器把一个字在五种编码下的字节并排摊开:GB2312 的 区位码(区 / 位各加 0xA0)、GBK 下探尾字节到 0x40 打开的低字节坑(半个汉字漏成字母)、以及 GB18030 为什么存在——用 1/2/4 字节变长既兼容 GBK 老数据、又无损覆盖全 Unicode。末尾一个跨编码乱码 demo(同串字节 GBK vs Big5 读出两批字)与一条发展时间线。和 code point 那页互补:那页讲 Unicode 怎么变字节,这页讲它之前。
三套转义:HTML / CSS / JS 怎么写出一个码位
知道了码位(😀 = U+1F600),怎么在源码里把它打出来?HTML 😀、CSS \1F600、JS \u{1F600} 三套长得像、规则各不同,各有一个易错点:CSS 转义贪心吞掉后面的 hex(要加「终结空格」)、HTML 数字引用只认码位不认代理对、JS \uXXXX 工作在码元层能造出 lone surrogate「半个字符」。一个输入框实时出三套写法 + 三条路各渲染一遍,再逐个拆解三个易错点。和 code point 与 UTF-16 那页互补:那页讲码位「是什么」,这页讲码位「怎么写出来」。
码点的档案 · \p{…} 属性类
每个 code point 在 UCD 里有一份档案,\p{…} 就是按档案匹配。先认 General_Category / Script / 二元属性三条线,再把 \p{P} 标点、\p{S} 符号两大类展开到子类,顺带看「不是用来写字」的特殊码点。
\p{…} 属性类:General_Category、Script 与二元属性
每个 code point 在 UCD 里有一份档案,\p{…} 就是按档案匹配:General_Category(\p{L},七大类 30 小类)、Script(\p{Script=Han},顺带 sc vs scx 之辨——顿号为什么不算 Han)、二元属性(\p{White_Space} / \p{Math},跨着 gc 切)。对比 \w \d 的 ASCII 视野;最后看档案里的 Block(住址)为什么不在 JS 支持之列。
\p{Math} 全量速查:所有「数学用」码点
把「\p{…} 属性类」页讲到的二元属性 \p{Math} 全列出来:用 /\p{Math}/u 逐码点实测,枚举 Unicode 17.0 全部 2322 个 Math=Yes 码点,按 Unicode block 分组、可搜索。看清「数学符号」这概念怎么横跨 Sm / 字母(𝐀 ℵ)/ 标点 / 全角区好几个 gc 与 block——以及为什么常规希腊字母 π Σ 不在其中、math 变体 ϑ ϵ ϖ 却在。
\p{P} 标点七子类:脚注符号归哪类、开闭为什么配不齐
把「\p{…} 属性类」页 gc 表里 \p{P} 那一行展开:标点切成 7 个子类 Pc Pd Ps Pe Pi Pf Po,每个码点恰好属一个。脚注符号 * † ‡ § ¶ ‖ 全落在 catch-all 子类 Po(独占 641/856);| 看着像标点其实是 Sm。探针测任意字符的子类 + 七子类着色器;最后解一个反直觉:看着«成对»的开 / 闭括号(Ps/Pe = 79/77)、前 / 后引号(Pi/Pf = 12/10)数量为什么配不齐——落单的都是引号,真正的配对信息在 Bidi_Paired_Bracket。
\p{S} 符号四子类:| 是符号、^ 不是重音、😀 归哪类
标点页的姊妹篇:把 gc 表里 \p{S} 那行展开成 4 个子类 Sm Sc Sk So,每个码点恰好属一个。+ | ~ 是数学符号 Sm(不是标点)、$ € 是货币 Sc、^ ´ ¨ 是修饰符号 Sk(看着像重音、其实独立占一格)、emoji / © ★ 全挤在 catch-all 子类 So(7468 个,占 87%)。先认一组「看着像符号其实是标点 / 字母」的易混字符(% ′ 是 Po、µ Ω ˆ 是字母);探针 + 四子类着色器;再深挖 Sm 全是 \p{Math} 而 So 大多不是,以及 Sk 间隔修饰符 vs Mn 组合记号(^ vs ◌̂)。
形状符号:用 \p{So} 在纯文本里画图
另一大类 So 符号纯粹是图形、不是字:Box Drawing、Block Elements、Geometric Shapes。等宽下每个占一格,于是能拼进度条 / 表格框 / TUI / ASCII art。搬来 ▰▱ 进度条 demo(拖滑块 / 换字符对)+ Box Drawing 画框示例,再附「画图字符工具箱」359 个全量速查(按 block 可搜索切换);并讲清为什么形状能拼、emoji 不能(East_Asian_Width)。
不是用来写字的码点:边界、保留、私用与回退记号
🇹🇼 只剩字母方块、U+10FFFF 是最后一个码点——code point ≠ 普通可见字符。一个探针拖一个码点就报它的身份:边界(U+0000/U+10FFFF 上限由 UTF-16 代理对决定)、代理区孤儿(U+D800~U+DFFF 非 scalar value)、非字符(66 个永不分配)、私用区 PUA(接着「\p{…} 属性类」页的 Block「住址」继续展开)。再看 17 平面地图 + JS \p{…} 判定;末尾四个现场:RI 单用字母方块、孤儿代理报错、U+FFFD 替换符、U+202E bidi 反转(Trojan Source)。
竖排朝向:同一个字,横排立着、竖排可能要躺倒
档案里还有一栏横排用不到、一进竖排就生效的属性:Vertical_Orientation(vo,UAX #50)。把中日文 writing-mode: vertical-rl 竖过来,汉字 / 假名原样立着(vo=U)、夹在中间的拉丁与数字整体躺倒 90°(vo=R)——这由每个码点的 vo 逐字决定。竖排是汉字文化圈共同的传统方向(中文是源头,繁体中文 / 古籍 / 对联仍用),vo 对 CJK 各语言统一。三列并排看同一串在 text-orientation 的 mixed / upright / sideways 下的差异,再把文本逐字按 vo 上色拆开,讲清 Tu/Tr 这类变换字符为何把最终样子交给字体的竖排字形(OpenType vert)。末尾一桩进行中的 W3C 悬案:vo=Tr 的引号在字体缺竖排字形时,Chrome 正立、Firefox/Safari 旋转,根因是 CSS Writing Modes 那句「may wish to」太松。和 Block 一样,vo 是 UCD 档案却不在 JS \p{…} 之列。
Emoji · 一个字符是一串码点
emoji 难,是因为「一个用户眼里的字符」常常是一串 code point。从单码点的 Basic_Emoji 起,到一个码点身上的六个 emoji 属性,再到 RGI 序列家族(国旗 / Tag / 肤色)。
Basic_Emoji、变体选择符与 ZWJ
Basic_Emoji = 单码点 emoji 或单码点 + VS16,不依赖复杂组合。认识 U+FE0F(VS16 强制 emoji 脸)/ U+FE0E(VS15 强制文本脸);\p{Join_Control} 只含 ZWNJ U+200C / ZWJ U+200D 两个;再看 ZWJ 怎么把 👨+👩+👧 粘成 👨👩👧。
一个码点身上的 emoji 标签:六个属性与派生
换一条正交线:任意一个 code point 被 Unicode 打了哪几个 emoji 标签?六个码点级二元属性 \p{Emoji} / \p{Emoji_Presentation} / \p{Emoji_Modifier(_Base)} / \p{Emoji_Component} / \p{Extended_Pictographic}。看懂派生规则(Emoji=No ⇒ 三个子属性皆 No)、反直觉处(肤色自己就是 Emoji、❤ 不是 Emoji_Presentation),以及为什么这些码点属性自研引擎能跑、而 \p{RGI_Emoji} 不能。
RGI:国旗 / 子地区旗 / 肤色,与 \p{RGI_Emoji}
RGI = Recommended for General Interchange,各平台都能可靠显示的 emoji 子集。拼旗器(两个 Regional Indicator U+1F1E6~U+1F1FF)、Tag 子地区旗(U+E0020~U+E007E 映射 ASCII)、肤色修饰符(5 档 U+1F3FB~U+1F3FF);最后用 /\p{RGI_Emoji}/v 整段框出文本里每个完整 emoji。
检测 emoji 支持:字体里到底有没有这个字形
\p{RGI_Emoji} 只回答「这串码点是不是合法 emoji」,不回答「当前设备渲不渲染得出来」——后者取决于字体里有没有对应字形,和系统版本、地区强绑定。抛开可被伪装的 User-Agent 嗅探,直接测量渲染结果:三种互补的检查手段合到一个 lab,勾选启用哪几种、按「不支持优先」得出一个综合结论。A 字形存在(measureText:单码点宽度 ≠ 豆腐块 .notdef);B 连字塌缩(measureText:序列宽度 < 拆开写法);C 彩色像素(getImageData:国旗 / Tag 旗)。关键坑:🇹🇼 在中国大陆地区系统上退成「方块里两个字母」,宽度也塌窄,B 会误判为支持——真旗有大片彩色、字母方块是灰度,故国旗要用 C 纠正(取消勾选 C 可当场复现误判)。附一排跨 Unicode 版本的 emoji 体检,看清这台设备支持到哪一代。
一个字符到底多长 · grapheme 与切分
把 emoji 的现象收束成通用概念:用户眼里的「一个字符」= grapheme cluster(字素簇);切分不止字素簇一种粒度(词 / 句 / 断行);叠在字母上的组合记号 \p{M} 同样是「多码点 = 一个字符」。
字素簇:用户眼里的「一个字符」到底多长
🤦🏼♂️ 的长度:Python 5、JS/Java 7、Rust 17、Swift 1——同一个「字符」,各语言数出的全不同,差别在按哪层数(UTF-8 字节 / UTF-16 码元 / code point / grapheme cluster)。用原生 Intl.Segmenter 把文本切成「一个个字符」,看清前面 emoji 几页的 ZWJ/modifier/旗其实都是「多码点 = 一个字素簇」;再手写一遍 UAX #29 规则引擎(Grapheme_Cluster_Break 分类 + GB1~GB999 边界表),与 Intl.Segmenter 逐簇对账;最后演示 split('').reverse() 为什么会把 emoji 切碎。
分词与断行:把文本切成词 / 句,以及哪里能换行
字素簇是「一个字符」的粒度,但切分不止一种。Intl.Segmenter 还能按 word / sentence 切,中文这种没有空格的文字也能切出词;另有一层 line breaking(UAX #14)管「哪里允许换行」——它和字素簇边界不是一回事(can't 中间能断字素簇却不该断行)。三种粒度逐一上手,断行用实测把浏览器的真实折点标出来。
组合记号:叠在字母上的点和圈,以及 Zalgo
é 可以是 e + 急音符两码点,那个符号自己不占位、叠到前一个字符上。组合记号 \p{M} 分 Mn / Mc / Me 三子类,能无限叠加(Zalgo 的原理),排序受 canonical combining class 约束。亲手往基字符上叠记号、看它仍是一个字素簇;辨明它和长得像、却独立占位的 Sk 修饰符号的区别。
同义写法与同码点多形 · 拍平与展开
同一个字常有多种写法,同一个码点也可能不止一个含义。归一化 / 大小写把「同义写法」拍平;Han 统一 / 拼音则相反——同一码点的字形 / 读音不写在码点里;排序规则同样在码点之外。
归一化:同一个「Å」为什么有几种写法
Å 可以是单码点 U+00C5,也可以是 A + 组合环两码点——看着一样,=== 却不等。四种归一化 NFC/NFD/NFKC/NFKD 把「同字多形」拍平:NFC/NFD 组合↔分解、字形不变,NFKC/NFKD 兼容分解会改字形(全角 A→A、①→1、fi→fi、𝕏→X)。列全两层等价的全部类目(<wide>/<font>/<circle>… 的兼容标记 + 规范层 singleton),并深入全角 / 半角这一类,牵出显示宽度(East_Asian_Width)与正则匹配(\d 不认全角、先 NFKC 再匹配)两个下游后果。比较 / 查重前先 normalize。
大小写:toLowerCase 不够,正则 i 走的是另一套
大小写会改长度(ß 的大写是两个字母 SS)、依赖 locale(土耳其的 I 转小写不是 i),而且「给人看的转换」(case mapping)和「给比较用的折叠」(case folding)是两套规则——正则 i 标志走的是后者,认得 K=k(开尔文)、ſ=s(长 s)。把这几层拆开,再看 \p{Cased} / \p{Changes_When_*} 等大小写属性,和归一化同属「同义写法拍平」。
Han 统一:同一个码点,中日韩为什么显示成不同的字
和归一化正相反的一面:U+5203(刃)只有一个码点,却在中 / 日 / 韩字体里画成不同字形——这就是 Han Unification(汉字统一):Unicode 把三地历史同源的汉字合并到同一批码点,字形差异交给字体 + lang(OpenType locl)处理,边界由 Source Separation Rule 划定(戶/戸 这类本就分编的仍是不同码点)。把同一个码点用 zh-Hans / zh-Hant / ja / ko 四语并排渲染,再扫一遍「刃骨令内直草道切角」等经典分歧字;点明 === / 正则只认码点看不见字形,所以不标 lang 就会拿中文字形显示日文(字写错了);再补一节反面——繁简 / 异体字(謝 / 谢 是不同码点却同一个词,归一化都不合并,信息记在 Unihan 的 kSimplifiedVariant 而非码点里)。
给汉字注音:拼音不在码点里
和 Han 统一同构的另一面:码点不带读音。U+91CD(重)的码点既不含字形、也不含「读 zhòng 还是 chóng」——读音记在 Unihan(kMandarin)/ CLDR 转写表里。用 pinyin-pro 给文本注音,按标准 <ruby> 渲染;再看多音字的「一对多」(和繁简一样靠词典 + 分词选音,不是字符级映射)、一个音节拆成声母 / 韵母 / 声调,以及带调元音 ǚ 牵出的归一化(NFC 单码点 vs NFD 拆成组合记号)。
排序:为什么 arr.sort() 把 Z 排在 a 前面
默认 sort() 按 UTF-16 码元值比,于是 "Z" < "a"、带音标的字母垫底、"file10" < "file9"。真正按语言的字典序排要用 Intl.Collator(UCA):三层权重与 sensitivity(忽略到哪层)、locale 差异(瑞典语 å ä ö 排在字母表末尾)、numeric 自然排序。和拼音一样,排序规则不写在码点里。
实战 · 把字符模型合进工具
把前面拆过的字符模型合进真实工具:一个检视器逐字符拆码点(读),一个按名反查工具按 Unicode 名找码点(查),一个受控输入框按字形限长、注入控制字符(写)。
code point 检视器:任意文本逐字符拆码点
把前面拆过的几层字符模型合到一个工具里:输入 / 粘贴任意文本,逐个 code point 摊开——每个码点的 U+ 编号、General_Category、Block,与 UTF-8 / UTF-16 两套存储层的字节。多码点拼成的字素簇(emoji 序列 / 组合记号)用虚线框聚拢;顶部并列字形 / code point / UTF-16 码元 / UTF-8 字节四个口径,一眼看清三者为何对不上。和按名反查(查)、受控文本输入框(写)合成「读 / 查 / 写」三件套。
按名反查:输入 heart,列出所有相关码点
检视器的反方向:不是「拿字符看名字」,而是「给名字找字符」。每个码点在 UCD 里有一个固定的英文 Unicode 名(❤ = HEAVY BLACK HEART);输入一个英文子串,实时列出名字含该词的全部码点,按 Block 分组、标 General_Category、命中处高亮,点 U+ 直达 codepoints.net。覆盖 UnicodeData.txt 里 4 万余个有显式名的码点(刻意排除 CJK / Hangul 等算法名大区间与 <control> 占位名)。和检视器互为码点 ⇄ 名字正反一对。
受控文本输入框:按字形限长 · 注入控制字符 · twemoji 渲染
把前面拆过的概念合进一个真实输入组件:用 @vega/smart-input 接管 <textarea>,normalize 按 grapheme(用户可见字符)而非 .length 限长——🤦🏼♂️ 只算 1 个、截断不切碎序列;右侧调色板把不可见 / 控制字符(零宽、U+202E bidi 反转 = Trojan Source)插到光标处,计数器实时暴露「肉眼没多、限额被吃」;末尾把 emoji 用 twemoji 的 SVG 渲染,跨平台同一份字形,取不到则回退系统字体。
安全 · 看不见的攻击面
同一段文本,「逐字节」和「屏幕渲染」可以完全不同——bidi 重排与同形字是两类看不见的攻击面(Trojan Source / homograph attack),都建立在前面的字符模型(bidi class / Script)之上。
方向控制字符:看不见的字符如何重排显示顺序
文本的存储顺序(逻辑序,正则 / 编译器看到的)和显示顺序(视觉序)不一定相同——LTR/RTL 混排时浏览器按 UAX #9 重排。用 getBoundingClientRect 实测渲染坐标,把「逻辑序」「视觉序」两行格子并排,看哪些字符被调换;每格按 bidi class(强 L/R/AL · 弱 EN/AN · 中性)上色。再认全方向控制字符三代机制(mark LRM/RLM / embedding·override RLE/RLO/PDF / 推荐的 isolate LRI/FSI/PDI),点「注入」看效果;末尾拆 Trojan Source(CVE-2021-42574):同一段源码「逐字节」与「屏幕渲染」三视图对照,以及怎么扫出来。
同形字:看着是 apple.com,其实是 аpple.com
Cyrillic а 和 Latin a 字形一样、码点不同,可拼出肉眼无异的假域名 / 假品牌(homograph attack)。UTS #39 用 confusables 表折叠出 skeleton 判断可混淆,再加混合脚本检测。逐 code point 验 Script、算 skeleton 对比、看 IDN 域名实际编码成的 xn-- punycode;和 bidi 的 Trojan Source 是「看不见的攻击面」双子页。
正则语法本身
回到正则语法本身:本仓库 @vega/parsing/regex 基本都覆盖。先过一遍各种写法,再把几个最易踩的点(. 与 \s / 有状态的 g / 命名捕获 vs 反向引用 / 量词下的分组)单独掰开。
regex 语法速览:一个 playground 跑通各种写法
回到正则语法本身:字面量 / . / 字符类 / 量词(贪婪 vs 惰性)/ 分组与选择 / 命名组 / 锚点 ^ $ \b / 反向引用 / lookaround / 标志位与内联修饰符 / 码点属性 \p{…}。这些本仓库的 @vega/parsing/regex 基本都覆盖——一个实时匹配器 + 按类别分好的例子,点一下就看命中高亮、捕获组与走了哪条引擎。
空白与换行:. 不匹配什么、\s 匹配什么
把 syntax 一笔带过的两点掰开:. 默认不匹配 4 个行终止符(\n \r U+2028 U+2029,开 s 才放行);\s 则匹配 ECMAScript WhiteSpace 全集——NBSP、Ogham、en/em/thin 各种排版空格、全角空格,甚至 BOM。看宽度标尺逐个对比,认两个反直觉(BOM 算 \s、零宽空格 U+200B 不算),再用扫描器把文本里的隐形字符标出来。
零宽匹配:正则在字符之间的「位置」上移动
"good".match(/o*/g) 会得到 ["", "oo", "", ""]——凭空多出三个空串,根因是正则匹配的不是「字符」而是字符之间的位置:长度 n 的串有 n+1 个位置,引擎站在某个位置上尝试,匹配到的内容允许是零长度的。把 token 按「消不消耗字符」分三类:消耗字符(\d / [a-z]+)、可零宽(o* / \w? 下限为 0)、纯位置的零宽断言(锚点 ^ $ \b / 环视 (?=…) (?<=…) 只校验位置)。单步看一次全局扫描每个位置产出什么,再用千分位逗号 \B(?=(\d{3})+(?!\d)) 演示「在位置上插入」这一零宽匹配的实战用途。和有状态的全局匹配页的空匹配 +1 规则互补。
有状态的全局匹配:g、lastIndex 与只在第二次调用才暴露的状态问题
g 不只是「找全部」——它给 regex 对象挂了个可变游标 lastIndex,一连串经典 bug 的根。从「re.test(s) 连续调用两次结果不同」的现象讲起:空匹配 +1 单步可视化(aaabc → ||b|c|)/ g vs y 跳着找 vs 锚定不跳 / matchAll 为什么强制要 g / 收尾对照 sed·Rust·Raku 三副词,看「全局推进」本就有多种选择。
命名捕获与反向引用:一个共用语法,一个共用文本
把 syntax 一笔带过的两点掰开,分清最易混的一对:命名捕获 (?<x>…) 只是给组起个名字(\1 的可读别名,在 pattern / 替换串 $<x> / 结果 namedGroups.x 处处复用,不改能力);反向引用 \1 / \k<x> 复用的是组实际匹配到的那串文本((\w+) \1 是「同一个词」而非「任意词」,超出正则语言要回溯)。一个对照框看清两者何时分道扬镳,再用 \k<q> 配对引号 / 标签。
量词下的分组:为什么只捕获到最后一次?
(\d)+ 匹配 "123",可 $1 只有 "3"——一个捕获组只有一个槽,量词每重复一次就覆盖上一次。单步观察那个槽被逐次覆盖、对比 (\d+)(量词在组内 → 一次吃下 "123");再看别的语言的取舍(Perl (?{}) 打印过程、Raku 收进 list、.NET Captures 留底)。末尾一道把它放进 lookbehind 的思考题:(?<=(.){3}) 的 $1 为何是 "1" 而非 "3"。
自研引擎 · @vega/parsing/regex
最后拆本仓库 @vega/parsing/regex 的实现:pattern → AST → Pike VM 或回溯两条引擎;对比 combinator 与递归下降两种解析写法;以及回溯引擎的 ReDoS 风险与线性引擎为何贴地。
自己写的正则引擎:AST → 两条引擎
调用本仓库的 @vega/parsing/regex:pattern → AST → Pike VM(线性、抗 ReDoS)或回溯(全功能)。实时看匹配结果(命名组 / indices)、自动派发选了哪条引擎、AST 树与 Pike 字节码;并对比它支持码点属性 \p{Emoji} 却不收字符串值属性 \p{RGI_Emoji}——后者正是前面 emoji 几页要用原生 v 的原因。
parser combinator vs 递归下降:同一棵 AST,两种写法
「解析引擎」那页的解析器走递归下降。这页把它和 parser combinator(seq/alt/many 拼出来的声明式写法)并排:同一段正则,两个 toy 解析器都真在跑,实时渲染两棵 AST 并 deep-equal 校验——看清 combinator 只是换种描述文法的方式,不增加能力。
灾难性回溯:一条正则如何耗尽 CPU
嵌套量词((a+)+)在匹配失败时会让回溯引擎把指数级的拆法全部尝试一遍——拖动输入长度,实测每加一个字符步数翻倍(O(2ⁿ))的柱状图。再用本仓库 @vega/parsing 的回溯 vs Pike VM 两条真引擎实测耗时:回溯飙升、线性引擎贴地。这就是 ReDoS,以及为什么默认 auto 会自动躲开回溯。和「解析引擎」那页的抗 ReDoS 互为正反面。
@vega/parsing/regex 的 Pike VM 正是 有限自动机里 Thompson 构造(regex → NFA)的工程落地;它的线性匹配与 字符串匹配一脉相承。emoji 部分则回答了那些系列里没细讲的「一个字符到底是什么」。🔗 相关链接
- The Absolute Minimum Every Developer Must Know About Unicode · tonsky.me 通用 Unicode 必读:code point、UTF-8/16、字素簇、归一化一篇讲透。本系列的 UTF-8 / 字素簇 / 归一化几页正是把它的核心做成了可上手的 demo。
-
UTS #51 · Unicode Emoji(权威规范)
· unicode.org
emoji 半边的源头标准:
Emoji/Emoji_Presentation等属性、ZWJ 序列、肤色 modifier、国旗与 Tag、RGI 子集全部定义于此。本系列 Basic_Emoji / 六属性 / RGI 几页讲的规则,出处就是这份报告。 -
UAX #29 · Unicode Text Segmentation(权威规范)
· unicode.org
字素簇 / 词 / 句切分的源头标准:
Grapheme_Cluster_Break属性值与边界规则表(GB1~GB999)全部定义于此。本系列字素簇一页的「手写规则引擎」就是照这份报告实现Intl.Segmenter的 grapheme 切分。 -
UAX #14 · Unicode Line Breaking Algorithm(权威规范)
· unicode.org
「哪里允许换行」的源头标准:每个码点的
Line_Break属性值与折行规则全部定义于此。本系列分词与断行一页里浏览器实测出的折点,背后跑的就是这套规则。 -
UTS #10 · Unicode Collation Algorithm(权威规范)
· unicode.org
排序的源头标准:多级权重(primary/secondary/tertiary)与 DUCET 默认权重表全部定义于此。本系列排序一页用的
Intl.Collator正是它的工程实现,locale 差异则来自 CLDR 的 tailoring。 -
UAX #50 · Unicode Vertical Text Layout(权威规范)
· unicode.org
竖排朝向的源头标准:
Vertical_Orientation(vo)属性的四个取值 U/R/Tu/Tr、未分配码点的默认区间、与字体竖排替代字形的关系全部定义于此。本系列竖排朝向一页每格的vo值就照它的VerticalOrientation.txt实现。 - UTS #39 · Unicode Security Mechanisms(权威规范) · unicode.org 同形混淆与脚本混用的源头标准:confusables 表、skeleton 算法、mixed-script 检测全部定义于此。本系列同形字一页的 skeleton 与混合脚本判定,出处就是这份报告。
- Codepoints · 逐码点数据库 · codepoints.net 查任意 code point 的属性大全:名称、General_Category、所属 block/script、各编码字节、组合/大小写映射、相关字符。验证本系列各页拆出来的码点、属性时一键可查。
- Unicode Code Charts · 官方字符表 · unicode.org 最权威的源头:按 Block 一份份 PDF,逐个 code point 列出官方字形与名称——「特殊用意的码点」那页的 Block / 边界 / 私用区,在这里能找到每段的官方定义图表。
- Full Emoji List · emoji 全表 · unicode.org 官方 emoji 总表:逐个列出 code point、CLDR 名称,并排展示各厂商(Apple/Google/Microsoft…)的实际渲染。对照本系列 Basic_Emoji / RGI / ZWJ 序列拆出的码点最直观——一眼看清同一序列在不同平台长什么样。
- Emojipedia · emoji 百科 · emojipedia.org 逐个 emoji 的词条:code point、shortcode、各平台 / 各 Unicode 版本的历代渲染、ZWJ 组成与含义流变。比 Full Emoji List 更适合「查某一个 emoji 的来龙去脉」。
- RegexLearn · 分步交互课程 · regexlearn.com 从零开始的闯关式教程,每步即时校验;有中文,适合系统补一遍语法。
- learn-regex · 图解速成(中文) · github.com 用图把元字符 / 量词 / 分组 / lookaround 讲清楚的速查仓库,这里直达其中文翻译。
- 正则表达式 30 分钟入门教程 · deerchao.cn 流传多年的中文经典长文,从字符匹配一路讲到分组 / 反向引用 / lookaround,术语对照清楚,适合一口气通读入门。
- RegexBuddy · 正则构建与调试工具 · regexbuddy.com 桌面端正则 IDE:逐节点解析正则、单步调试匹配过程、跨多种 flavor(PCRE / JS / .NET / Java…)对照语法差异,并生成各语言调用代码。出自 regular-expressions.info 同一作者。
- The Typing of the Regex · 正则打字游戏 · thetypingoftheregex.com 敌人是一串文本,你得现写正则把它们「打」掉——边玩边练手感与直觉。