mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-12-29 03:20:46 +08:00
Merge b8f66e461b into 5d169e933a
This commit is contained in:
commit
7afeebfe65
5 changed files with 57 additions and 39 deletions
|
|
@ -265,7 +265,7 @@ describe("string utils", () => {
|
|||
] as const)(
|
||||
"should return %s for word '%s' (%s)",
|
||||
(expected: boolean, word: string, _description: string) => {
|
||||
expect(Strings.__testing.hasRTLCharacters(word)).toBe(expected);
|
||||
expect(Strings.__testing.hasRTLCharacters(word)[0]).toBe(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
@ -321,27 +321,27 @@ describe("string utils", () => {
|
|||
languageRTL: boolean,
|
||||
_description: string,
|
||||
) => {
|
||||
expect(Strings.isWordRightToLeft(word, languageRTL)).toBe(expected);
|
||||
expect(Strings.isWordRightToLeft(word, languageRTL)[0]).toBe(expected);
|
||||
},
|
||||
);
|
||||
|
||||
it("should return languageRTL for undefined word", () => {
|
||||
expect(Strings.isWordRightToLeft(undefined, false)).toBe(false);
|
||||
expect(Strings.isWordRightToLeft(undefined, true)).toBe(true);
|
||||
expect(Strings.isWordRightToLeft(undefined, false)[0]).toBe(false);
|
||||
expect(Strings.isWordRightToLeft(undefined, true)[0]).toBe(true);
|
||||
});
|
||||
|
||||
// testing reverseDirection
|
||||
it("should return true for LTR word with reversed direction", () => {
|
||||
expect(Strings.isWordRightToLeft("hello", false, true)).toBe(true);
|
||||
expect(Strings.isWordRightToLeft("hello", true, true)).toBe(true);
|
||||
expect(Strings.isWordRightToLeft("hello", false, true)[0]).toBe(true);
|
||||
expect(Strings.isWordRightToLeft("hello", true, true)[0]).toBe(true);
|
||||
});
|
||||
it("should return false for RTL word with reversed direction", () => {
|
||||
expect(Strings.isWordRightToLeft("مرحبا", true, true)).toBe(false);
|
||||
expect(Strings.isWordRightToLeft("مرحبا", false, true)).toBe(false);
|
||||
expect(Strings.isWordRightToLeft("مرحبا", true, true)[0]).toBe(false);
|
||||
expect(Strings.isWordRightToLeft("مرحبا", false, true)[0]).toBe(false);
|
||||
});
|
||||
it("should return reverse of languageRTL for undefined word with reversed direction", () => {
|
||||
expect(Strings.isWordRightToLeft(undefined, false, true)).toBe(true);
|
||||
expect(Strings.isWordRightToLeft(undefined, true, true)).toBe(false);
|
||||
expect(Strings.isWordRightToLeft(undefined, false, true)[0]).toBe(true);
|
||||
expect(Strings.isWordRightToLeft(undefined, true, true)[0]).toBe(false);
|
||||
});
|
||||
|
||||
describe("caching", () => {
|
||||
|
|
@ -364,8 +364,8 @@ describe("string utils", () => {
|
|||
it("should use cache for repeated calls", () => {
|
||||
// First call should cache the result (cache miss)
|
||||
const result1 = Strings.isWordRightToLeft("hello", false);
|
||||
expect(result1).toBe(false);
|
||||
expect(mapSetSpy).toHaveBeenCalledWith("hello", false);
|
||||
expect(result1[0]).toBe(false);
|
||||
expect(mapSetSpy).toHaveBeenCalledWith("hello", [false, 0]);
|
||||
|
||||
// Reset spies to check second call
|
||||
mapGetSpy.mockClear();
|
||||
|
|
@ -373,7 +373,7 @@ describe("string utils", () => {
|
|||
|
||||
// Second call should use cache (cache hit)
|
||||
const result2 = Strings.isWordRightToLeft("hello", false);
|
||||
expect(result2).toBe(false);
|
||||
expect(result2[0]).toBe(false);
|
||||
expect(mapGetSpy).toHaveBeenCalledWith("hello");
|
||||
expect(mapSetSpy).not.toHaveBeenCalled(); // Should not set again
|
||||
|
||||
|
|
@ -382,7 +382,7 @@ describe("string utils", () => {
|
|||
mapSetSpy.mockClear();
|
||||
|
||||
const result3 = Strings.isWordRightToLeft("hello", true);
|
||||
expect(result3).toBe(false); // Still false because "hello" is LTR regardless of language
|
||||
expect(result3[0]).toBe(false); // Still false because "hello" is LTR regardless of language
|
||||
expect(mapGetSpy).toHaveBeenCalledWith("hello");
|
||||
expect(mapSetSpy).not.toHaveBeenCalled(); // Should not set again
|
||||
});
|
||||
|
|
@ -390,15 +390,15 @@ describe("string utils", () => {
|
|||
it("should cache based on core word without punctuation", () => {
|
||||
// First call should cache the result for core "hello"
|
||||
const result1 = Strings.isWordRightToLeft("hello", false);
|
||||
expect(result1).toBe(false);
|
||||
expect(mapSetSpy).toHaveBeenCalledWith("hello", false);
|
||||
expect(result1[0]).toBe(false);
|
||||
expect(mapSetSpy).toHaveBeenCalledWith("hello", [false, 0]);
|
||||
|
||||
mapGetSpy.mockClear();
|
||||
mapSetSpy.mockClear();
|
||||
|
||||
// These should all use the same cache entry since they have the same core
|
||||
const result2 = Strings.isWordRightToLeft("hello!", false);
|
||||
expect(result2).toBe(false);
|
||||
expect(result2[0]).toBe(false);
|
||||
expect(mapGetSpy).toHaveBeenCalledWith("hello");
|
||||
expect(mapSetSpy).not.toHaveBeenCalled();
|
||||
|
||||
|
|
@ -406,7 +406,7 @@ describe("string utils", () => {
|
|||
mapSetSpy.mockClear();
|
||||
|
||||
const result3 = Strings.isWordRightToLeft("!hello", false);
|
||||
expect(result3).toBe(false);
|
||||
expect(result3[0]).toBe(false);
|
||||
expect(mapGetSpy).toHaveBeenCalledWith("hello");
|
||||
expect(mapSetSpy).not.toHaveBeenCalled();
|
||||
|
||||
|
|
@ -414,7 +414,7 @@ describe("string utils", () => {
|
|||
mapSetSpy.mockClear();
|
||||
|
||||
const result4 = Strings.isWordRightToLeft("!hello!", false);
|
||||
expect(result4).toBe(false);
|
||||
expect(result4[0]).toBe(false);
|
||||
expect(mapGetSpy).toHaveBeenCalledWith("hello");
|
||||
expect(mapSetSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
@ -422,7 +422,7 @@ describe("string utils", () => {
|
|||
it("should handle cache clearing", () => {
|
||||
// Cache a result
|
||||
Strings.isWordRightToLeft("test", false);
|
||||
expect(mapSetSpy).toHaveBeenCalledWith("test", false);
|
||||
expect(mapSetSpy).toHaveBeenCalledWith("test", [false, 0]);
|
||||
|
||||
// Clear cache
|
||||
Strings.clearWordDirectionCache();
|
||||
|
|
@ -434,23 +434,23 @@ describe("string utils", () => {
|
|||
|
||||
// Should work normally after cache clear (cache miss again)
|
||||
const result = Strings.isWordRightToLeft("test", false);
|
||||
expect(result).toBe(false);
|
||||
expect(mapSetSpy).toHaveBeenCalledWith("test", false);
|
||||
expect(result[0]).toBe(false);
|
||||
expect(mapSetSpy).toHaveBeenCalledWith("test", [false, 0]);
|
||||
});
|
||||
|
||||
it("should demonstrate cache miss vs cache hit behavior", () => {
|
||||
// Test cache miss - first time seeing this word
|
||||
const result1 = Strings.isWordRightToLeft("unique", false);
|
||||
expect(result1).toBe(false);
|
||||
expect(result1[0]).toBe(false);
|
||||
expect(mapGetSpy).toHaveBeenCalledWith("unique");
|
||||
expect(mapSetSpy).toHaveBeenCalledWith("unique", false);
|
||||
expect(mapSetSpy).toHaveBeenCalledWith("unique", [false, 0]);
|
||||
|
||||
mapGetSpy.mockClear();
|
||||
mapSetSpy.mockClear();
|
||||
|
||||
// Test cache hit - same word again
|
||||
const result2 = Strings.isWordRightToLeft("unique", false);
|
||||
expect(result2).toBe(false);
|
||||
expect(result2[0]).toBe(false);
|
||||
expect(mapGetSpy).toHaveBeenCalledWith("unique");
|
||||
expect(mapSetSpy).not.toHaveBeenCalled(); // No cache set on hit
|
||||
|
||||
|
|
@ -459,9 +459,9 @@ describe("string utils", () => {
|
|||
|
||||
// Test cache miss - different word
|
||||
const result3 = Strings.isWordRightToLeft("different", false);
|
||||
expect(result3).toBe(false);
|
||||
expect(result3[0]).toBe(false);
|
||||
expect(mapGetSpy).toHaveBeenCalledWith("different");
|
||||
expect(mapSetSpy).toHaveBeenCalledWith("different", false);
|
||||
expect(mapSetSpy).toHaveBeenCalledWith("different", [false, 0]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -319,6 +319,10 @@
|
|||
&.rightToLeftTest {
|
||||
//flex-direction: row-reverse; // no need for hacking 😉, CSS fully support right-to-left languages
|
||||
direction: rtl;
|
||||
|
||||
.wordRtl {
|
||||
unicode-bidi: bidi-override;
|
||||
}
|
||||
}
|
||||
&.withLigatures {
|
||||
.word {
|
||||
|
|
@ -789,6 +793,10 @@
|
|||
&.rightToLeftTest {
|
||||
//flex-direction: row-reverse; // no need for hacking 😉, CSS fully support right-to-left languages
|
||||
direction: rtl;
|
||||
|
||||
.wordRtl {
|
||||
unicode-bidi: bidi-override;
|
||||
}
|
||||
}
|
||||
&.withLigatures {
|
||||
.word {
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ async function joinOverlappingHints(
|
|||
activeWordLetters: NodeListOf<Element>,
|
||||
hintElements: HTMLCollection,
|
||||
): Promise<void> {
|
||||
const isWordRightToLeft = Strings.isWordRightToLeft(
|
||||
const [isWordRightToLeft, _isFullMatch] = Strings.isWordRightToLeft(
|
||||
TestWords.words.getCurrent(),
|
||||
TestState.isLanguageRightToLeft,
|
||||
TestState.isDirectionReversed,
|
||||
|
|
|
|||
|
|
@ -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,6 +455,7 @@ export class Caret {
|
|||
|
||||
// yes, this is all super verbose, but its easier to maintain and understand
|
||||
if (isWordRTL) {
|
||||
if (isFullMatch) options.word.addClass("wordRtl");
|
||||
let afterLetterCorrection = 0;
|
||||
if (options.side === "afterLetter") {
|
||||
if (this.isFullWidth()) {
|
||||
|
|
|
|||
|
|
@ -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<string, boolean> = new Map();
|
||||
let wordDirectionCache: Map<string, [boolean, number]> = new Map();
|
||||
|
||||
export function clearWordDirectionCache(): void {
|
||||
wordDirectionCache.clear();
|
||||
|
|
@ -240,25 +241,33 @@ 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 = [
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue