From 8e1bccf97ec85656f46705ca17b26b2512e1d41f Mon Sep 17 00:00:00 2001 From: can1357 Date: Tue, 12 May 2026 10:11:18 +0200 Subject: [PATCH] fix(tui): normalized Thai/Lao AM output for terminal rendering - Added normalizeTerminalOutput to convert precomposed Thai and Lao AM vowels into compatibility decompositions for terminal rendering. - Converted ASCII classification tables to Uint8Array and updated whitespace and punctuation checks to use bounded code-point lookups. --- packages/tui/src/utils.ts | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/tui/src/utils.ts b/packages/tui/src/utils.ts index a2e11db15..14ba4b0a8 100644 --- a/packages/tui/src/utils.ts +++ b/packages/tui/src/utils.ts @@ -112,12 +112,26 @@ export function visibleWidth(str: string): number { return visibleWidthRaw(str); } -const makeBoolArray = (chars: string): ReadonlyArray => { - const table = Array.from({ length: 128 }, () => false); +const THAI_LAO_AM_REGEX = /[\u0e33\u0eb3]/; +const THAI_LAO_AM_GLOBAL_REGEX = /[\u0e33\u0eb3]/g; + +/** + * Normalize text for terminal output without changing logical editor content. + * Some terminals render precomposed Thai/Lao AM vowels inconsistently during + * differential repaint. Their compatibility decompositions have the same cell + * width but avoid stale-cell artifacts in terminal renderers. + */ +export function normalizeTerminalOutput(str: string): string { + if (!THAI_LAO_AM_REGEX.test(str)) return str; + return str.replace(THAI_LAO_AM_GLOBAL_REGEX, (char) => (char === "\u0e33" ? "\u0e4d\u0e32" : "\u0ecd\u0eb2")); +} + +const makeBoolArray = (chars: string): Uint8Array => { + const table = new Uint8Array(128); for (let i = 0; i < chars.length; i++) { const code = chars.charCodeAt(i); if (code < table.length) { - table[code] = true; + table[code] = 1; } } return table; @@ -129,8 +143,8 @@ const ASCII_WHITESPACE = makeBoolArray("\x09\x0a\x0b\x0c\x0d\x20"); * Check if a character is whitespace. */ export function isWhitespaceChar(char: string): boolean { - const code = char.codePointAt(0) || 0; - return ASCII_WHITESPACE[code] ?? false; + const code = char.codePointAt(0) ?? 0; + return code < 128 && ASCII_WHITESPACE[code] === 1; } const ASCII_PUNCTUATION = makeBoolArray("(){}[]<>.,;:'\"!?+-=*/\\|&%^$#@~`"); @@ -139,8 +153,8 @@ const ASCII_PUNCTUATION = makeBoolArray("(){}[]<>.,;:'\"!?+-=*/\\|&%^$#@~`"); * Check if a character is punctuation. */ export function isPunctuationChar(char: string): boolean { - const code = char.codePointAt(0) || 0; - return ASCII_PUNCTUATION[code] ?? false; + const code = char.codePointAt(0) ?? 0; + return code < 128 && ASCII_PUNCTUATION[code] === 1; } export type WordNavKind = "whitespace" | "delimiter" | "cjk" | "word" | "other";