diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts index 86dba54a5..de22c1ea1 100644 --- a/frontend/src/ts/test/test-ui.ts +++ b/frontend/src/ts/test/test-ui.ts @@ -217,7 +217,7 @@ async function joinOverlappingHints( activeWordLetters: NodeListOf, hintElements: HTMLCollection, ): Promise { - const isWordRightToLeft = Strings.isWordRightToLeft( + const [isWordRightToLeft, _isFullMatch] = Strings.isWordRightToLeft( TestWords.words.getCurrent(), TestState.isLanguageRightToLeft, TestState.isDirectionReversed, diff --git a/frontend/src/ts/utils/caret.ts b/frontend/src/ts/utils/caret.ts index 60629c6bd..11935474a 100644 --- a/frontend/src/ts/utils/caret.ts +++ b/frontend/src/ts/utils/caret.ts @@ -405,7 +405,7 @@ export class Caret { isLanguageRightToLeft: boolean; isDirectionReversed: boolean; }): { left: number; top: number; width: number } { - const isWordRTL = isWordRightToLeft( + const [isWordRTL, isFullMatch] = isWordRightToLeft( options.wordText, options.isLanguageRightToLeft, options.isDirectionReversed, @@ -455,7 +455,7 @@ export class Caret { // yes, this is all super verbose, but its easier to maintain and understand if (isWordRTL) { - if (options.wordText) options.word.addClass("wordRtl"); + if (isFullMatch) options.word.addClass("wordRtl"); let afterLetterCorrection = 0; if (options.side === "afterLetter") { if (this.isFullWidth()) { diff --git a/frontend/src/ts/utils/strings.ts b/frontend/src/ts/utils/strings.ts index 993f49682..bf9e79f3f 100644 --- a/frontend/src/ts/utils/strings.ts +++ b/frontend/src/ts/utils/strings.ts @@ -214,23 +214,24 @@ export function replaceControlCharacters(textToClear: string): string { * @param word the word to check for RTL characters * @returns true if the word contains RTL characters, false otherwise */ -function hasRTLCharacters(word: string): boolean { +function hasRTLCharacters(word: string): [boolean, number] { if (!word || word.length === 0) { - return false; + return [false, 0]; } // This covers Arabic, Farsi, Urdu, and other RTL scripts const rtlPattern = - /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/; + /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]+/; - return rtlPattern.test(word); + const result = rtlPattern.exec(word); + return [result !== null, result?.[0].length ?? 0]; } /** * Cache for word direction to avoid repeated calculations per word * Keyed by the stripped core of the word; can be manually cleared when needed */ -let wordDirectionCache: Map = new Map(); +let wordDirectionCache: Map = new Map(); export function clearWordDirectionCache(): void { wordDirectionCache.clear(); @@ -240,25 +241,31 @@ export function isWordRightToLeft( word: string | undefined, languageRTL: boolean, reverseDirection?: boolean, -): boolean { +): [boolean, boolean] { if (word === undefined || word.length === 0) { - return reverseDirection ? !languageRTL : languageRTL; + return reverseDirection ? [!languageRTL, false] : [languageRTL, false]; } // Strip leading/trailing punctuation and whitespace so attached opposite-direction // punctuation like "word؟" or "،word" doesn't flip the direction detection // and if only punctuation/symbols/whitespace, use main language direction const core = word.replace(/^[\p{P}\p{S}\s]+|[\p{P}\p{S}\s]+$/gu, ""); - if (core.length === 0) return reverseDirection ? !languageRTL : languageRTL; + if (core.length === 0) + return reverseDirection ? [!languageRTL, false] : [languageRTL, false]; // cache by core to handle variants like "word" vs "word؟" const cached = wordDirectionCache.get(core); - if (cached !== undefined) return reverseDirection ? !cached : cached; + if (cached !== undefined) + return reverseDirection + ? [!cached[0], false] + : [cached[0], cached[1] === word.length]; const result = hasRTLCharacters(core); wordDirectionCache.set(core, result); - return reverseDirection ? !result : result; + return reverseDirection + ? [!result[0], false] + : [result[0], result[1] === word.length]; } export const CHAR_EQUIVALENCE_SETS = [