텍스트, Bidi와 IME
바이트에서 자소 클러스터, 양방향 단락, IME 조합까지. zenit은 텍스트를 코드 포인트마다 늘어놓은 글리프 행이 아니라 시스템 기능으로 다룹니다.
한 줄의 텍스트, 네 개의 계층
저장, 편집 경계, 방향 해석, 플랫폼 셰이핑은 각각 정확히 하나의 authority를 가지며, 함께 히트 테스트와 선택이 가능한 시각적 줄을 만듭니다. 어떤 계층도 다른 계층을 대신해 결정하지 않습니다.
src/text_coresrc/text_core/grapheme.zigsrc/i18n/bidi.zigsrc/render/text_renderer.zig코드 포인트가 아닌 자소
가족 이모지는 7개의 Unicode scalar(사람 4명과 ZWJ 3개)이고, é는 e에 결합 양음 부호를 더한 것일 수 있으며, 국기는 지역 표시자 두 개입니다. 사용자에게는 각각이 한 글자입니다. 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 파일 전체를 (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, anchor를 다시 읽음scripts/verify_ime.sh가 시스템 병음 / 일본어 입력 소스로 NSTextInputClient 전체 경로를 구동IME 테스트
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 자동화를 참고하세요.


