文本、Bidi 与 IME
从字节、字素簇、双向段落到输入法合成:zenit 把文本当作系统能力,而不是一串按码点排开的 glyph。
一条文本,四层职责
存储、编辑边界、方向解析和平台塑形各有唯一的 authority,最后统一成可命中、可选择的视觉行。任何一层都不替另一层做决定。
src/text_coresrc/text_core/grapheme.zigsrc/i18n/bidi.zigsrc/render/text_renderer.zig字素不是码点
一个家庭 emoji 是 7 个 Unicode scalar(4 个人加 3 个 ZWJ),é 可以是 e 加一个组合重音,一面旗帜是两个区域指示符。用户眼里它们各是一个字符。zenit 用扩展字素簇作为光标移动、删除、选区和容量截断的原子。
双向文本
文本在内存里按逻辑顺序存放,屏幕上按视觉顺序显示。UAX #9 先为每个字符解析嵌入层级,再按规则 L2 从最高层级开始逐级反转。下例是 LTR 段落中的 abc אבג 123:希伯来文得到层级 1,紧随其后的数字得到层级 2。
UAX #9 解析器是 zenit 自研的可移植实现,位于 src/i18n/bidi.zig,属性表由固定的 Unicode 17.0.0 数据生成。macOS 上最终的 glyph 塑形与 RTL 连字仍整段交给 CoreText,文本渲染器不会把 RTL 片段逐码点拆开。
Unicode 17 一致性门禁
运行时属性表由仓库固定的 Unicode 17.0.0 数据生成;官方测试文件原样放在 vendor/unicode/17.0.0,并在 SHA256SUMS 中用 SHA-256 固定。这里的「支持」不是挑几个 emoji 写单元测试,而是跑完整的官方用例。
python3 tools/generate_grapheme_data.py --check
python3 tools/generate_bidi_data.py --check
(cd vendor/unicode/17.0.0 && shasum -a 256 -c SHA256SUMS)
zig build test-text-core
zig build test-bidi-conformance--check 确认生成的 Zig 表与数据一致;test-text-core 跑完 GraphemeBreakTest 全部用例;test-bidi-conformance 跑两个完整的官方 bidi 文件(到 UAX #9 规则 L2),它也包含在 zig build test-headless 中。
IME 是一等公民
IME 不是把最终汉字伪装成一次键盘输入。平台事件保留阶段语义:ime_preedit 携带合成文本和合成区内的光标偏移,ime_commit 携带最终文本。合成期间 Input 与 Textarea 渲染带下划线的 marked text,但 canonical buffer 只在提交时改变。没有单独的「取消」事件:空的 preedit 就表示取消合成。
ime_preedit / ime_commit,不丢阶段信息;空 preedit 取消合成ime_phase、ime_preedit_len、buffer、cursor_pos、anchorscripts/verify_ime.sh 用系统拼音 / 日文输入源走完整的 NSTextInputClient 路径测试输入法
E2E 客户端把两个阶段作为独立的 RPC 暴露出来,inputState 按 test id 回读输入框的真实状态。
import { imeCommit, imePreedit, inputState } from "./client";
// The input must already have focus (e.g. click it first).
await imePreedit("nihongo");
let s = await inputState("story.input.name");
// s.ime_phase === "composing", s.ime_preedit_len === 7, s.buffer === ""
await imeCommit("日本語");
s = await inputState("story.input.name");
// s.buffer === "日本語", s.ime_preedit_len === 0
await imePreedit(""); // empty preedit cancels an active compositionHarness 的完整用法见 E2E 自动化。


