Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions frontend/__tests__/utils/strings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
34 changes: 3 additions & 31 deletions frontend/src/ts/controllers/input-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down
35 changes: 35 additions & 0 deletions frontend/src/ts/utils/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading