diff --git a/frontend/__tests__/utils/strings.spec.ts b/frontend/__tests__/utils/strings.spec.ts index 78ee18de2b8c..a84029d13a1a 100644 --- a/frontend/__tests__/utils/strings.spec.ts +++ b/frontend/__tests__/utils/strings.spec.ts @@ -465,4 +465,44 @@ describe("string utils", () => { }); }); }); + + describe("areCharactersVisuallyEqual", () => { + it("should return true for identical characters", () => { + expect(Strings.areCharactersVisuallyEqual("a", "a")).toBe(true); + expect(Strings.areCharactersVisuallyEqual("!", "!")).toBe(true); + }); + + it("should return false for different characters", () => { + expect(Strings.areCharactersVisuallyEqual("a", "b")).toBe(false); + expect(Strings.areCharactersVisuallyEqual("!", "?")).toBe(false); + }); + + it("should return true for equivalent apostrophe variants", () => { + expect(Strings.areCharactersVisuallyEqual("'", "'")).toBe(true); + expect(Strings.areCharactersVisuallyEqual("'", "'")).toBe(true); + expect(Strings.areCharactersVisuallyEqual("'", "ʼ")).toBe(true); + }); + + it("should return true for equivalent quote variants", () => { + expect(Strings.areCharactersVisuallyEqual('"', '"')).toBe(true); + expect(Strings.areCharactersVisuallyEqual('"', '"')).toBe(true); + expect(Strings.areCharactersVisuallyEqual('"', "„")).toBe(true); + }); + + it("should return true for equivalent dash variants", () => { + expect(Strings.areCharactersVisuallyEqual("-", "–")).toBe(true); + expect(Strings.areCharactersVisuallyEqual("-", "—")).toBe(true); + expect(Strings.areCharactersVisuallyEqual("–", "—")).toBe(true); + }); + + it("should return true for equivalent comma variants", () => { + expect(Strings.areCharactersVisuallyEqual(",", "‚")).toBe(true); + }); + + it("should return false for characters from different equivalence groups", () => { + expect(Strings.areCharactersVisuallyEqual("'", '"')).toBe(false); + expect(Strings.areCharactersVisuallyEqual("-", "'")).toBe(false); + expect(Strings.areCharactersVisuallyEqual(",", '"')).toBe(false); + }); + }); }); diff --git a/frontend/src/ts/controllers/input-controller.ts b/frontend/src/ts/controllers/input-controller.ts index 07ece1feba5a..74de37c171de 100644 --- a/frontend/src/ts/controllers/input-controller.ts +++ b/frontend/src/ts/controllers/input-controller.ts @@ -44,6 +44,7 @@ import { import { tryCatchSync } from "@monkeytype/util/trycatch"; import { canQuickRestart } from "../utils/quick-restart"; import * as PageTransition from "../states/page-transition"; +import { areCharactersVisuallyEqual } from "../utils/strings"; let dontInsertSpace = false; let correctShiftUsed = true; @@ -405,37 +406,8 @@ function isCharCorrect(char: string, charIndex: number): boolean { } } - if ( - (char === "’" || - char === "‘" || - char === "'" || - char === "ʼ" || - char === "׳" || - char === "ʻ") && - (originalChar === "’" || - originalChar === "‘" || - originalChar === "'" || - originalChar === "ʼ" || - originalChar === "׳" || - originalChar === "ʻ") - ) { - return true; - } - - if ( - (char === `"` || char === "”" || char === "“" || char === "„") && - (originalChar === `"` || - originalChar === "”" || - originalChar === "“" || - originalChar === "„") - ) { - return true; - } - - if ( - (char === "–" || char === "—" || char === "-") && - (originalChar === "-" || originalChar === "–" || originalChar === "—") - ) { + const visuallyEqual = areCharactersVisuallyEqual(char, originalChar); + if (visuallyEqual) { return true; } diff --git a/frontend/src/ts/utils/strings.ts b/frontend/src/ts/utils/strings.ts index 0786bd3e41d0..22e567dd6f81 100644 --- a/frontend/src/ts/utils/strings.ts +++ b/frontend/src/ts/utils/strings.ts @@ -261,6 +261,41 @@ export function isWordRightToLeft( return reverseDirection ? !result : result; } +export const CHAR_EQUIVALENCE_MAPS = [ + new Map( + ["’", "‘", "'", "ʼ", "׳", "ʻ", "᾽", "᾽"].map((char, index) => [char, index]) + ), + new Map([`"`, "”", "“", "„"].map((char, index) => [char, index])), + new Map(["–", "—", "-", "‐"].map((char, index) => [char, index])), + new Map([",", "‚"].map((char, index) => [char, index])), +]; + +/** + * Checks if two characters are visually/typographically equivalent for typing purposes. + * This allows users to type different variants of the same character and still be considered correct. + * @param char1 The first character to compare + * @param char2 The second character to compare + * @returns true if the characters are equivalent, false otherwise + */ +export function areCharactersVisuallyEqual( + char1: string, + char2: string +): boolean { + // If characters are exactly the same, they're equivalent + if (char1 === char2) { + return true; + } + + // Check each equivalence map + for (const map of CHAR_EQUIVALENCE_MAPS) { + if (map.has(char1) && map.has(char2)) { + return true; + } + } + + return false; +} + // Export testing utilities for unit tests export const __testing = { hasRTLCharacters,