docs/guide/text-engine
コアコンセプト · Unicode 17

テキスト、Bidi と IME

バイトから書記素クラスタ、双方向の段落、IME の変換まで。zenit はテキストをシステムの機能として扱い、コードポイントごとに並べたグリフの列としては扱いません。

約 12 分で読めます · 実ウィンドウの録画付き

1 行のテキスト、4 つの層

ストレージ、編集境界、方向の解決、プラットフォームのシェーピングはそれぞれ唯一の authority を持ち、最終的にヒットテストや選択が可能な視覚行にまとまります。どの層も別の層の代わりに判断することはありません。

TEXT STACK
最初の 2 層は src/text_core、双方向アルゴリズムは src/i18n にあり、macOS でのシェーピングは CoreText が担当します。
層担当場所
UTF-8 文書座標安定した byte offset を保持し、すべての編集入口で境界に正規化src/text_core
UAX #29 書記素カーソル、削除、選択が ZWJ 絵文字、結合文字、旗を分断しないsrc/text_core/grapheme.zig
UAX #9 Bidi段落方向、isolate、embedding、括弧ペア、論理 ↔ 視覚のマッピングsrc/i18n/bidi.zig
CoreText シェーピングフォント fallback、合字、glyph run、視覚配置と caret xsrc/render/text_renderer.zig

コードポイントではなく書記素

家族の絵文字は 7 つの Unicode scalar(4 人と 3 つの ZWJ)でできており、é は e と結合アキュートの組み合わせでもあり得ます。旗は 2 つの地域指示子です。ユーザーにとってはどれも 1 文字です。zenit は拡張書記素クラスタを、カーソル移動、削除、選択、容量による切り詰めの最小単位として使います。

UAX #29
cursor_pos は UTF-8 のバイトオフセットで、必ずクラスタ境界に止まります。1 行 Input の 2048 バイト容量での切り詰めでも、書記素が半分だけ残ることはありません。
カラー絵文字は BGRA アトラスページとシェーダーのカラー分岐を通ります。グレースケールの対照群はグレーのままで、混在した行は通常のテキストと同じパイプラインを共有します。
Alt+← / → でギリシャ文字やキリル文字の単語を単語単位でジャンプし、ダブルクリックで単語を選択します。家族の絵文字は常に 1 つのクラスタです。

双方向テキスト

テキストはメモリ上では論理順に格納され、画面上では視覚順に表示されます。UAX #9 はまず各文字の埋め込みレベルを解決し、次に規則 L2 で最も高いレベルから順に run を反転します。下の例は LTR 段落内の abc אבג 123 です。ヘブライ文字はレベル 1、その後に続く数字はレベル 2 に解決されます。

UAX #9
数字自体は左から右に読みますが、RTL 区間全体(数字を含む)は 1 つのブロックとして右から左に配置されます。

UAX #9 リゾルバは src/i18n/bidi.zig にある zenit 独自のポータブルな実装で、プロパティテーブルは固定された Unicode 17.0.0 のデータから生成されます。macOS では最終的なグリフのシェーピングと RTL の連結は、引き続き run 単位でまとめて CoreText に渡されます。テキストレンダラーが RTL の run をコードポイントごとに分割することはありません。

zenit Storybook showing mixed Arabic, Hebrew and Latin text
実際の Storybook:アラビア文字、ヘブライ文字、latin + arabic + latin の混在を 1 つのテキストパイプラインでレイアウト。仮想マウスは RTL の story の上に止まっています。
RTL および方向が混在する段落でのカーソル移動と選択。

Unicode 17 適合性ゲート

実行時のプロパティテーブルは、リポジトリに固定された Unicode 17.0.0 のデータから生成されます。公式テストファイルは vendor/unicode/17.0.0 に無改変で同梱され、SHA256SUMS で SHA-256 により固定されています。ここでいう「サポート」とは、いくつかの絵文字の単体テストを書くことではなく、公式テストスイートを完全に実行することです。

490,846BidiTest 型シーケンス
770,241段落方向の判定
91,707BidiCharacterTest シーケンス
766GraphemeBreakTest ケース
Unicode conformance gates
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 LIFECYCLE
preedit と commit は 2 種類のイベントであり、1 つの文字列の途中経過ではありません。確定後、入力は一時的に commit_pending_end に留まり、プラットフォームが続けて送ってくるかもしれない同じテキストの重複を吸収してから idle に戻ります。
ファーストクラスの意味具体的な契約
独立したイベントime_preedit / ime_commit でフェーズ情報を保持。空の preedit でキャンセル
再変換どちらのイベントも replacement range(UTF-8 バイト範囲)を持てるため、確定済みテキストを変換中に戻したりその場で置換したりできる
視覚的な変換表示marked range の下線とハイライト、変換中カーソル、候補ウィンドウの位置が視覚行に追従
編集の安全性カーソル、削除、選択、容量による切り詰めはすべて拡張書記素境界に揃う
テスト可能harness は preedit / commit を個別に注入し、ime_phase、ime_preedit_len、buffer、cursor_pos、anchor を読み戻す
実機ゲートscripts/verify_ime.sh がシステムのピンイン / 日本語入力ソースで NSTextInputClient の全経路を通す
Input showing underlined nihongo preedit text
Preedit:下線付きの marked text と I-beam が見えますが、canonical buffer はまだ変わっていません。
Input showing committed 日本語
Commit:日本語 が canonical buffer に入り、preedit はクリアされます。

IME のテスト

E2E クライアントは 2 つのフェーズを別々の RPC として公開し、inputState は test id で入力欄の実際の状態を読み戻します。

ime.test.ts
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 composition
harness は実際の Retina ウィンドウで Input にフォーカスし、preedit と commit を個別に注入してから RTL と Checkbox に移ります。I-beam、手のカーソル、クリックのパルスはすべて仮想マウスが drawable に描画しています。

harness の詳しい使い方は E2E 自動化 を参照してください。

zenit · デュアルライセンスオープンソースプロジェクトは GPL-3.0-only のもとで無料で使えます。クローズドソースや商用製品には商用ライセンスが必要です。作者への連絡先:zongyi.xzy#gmail.com(# を @ に置き換え)zenit 5f9add5+wip 2026-09-30