テキスト、Bidi と IME
バイトから書記素クラスタ、双方向の段落、IME の変換まで。zenit はテキストをシステムの機能として扱い、コードポイントごとに並べたグリフの列としては扱いません。
1 行のテキスト、4 つの層
ストレージ、編集境界、方向の解決、プラットフォームのシェーピングはそれぞれ唯一の authority を持ち、最終的にヒットテストや選択が可能な視覚行にまとまります。どの層も別の層の代わりに判断することはありません。
src/text_coresrc/text_core/grapheme.zigsrc/i18n/bidi.zigsrc/render/text_renderer.zigコードポイントではなく書記素
家族の絵文字は 7 つの Unicode scalar(4 人と 3 つの ZWJ)でできており、é は e と結合アキュートの組み合わせでもあり得ます。旗は 2 つの地域指示子です。ユーザーにとってはどれも 1 文字です。zenit は拡張書記素クラスタを、カーソル移動、削除、選択、容量による切り詰めの最小単位として使います。
双方向テキスト
テキストはメモリ上では論理順に格納され、画面上では視覚順に表示されます。UAX #9 はまず各文字の埋め込みレベルを解決し、次に規則 L2 で最も高いレベルから順に run を反転します。下の例は LTR 段落内の abc אבג 123 です。ヘブライ文字はレベル 1、その後に続く数字はレベル 2 に解決されます。
UAX #9 リゾルバは src/i18n/bidi.zig にある zenit 独自のポータブルな実装で、プロパティテーブルは固定された Unicode 17.0.0 のデータから生成されます。macOS では最終的なグリフのシェーピングと RTL の連結は、引き続き run 単位でまとめて CoreText に渡されます。テキストレンダラーが RTL の run をコードポイントごとに分割することはありません。
Unicode 17 適合性ゲート
実行時のプロパティテーブルは、リポジトリに固定された Unicode 17.0.0 のデータから生成されます。公式テストファイルは vendor/unicode/17.0.0 に無改変で同梱され、SHA256SUMS で SHA-256 により固定されています。ここでいう「サポート」とは、いくつかの絵文字の単体テストを書くことではなく、公式テストスイートを完全に実行することです。
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 ファイル 2 つを完全に(UAX #9 規則 L2 まで)実行します。後者は zig build test-headless にも含まれています。
IME はファーストクラス
IME は、確定後の文字を 1 回のキー入力に見せかけるものではありません。プラットフォームのイベントはフェーズの意味を保持します。ime_preedit は変換中のテキストとその中のカーソルオフセットを、ime_commit は確定テキストを運びます。変換中、Input と Textarea は下線付きの marked text を描画しますが、canonical buffer が変わるのは確定時だけです。独立した「キャンセル」イベントはなく、空の preedit が変換のキャンセルを意味します。
ime_preedit / ime_commit でフェーズ情報を保持。空の preedit でキャンセルime_phase、ime_preedit_len、buffer、cursor_pos、anchor を読み戻すscripts/verify_ime.sh がシステムのピンイン / 日本語入力ソースで NSTextInputClient の全経路を通すIME のテスト
E2E クライアントは 2 つのフェーズを別々の 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 自動化 を参照してください。


