---
title: "テキスト、Bidi と IME — zenit Zig UI ドキュメント"
description: "バイトから書記素クラスタ、双方向の段落、IME の変換まで。zenit はテキストをシステムの機能として扱い、コードポイントごとに並べたグリフの列としては扱いません。"
url: https://zenit.z.express/ja/docs/guide/text-engine
language: ja
alternate_en: https://zenit.z.express/docs/guide/text-engine.md
alternate_zh: https://zenit.z.express/zh/docs/guide/text-engine.md
alternate_es: https://zenit.z.express/es/docs/guide/text-engine.md
alternate_ko: https://zenit.z.express/ko/docs/guide/text-engine.md
alternate_fr: https://zenit.z.express/fr/docs/guide/text-engine.md
alternate_de: https://zenit.z.express/de/docs/guide/text-engine.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# テキスト、Bidi と IME

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

## 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 x | `src/render/text_renderer.zig` |

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

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

UAX #29

cursor\_pos は UTF-8 のバイトオフセットで、必ずクラスタ境界に止まります。1 行 Input の 2048 バイト容量での切り詰めでも、書記素が半分だけ残ることはありません。

[Video](https://zenit.z.express/media/stories/emoji.mp4?v=5f65f98444)

カラー絵文字は BGRA アトラスページとシェーダーのカラー分岐を通ります。グレースケールの対照群はグレーのままで、混在した行は通常のテキストと同じパイプラインを共有します。

[Video](https://zenit.z.express/media/stories/wordnav.mp4?v=1fd0aa8709)

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](https://zenit.z.express/media/unicode-bidi.png?v=d1e0e89e51)](https://zenit.z.express/media/unicode-bidi.png?v=d1e0e89e51)

実際の Storybook：アラビア文字、ヘブライ文字、latin + arabic + latin の混在を 1 つのテキストパイプラインでレイアウト。仮想マウスは RTL の story の上に止まっています。

[Video](https://zenit.z.express/media/stories/rtl.mp4?v=bc200dee27)

RTL および方向が混在する段落でのカーソル移動と選択。

## Unicode 17 適合性ゲート

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

**490,846**BidiTest 型シーケンス

**770,241**段落方向の判定

**91,707**BidiCharacterTest シーケンス

**766**GraphemeBreakTest ケース

```sh
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](https://zenit.z.express/ja/components/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](https://zenit.z.express/media/ime-preedit.png?v=6c12ff351b)](https://zenit.z.express/media/ime-preedit.png?v=6c12ff351b)

Preedit：下線付きの marked text と I-beam が見えますが、canonical buffer はまだ変わっていません。

[![Input showing committed 日本語](https://zenit.z.express/media/ime-commit.png?v=f665179c69)](https://zenit.z.express/media/ime-commit.png?v=f665179c69)

Commit：日本語 が canonical buffer に入り、preedit はクリアされます。

## IME のテスト

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

`ime.test.ts`

```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
```

[Video](https://zenit.z.express/media/e2e-text-ime.mp4?v=efa93e6b3f)

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

> TIP
> 
> **注入テストと実際の IME は検証対象が異なります。** harness による注入で、フェーズ、buffer、ピクセルのアサーションを再現可能にします。候補ウィンドウ、入力ソースの切り替え、NSTextInputClient との統合は、引き続き `scripts/verify_ime.sh` の実システム経路で検証します（GUI セッションとアクセシビリティ権限が必要で、実行中の約 8 秒間はキーボードに触れないでください）。

harness の詳しい使い方は [E2E 自動化](https://zenit.z.express/ja/docs/advanced/e2e) を参照してください。
