From f86f2535616c60fddd8360b0831c5a8b58eb5280 Mon Sep 17 00:00:00 2001 From: Miodec Date: Sun, 23 Nov 2025 18:16:24 +0100 Subject: [PATCH 1/9] fix: non-breaking space behaving like a character --- frontend/__tests__/utils/strings.spec.ts | 10 ++++---- frontend/src/ts/utils/strings.ts | 31 ++++++++++++++---------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/frontend/__tests__/utils/strings.spec.ts b/frontend/__tests__/utils/strings.spec.ts index 3122c746e0bd..ce49da897fd3 100644 --- a/frontend/__tests__/utils/strings.spec.ts +++ b/frontend/__tests__/utils/strings.spec.ts @@ -474,14 +474,14 @@ describe("string utils", () => { ["\u2003", 0x2003, "em space", true], ["\u2009", 0x2009, "thin space", true], [" ", 0x3000, "ideographic space", true], + ["\u00A0", 0x00a0, "non-breaking space", true], + ["\u2007", 0x2007, "figure space", true], + ["\u2008", 0x2008, "punctuation space", true], + ["\u200A", 0x200a, "hair space", true], + ["​", 0x200b, "zero-width space", true], // Should return false for other characters ["\t", 0x0009, "tab", false], - ["\u00A0", 0x00a0, "non-breaking space", false], - ["\u2007", 0x2007, "figure space", false], - ["\u2008", 0x2008, "punctuation space", false], - ["\u200A", 0x200a, "hair space", false], - ["​", 0x200b, "zero-width space", false], ["a", 0x0061, "letter a", false], ["A", 0x0041, "letter A", false], ["1", 0x0031, "digit 1", false], diff --git a/frontend/src/ts/utils/strings.ts b/frontend/src/ts/utils/strings.ts index 9ea94caee40c..576029df0b6a 100644 --- a/frontend/src/ts/utils/strings.ts +++ b/frontend/src/ts/utils/strings.ts @@ -336,19 +336,24 @@ export function isSpace(char: string): boolean { const codePoint = char.codePointAt(0); if (codePoint === undefined) return false; - // Directly typable spaces: - // U+0020 - Regular space (spacebar) - // U+2002 - En space (Option+Space on Mac) - // U+2003 - Em space (Option+Shift+Space on Mac) - // U+2009 - Thin space (various input methods) - // U+3000 - Ideographic space (CJK input methods) - return ( - codePoint === 0x0020 || - codePoint === 0x2002 || - codePoint === 0x2003 || - codePoint === 0x2009 || - codePoint === 0x3000 - ); + const spaces = new Set([ + 0x0020, // Regular space (spacebar) + 0x2002, // En space (Option+Space on Mac) + 0x2003, // Em space (Option+Shift+Space on Mac) + 0x2009, // Thin space (various input methods) + 0x3000, // Ideographic space (CJK input methods) + 0x00a0, // Non-breaking space (Alt+0160 on Windows, Option+Space on Mac) + 0x1680, // Ogham space mark (rare, but included for completeness) + 0x202f, // Narrow no-break space (various input methods) + 0xfeff, // Zero width no-break space (various input methods) + 0x2007, // Figure space (various input methods) + 0x2008, // Punctuation space (various input methods) + 0x2004, // Three-per-em space (various input methods) + 0x200a, // Hair space (various input methods) + 0x200b, // Zero width space (various input methods) + ]); + + return spaces.has(codePoint); } // Export testing utilities for unit tests From 4e7bda023844b7a7b7e18a76dfc0bddb21dd330b Mon Sep 17 00:00:00 2001 From: Miodec Date: Sun, 23 Nov 2025 18:54:06 +0100 Subject: [PATCH 2/9] fix(input): broken accents in safari turns out safari uses a completely different event for composition ending --- frontend/src/ts/input/helpers/input-type.ts | 6 ++--- frontend/src/ts/input/listeners/input.ts | 26 ++++++++++++++------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/frontend/src/ts/input/helpers/input-type.ts b/frontend/src/ts/input/helpers/input-type.ts index e7762ae982f0..b520e1c5a4ee 100644 --- a/frontend/src/ts/input/helpers/input-type.ts +++ b/frontend/src/ts/input/helpers/input-type.ts @@ -1,15 +1,15 @@ export type InsertInputType = | "insertText" | "insertCompositionText" + | "insertFromComposition" | "insertLineBreak"; - export type DeleteInputType = "deleteWordBackward" | "deleteContentBackward"; -export type SupportedInputType = InsertInputType | DeleteInputType; - +type SupportedInputType = InsertInputType | DeleteInputType; const SUPPORTED_INPUT_TYPES: Set = new Set([ "insertText", "insertCompositionText", + "insertFromComposition", "insertLineBreak", "deleteWordBackward", "deleteContentBackward", diff --git a/frontend/src/ts/input/listeners/input.ts b/frontend/src/ts/input/listeners/input.ts index cef71a12a425..f0ccea37bd6c 100644 --- a/frontend/src/ts/input/listeners/input.ts +++ b/frontend/src/ts/input/listeners/input.ts @@ -1,9 +1,6 @@ import { onDelete } from "../handlers/delete"; import { onInsertText } from "../handlers/insert-text"; -import { - isSupportedInputType, - SupportedInputType, -} from "../helpers/input-type"; +import { isSupportedInputType } from "../helpers/input-type"; import { getInputElement } from "../input-element"; import { getLastInsertCompositionTextData, @@ -56,7 +53,10 @@ inputEl.addEventListener("beforeinput", async (event) => { inputType === "deleteContentBackward" ) { onBeforeDelete(event); - } else if (inputType === "insertCompositionText") { + } else if ( + inputType === "insertCompositionText" || + inputType === "insertFromComposition" + ) { // firefox fires this extra event which we dont want to handle if (!event.isComposing) { event.preventDefault(); @@ -80,11 +80,16 @@ inputEl.addEventListener("input", async (event) => { value: (event.target as HTMLInputElement).value, }); + // this shouldnt be neccesary because beforeinput already prevents default + // but some browsers (LIKE SAFARI) seem to ignore that, so just double checking here + if (!isSupportedInputType(event.inputType)) { + event.preventDefault(); + return; + } + const now = performance.now(); - // this is ok to cast because we are preventing default - // in the input listener for unsupported input types - const inputType = event.inputType as SupportedInputType; + const inputType = event.inputType; if ( (inputType === "insertText" && event.data !== null) || @@ -105,7 +110,10 @@ inputEl.addEventListener("input", async (event) => { inputType === "deleteContentBackward" ) { onDelete(inputType); - } else if (inputType === "insertCompositionText") { + } else if ( + inputType === "insertCompositionText" || + inputType === "insertFromComposition" + ) { // in case the data is the same as the last one, just ignore it if (getLastInsertCompositionTextData() !== event.data) { setLastInsertCompositionTextData(event.data ?? ""); From d617513fcf41729214930b0e51fbe5e9d79ffe77 Mon Sep 17 00:00:00 2001 From: Miodec Date: Sun, 23 Nov 2025 19:07:22 +0100 Subject: [PATCH 3/9] chore: add comment --- frontend/src/ts/input/helpers/input-type.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/ts/input/helpers/input-type.ts b/frontend/src/ts/input/helpers/input-type.ts index b520e1c5a4ee..d6ea52b9c97d 100644 --- a/frontend/src/ts/input/helpers/input-type.ts +++ b/frontend/src/ts/input/helpers/input-type.ts @@ -1,7 +1,7 @@ export type InsertInputType = | "insertText" | "insertCompositionText" - | "insertFromComposition" + | "insertFromComposition" // safari firing a deprecated input type - thanks apple! | "insertLineBreak"; export type DeleteInputType = "deleteWordBackward" | "deleteContentBackward"; From eab1737ea9122bfbfdfb0165ffe408e7355c11ca Mon Sep 17 00:00:00 2001 From: Miodec Date: Sun, 23 Nov 2025 19:16:15 +0100 Subject: [PATCH 4/9] fix: alt space highlighting correct word as incorrect also updates tests to just use the implementation instead of mocking it --- .../__tests__/input/helpers/validation.spec.ts | 16 +++++++++------- frontend/src/ts/input/helpers/validation.ts | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/frontend/__tests__/input/helpers/validation.spec.ts b/frontend/__tests__/input/helpers/validation.spec.ts index 51b28969eae6..9554f01c6168 100644 --- a/frontend/__tests__/input/helpers/validation.spec.ts +++ b/frontend/__tests__/input/helpers/validation.spec.ts @@ -14,10 +14,15 @@ vi.mock("../../../src/ts/test/funbox/list", () => ({ findSingleActiveFunboxWithFunction: vi.fn(), })); -vi.mock("../../../src/ts/utils/strings", () => ({ - areCharactersVisuallyEqual: vi.fn(), - isSpace: vi.fn(), -})); +vi.mock("../../../src/ts/utils/strings", async () => { + const actual = await vi.importActual( + "../../../src/ts/utils/strings" + ); + return { + ...actual, + areCharactersVisuallyEqual: vi.fn(), + }; +}); describe("isCharCorrect", () => { beforeEach(() => { @@ -34,7 +39,6 @@ describe("isCharCorrect", () => { null ); (Strings.areCharactersVisuallyEqual as any).mockReturnValue(false); - (Strings.isSpace as any).mockReturnValue(false); }); afterAll(() => { @@ -133,7 +137,6 @@ describe("isCharCorrect", () => { describe("shouldInsertSpaceCharacter", () => { beforeEach(() => { - (Strings.isSpace as any).mockReturnValue(true); replaceConfig({ mode: "time", stopOnError: "off", @@ -147,7 +150,6 @@ describe("shouldInsertSpaceCharacter", () => { }); it("returns null if data is not a space", () => { - (Strings.isSpace as any).mockReturnValue(false); expect( shouldInsertSpaceCharacter({ data: "a", diff --git a/frontend/src/ts/input/helpers/validation.ts b/frontend/src/ts/input/helpers/validation.ts index 4a2398a8dcd4..408b5416eba7 100644 --- a/frontend/src/ts/input/helpers/validation.ts +++ b/frontend/src/ts/input/helpers/validation.ts @@ -25,7 +25,7 @@ export function isCharCorrect(options: { throw new Error("Failed to check if char is correct - data is undefined"); } - if (data === " ") { + if (isSpace(data)) { return inputValue === targetWord; } From 032844d023e4141c93d64501465f3d7ac28825c5 Mon Sep 17 00:00:00 2001 From: Miodec Date: Sun, 23 Nov 2025 19:30:30 +0100 Subject: [PATCH 5/9] feat: add show personal best you can now view the pb for current test settings right above the test words, before starting the test --- .../ts/commandline/commandline-metadata.ts | 6 +++ frontend/src/ts/commandline/lists.ts | 3 ++ frontend/src/ts/config-metadata.ts | 5 ++ frontend/src/ts/constants/default-config.ts | 1 + frontend/src/ts/elements/modes-notice.ts | 47 +++++++++++++------ packages/schemas/src/configs.ts | 5 ++ 6 files changed, 53 insertions(+), 14 deletions(-) diff --git a/frontend/src/ts/commandline/commandline-metadata.ts b/frontend/src/ts/commandline/commandline-metadata.ts index fb105f009b66..4d2d2ebb12af 100644 --- a/frontend/src/ts/commandline/commandline-metadata.ts +++ b/frontend/src/ts/commandline/commandline-metadata.ts @@ -703,6 +703,12 @@ export const commandlineConfigMetadata: CommandlineConfigMetadataObject = { options: "fromSchema", }, }, + showPb: { + subgroup: { + options: "fromSchema", + }, + alias: "pb", + }, monkeyPowerLevel: { alias: "powermode", isVisible: false, diff --git a/frontend/src/ts/commandline/lists.ts b/frontend/src/ts/commandline/lists.ts index 1dbe01c36674..5cc8b61749ba 100644 --- a/frontend/src/ts/commandline/lists.ts +++ b/frontend/src/ts/commandline/lists.ts @@ -52,6 +52,7 @@ const confidenceModeCommand = buildCommandForConfigKey("confidenceMode"); const lazyModeCommand = buildCommandForConfigKey("lazyMode"); const layoutCommand = buildCommandForConfigKey("layout"); const showAverageCommand = buildCommandForConfigKey("showAverage"); +const showPbCommand = buildCommandForConfigKey("showPb"); const keymapLayoutCommand = buildCommandForConfigKey("keymapLayout"); const customThemeCommand = buildCommandForConfigKey("customTheme"); const adsCommand = buildCommandForConfigKey("ads"); @@ -216,6 +217,7 @@ export const commands: CommandsSubgroup = { "showOutOfFocusWarning", "capsLockWarning", showAverageCommand, + showPbCommand, "monkeyPowerLevel", "monkey" ), @@ -377,6 +379,7 @@ const lists = { lazyMode: lazyModeCommand.subgroup, paceCaretMode: paceCaretCommand.subgroup, showAverage: showAverageCommand.subgroup, + showPb: showPbCommand.subgroup, minWpm: minSpeedCommand.subgroup, minAcc: minAccCommand.subgroup, minBurst: MinBurstCommands[0]?.subgroup, diff --git a/frontend/src/ts/config-metadata.ts b/frontend/src/ts/config-metadata.ts index 7dc8726e8678..62650b386c79 100644 --- a/frontend/src/ts/config-metadata.ts +++ b/frontend/src/ts/config-metadata.ts @@ -742,6 +742,11 @@ export const configMetadata: ConfigMetadataObject = { displayString: "show average", changeRequiresRestart: false, }, + showPb: { + icon: "fa-crown", + displayString: "show personal best", + changeRequiresRestart: false, + }, // other (hidden) accountChart: { diff --git a/frontend/src/ts/constants/default-config.ts b/frontend/src/ts/constants/default-config.ts index fa08f54bc6ab..0fb967fb5c02 100644 --- a/frontend/src/ts/constants/default-config.ts +++ b/frontend/src/ts/constants/default-config.ts @@ -97,6 +97,7 @@ const obj: Config = { britishEnglish: false, lazyMode: false, showAverage: "off", + showPb: false, tapeMode: "off", tapeMargin: 50, maxLineWidth: 0, diff --git a/frontend/src/ts/elements/modes-notice.ts b/frontend/src/ts/elements/modes-notice.ts index aa2888565dcd..43c708eca211 100644 --- a/frontend/src/ts/elements/modes-notice.ts +++ b/frontend/src/ts/elements/modes-notice.ts @@ -9,8 +9,8 @@ import { isAuthenticated } from "../firebase"; import * as CustomTextState from "../states/custom-text-name"; import { getLanguageDisplayString } from "../utils/strings"; import Format from "../utils/format"; -import { getActiveFunboxNames } from "../test/funbox/list"; -import { escapeHTML } from "../utils/misc"; +import { getActiveFunboxes, getActiveFunboxNames } from "../test/funbox/list"; +import { escapeHTML, getMode2 } from "../utils/misc"; ConfigEvent.subscribe((eventKey) => { const configKeys: ConfigEvent.ConfigEventKey[] = [ @@ -26,6 +26,7 @@ ConfigEvent.subscribe((eventKey) => { "confidenceMode", "layout", "showAverage", + "showPb", "typingSpeedUnit", "quickRestart", "customPolyglot", @@ -191,6 +192,36 @@ export async function update(): Promise { } } + if (Config.showPb) { + if (!isAuthenticated()) { + return; + } + const mode2 = getMode2(Config, TestWords.currentQuote); + const pb = await DB.getLocalPB( + Config.mode, + mode2, + Config.punctuation, + Config.numbers, + Config.language, + Config.difficulty, + Config.lazyMode, + getActiveFunboxes() + ); + + let str = "no pb"; + + if (pb !== undefined) { + str = `${Format.typingSpeed(pb.wpm, { + showDecimalPlaces: true, + suffix: ` ${Config.typingSpeedUnit}`, + })} ${pb?.acc}% acc`; + } + + $(".pageTest #testModesNotice").append( + `` + ); + } + if (Config.minWpm !== "off") { $(".pageTest #testModesNotice").append( `