export const IME_COMPOSITION_SCHEMA_VERSION = 1 as const; export interface IMECompositionState { schemaVersion: typeof IME_COMPOSITION_SCHEMA_VERSION; composing: boolean; revision: number; pendingText: string; lastEvent: "IDLE" | "START" | "UPDATE" | "END"; } export type IMECompositionEvent = | { type: "compositionstart"; data?: string } | { type: "compositionupdate"; data?: string } | { type: "compositionend"; data?: string }; export function createIMECompositionState(): IMECompositionState { return { schemaVersion: IME_COMPOSITION_SCHEMA_VERSION, composing: false, revision: 0, pendingText: "", lastEvent: "IDLE" }; } export function reduceIMEComposition(state: IMECompositionState, event: IMECompositionEvent): IMECompositionState { if (!state || state.schemaVersion !== IME_COMPOSITION_SCHEMA_VERSION) throw new Error("IME_STATE_INVALID"); const text = typeof event.data === "string" ? event.data : ""; if (event.type === "compositionstart") return { schemaVersion: 1, composing: true, revision: state.revision + 1, pendingText: text, lastEvent: "START" }; if (event.type === "compositionupdate") { if (!state.composing) return state; return { schemaVersion: 1, composing: true, revision: state.revision + 1, pendingText: text, lastEvent: "UPDATE" }; } return { schemaVersion: 1, composing: false, revision: state.revision + 1, pendingText: text, lastEvent: "END" }; } export function shouldBlockOperatorShortcuts(state: IMECompositionState, eventIsComposing = false): boolean { if (!state || state.schemaVersion !== IME_COMPOSITION_SCHEMA_VERSION) throw new Error("IME_STATE_INVALID"); return state.composing || eventIsComposing; }