Skip to content

Commit c95b6bd

Browse files
committed
impr(input): extract visual equivalence check to a util function
!nuf also adds greek characters closes monkeytypegame#7036
1 parent 1cf4b07 commit c95b6bd

File tree

3 files changed

+78
-31
lines changed

3 files changed

+78
-31
lines changed

frontend/__tests__/utils/strings.spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,4 +465,44 @@ describe("string utils", () => {
465465
});
466466
});
467467
});
468+
469+
describe("areCharactersVisuallyEqual", () => {
470+
it("should return true for identical characters", () => {
471+
expect(Strings.areCharactersVisuallyEqual("a", "a")).toBe(true);
472+
expect(Strings.areCharactersVisuallyEqual("!", "!")).toBe(true);
473+
});
474+
475+
it("should return false for different characters", () => {
476+
expect(Strings.areCharactersVisuallyEqual("a", "b")).toBe(false);
477+
expect(Strings.areCharactersVisuallyEqual("!", "?")).toBe(false);
478+
});
479+
480+
it("should return true for equivalent apostrophe variants", () => {
481+
expect(Strings.areCharactersVisuallyEqual("'", "'")).toBe(true);
482+
expect(Strings.areCharactersVisuallyEqual("'", "'")).toBe(true);
483+
expect(Strings.areCharactersVisuallyEqual("'", "ʼ")).toBe(true);
484+
});
485+
486+
it("should return true for equivalent quote variants", () => {
487+
expect(Strings.areCharactersVisuallyEqual('"', '"')).toBe(true);
488+
expect(Strings.areCharactersVisuallyEqual('"', '"')).toBe(true);
489+
expect(Strings.areCharactersVisuallyEqual('"', "„")).toBe(true);
490+
});
491+
492+
it("should return true for equivalent dash variants", () => {
493+
expect(Strings.areCharactersVisuallyEqual("-", "–")).toBe(true);
494+
expect(Strings.areCharactersVisuallyEqual("-", "—")).toBe(true);
495+
expect(Strings.areCharactersVisuallyEqual("–", "—")).toBe(true);
496+
});
497+
498+
it("should return true for equivalent comma variants", () => {
499+
expect(Strings.areCharactersVisuallyEqual(",", "‚")).toBe(true);
500+
});
501+
502+
it("should return false for characters from different equivalence groups", () => {
503+
expect(Strings.areCharactersVisuallyEqual("'", '"')).toBe(false);
504+
expect(Strings.areCharactersVisuallyEqual("-", "'")).toBe(false);
505+
expect(Strings.areCharactersVisuallyEqual(",", '"')).toBe(false);
506+
});
507+
});
468508
});

frontend/src/ts/controllers/input-controller.ts

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
import { tryCatchSync } from "@monkeytype/util/trycatch";
4545
import { canQuickRestart } from "../utils/quick-restart";
4646
import * as PageTransition from "../states/page-transition";
47+
import { areCharactersVisuallyEqual } from "../utils/strings";
4748

4849
let dontInsertSpace = false;
4950
let correctShiftUsed = true;
@@ -405,37 +406,8 @@ function isCharCorrect(char: string, charIndex: number): boolean {
405406
}
406407
}
407408

408-
if (
409-
(char === "’" ||
410-
char === "‘" ||
411-
char === "'" ||
412-
char === "ʼ" ||
413-
char === "׳" ||
414-
char === "ʻ") &&
415-
(originalChar === "’" ||
416-
originalChar === "‘" ||
417-
originalChar === "'" ||
418-
originalChar === "ʼ" ||
419-
originalChar === "׳" ||
420-
originalChar === "ʻ")
421-
) {
422-
return true;
423-
}
424-
425-
if (
426-
(char === `"` || char === "”" || char === "“" || char === "„") &&
427-
(originalChar === `"` ||
428-
originalChar === "”" ||
429-
originalChar === "“" ||
430-
originalChar === "„")
431-
) {
432-
return true;
433-
}
434-
435-
if (
436-
(char === "–" || char === "—" || char === "-") &&
437-
(originalChar === "-" || originalChar === "–" || originalChar === "—")
438-
) {
409+
const visuallyEqual = areCharactersVisuallyEqual(char, originalChar);
410+
if (visuallyEqual) {
439411
return true;
440412
}
441413

frontend/src/ts/utils/strings.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,41 @@ export function isWordRightToLeft(
261261
return reverseDirection ? !result : result;
262262
}
263263

264+
export const CHAR_EQUIVALENCE_MAPS = [
265+
new Map(
266+
["’", "‘", "'", "ʼ", "׳", "ʻ", "᾽", "᾽"].map((char, index) => [char, index])
267+
),
268+
new Map([`"`, "”", "“", "„"].map((char, index) => [char, index])),
269+
new Map(["–", "—", "-", "‐"].map((char, index) => [char, index])),
270+
new Map([",", "‚"].map((char, index) => [char, index])),
271+
];
272+
273+
/**
274+
* Checks if two characters are visually/typographically equivalent for typing purposes.
275+
* This allows users to type different variants of the same character and still be considered correct.
276+
* @param char1 The first character to compare
277+
* @param char2 The second character to compare
278+
* @returns true if the characters are equivalent, false otherwise
279+
*/
280+
export function areCharactersVisuallyEqual(
281+
char1: string,
282+
char2: string
283+
): boolean {
284+
// If characters are exactly the same, they're equivalent
285+
if (char1 === char2) {
286+
return true;
287+
}
288+
289+
// Check each equivalence map
290+
for (const map of CHAR_EQUIVALENCE_MAPS) {
291+
if (map.has(char1) && map.has(char2)) {
292+
return true;
293+
}
294+
}
295+
296+
return false;
297+
}
298+
264299
// Export testing utilities for unit tests
265300
export const __testing = {
266301
hasRTLCharacters,

0 commit comments

Comments
 (0)