diff --git a/frontend/__tests__/test/events/stats.spec.ts b/frontend/__tests__/test/events/stats.spec.ts index 5569188a25de..fcaa3bc1c653 100644 --- a/frontend/__tests__/test/events/stats.spec.ts +++ b/frontend/__tests__/test/events/stats.spec.ts @@ -17,16 +17,25 @@ vi.mock("../../../src/ts/config/store", () => ({ })); vi.mock("../../../src/ts/test/test-words", () => { - const list: string[] = []; + type CommitChar = " " | "\n" | ""; + type Word = { text: string; textWithCommit: string; commit: CommitChar }; + const list: Word[] = []; return { words: { list, - getText(i?: number) { - if (i === undefined) return list; - return list[i]; + push(word: string) { + let commit: CommitChar = ""; + if (word.endsWith(" ")) { + commit = " "; + word = word.slice(0, -1); + } else if (word.endsWith("\n")) { + commit = "\n"; + word = word.slice(0, -1); + } + list.push({ text: word, textWithCommit: word + commit, commit }); }, - getCurrentText() { - return list[list.length - 1] ?? ""; + reset() { + list.length = 0; }, }, }; @@ -79,6 +88,10 @@ import { Keycode } from "../../../src/ts/constants/keys"; import * as TestState from "../../../src/ts/test/test-state"; import { words as TestWords } from "../../../src/ts/test/test-words"; +function pushWords(...words: string[]): void { + words.forEach((word, i) => TestWords.push(word, i)); +} + function keyDown(code: Keycode = "KeyA"): KeydownEventData { return { code }; } @@ -170,7 +183,7 @@ describe("stats.ts", () => { (Config as { words: number }).words = 25; (Config as { time: number }).time = 0; (TestState as { activeWordIndex: number }).activeWordIndex = 0; - TestWords.list.length = 0; + TestWords.reset(); inputPerWord.clear(); }); @@ -695,7 +708,7 @@ describe("stats.ts", () => { // sentinel + "=" together, which monkeytype interprets as crossing the // word boundary → goToPreviousWord. Word 1 is abandoned with leftover // "=" residue in its event stream; its final state should still be "". - TestWords.list.push("hello", "leave"); + pushWords("hello", "leave"); logTestEvent("timer", 1000, timer("start", 0)); logTestEvent( @@ -1014,28 +1027,28 @@ describe("stats.ts", () => { }); it("returns word without trailing space when it ends with newline", () => { - TestWords.list.push("hello\n"); + pushWords("hello\n"); expect( statsTesting.getTargetWord(buildEventLog(), 0, "hello", false), ).toBe("hello\n"); }); it("appends trailing space for non-last word", () => { - TestWords.list.push("hello"); + pushWords("hello"); expect( statsTesting.getTargetWord(buildEventLog(), 0, "hello", false), ).toBe("hello "); }); it("does not append trailing space for last word", () => { - TestWords.list.push("hello"); + pushWords("hello"); expect( statsTesting.getTargetWord(buildEventLog(), 0, "hello", true), ).toBe("hello"); }); it("does not append trailing space when nospace funbox is active", () => { - TestWords.list.push("hello"); + pushWords("hello"); (Config as { funbox: string[] }).funbox = ["nospace"]; expect( statsTesting.getTargetWord(buildEventLog(), 0, "hello", false), @@ -1043,7 +1056,7 @@ describe("stats.ts", () => { }); it("does not append trailing space when underscore_spaces funbox is active", () => { - TestWords.list.push("hello"); + pushWords("hello"); (Config as { funbox: string[] }).funbox = ["underscore_spaces"]; expect( statsTesting.getTargetWord(buildEventLog(), 0, "hello", false), @@ -1053,7 +1066,7 @@ describe("stats.ts", () => { describe("getChars", () => { it("counts all correct for a perfectly typed word", () => { - TestWords.list.push("hello"); + pushWords("hello"); (TestState as { activeWordIndex: number }).activeWordIndex = 0; logTestEvent("timer", 1000, timer("start", 0)); @@ -1074,7 +1087,7 @@ describe("stats.ts", () => { }); it("counts incorrect chars", () => { - TestWords.list.push("ab"); + pushWords("ab"); (TestState as { activeWordIndex: number }).activeWordIndex = 0; logTestEvent("timer", 1000, timer("start", 0)); @@ -1095,7 +1108,7 @@ describe("stats.ts", () => { }); it("counts extra chars", () => { - TestWords.list.push("ab"); + pushWords("ab"); (TestState as { activeWordIndex: number }).activeWordIndex = 0; logTestEvent("timer", 1000, timer("start", 0)); @@ -1120,7 +1133,7 @@ describe("stats.ts", () => { }); it("counts missed chars for completed non-last words", () => { - TestWords.list.push("hello", "world"); + pushWords("hello", "world"); (TestState as { activeWordIndex: number }).activeWordIndex = 1; logTestEvent("timer", 1000, timer("start", 0)); @@ -1166,7 +1179,7 @@ describe("stats.ts", () => { it("credits a word committed with an IME full-width space", () => { // Japanese IME commits words with the ideographic space U+3000, while the // target word separator is a regular space — normalize so it still counts - TestWords.list.push("しり", "かこ"); + pushWords("しり", "かこ"); (TestState as { activeWordIndex: number }).activeWordIndex = 1; logTestEvent("timer", 1000, timer("start", 0)); @@ -1206,7 +1219,7 @@ describe("stats.ts", () => { describe("getWpmHistory", () => { it("returns wpm at each timer boundary", () => { - TestWords.list.push("hello"); + pushWords("hello"); (TestState as { activeWordIndex: number }).activeWordIndex = 0; logTestEvent("timer", 1000, timer("start", 0)); @@ -1227,7 +1240,7 @@ describe("stats.ts", () => { }); it("returns cumulative wpm across boundaries", () => { - TestWords.list.push("ab", "cd"); + pushWords("ab", "cd"); (TestState as { activeWordIndex: number }).activeWordIndex = 1; logTestEvent("timer", 1000, timer("start", 0)); @@ -1272,7 +1285,7 @@ describe("stats.ts", () => { it("counts non-last word as correct without trailing space when nospace funbox is active", () => { (Config as { funbox: string[] }).funbox = ["nospace"]; - TestWords.list.push("ab", "cd"); + pushWords("ab", "cd"); (TestState as { activeWordIndex: number }).activeWordIndex = 1; logTestEvent("timer", 1000, timer("start", 0)); @@ -1306,7 +1319,7 @@ describe("stats.ts", () => { }); it("counts multiline word as correct when target ends in newline", () => { - TestWords.list.push("hello\n", "world"); + pushWords("hello\n", "world"); (TestState as { activeWordIndex: number }).activeWordIndex = 1; logTestEvent("timer", 1000, timer("start", 0)); diff --git a/frontend/src/ts/commandline/lists/result-screen.ts b/frontend/src/ts/commandline/lists/result-screen.ts index df7fe2a203ca..b07bc245e8bd 100644 --- a/frontend/src/ts/commandline/lists/result-screen.ts +++ b/frontend/src/ts/commandline/lists/result-screen.ts @@ -148,7 +148,11 @@ const commands: Command[] = [ const words = Config.mode === "zen" ? inputHistory.join("") - : TestWords.words.list.slice(0, inputHistory.length).join(" "); + : TestWords.words + .get() + .slice(0, inputHistory.length) + .map((word) => word.text) + .join(" "); navigator.clipboard.writeText(words).then( () => { diff --git a/frontend/src/ts/elements/caret.ts b/frontend/src/ts/elements/caret.ts index 15c326971653..8d54c3f3d4dc 100644 --- a/frontend/src/ts/elements/caret.ts +++ b/frontend/src/ts/elements/caret.ts @@ -287,7 +287,7 @@ export class Caret { const word = wordsCache.qs( `.word[data-wordindex="${options.wordIndex}"]`, ); - const wordText = TestWords.words.getText(options.wordIndex) ?? ""; + const wordText = TestWords.words.get(options.wordIndex)?.display ?? ""; const wordLength = Array.from(wordText).length; // caret can be either on the left side of the target letter or the right diff --git a/frontend/src/ts/input/handlers/before-delete.ts b/frontend/src/ts/input/handlers/before-delete.ts index b8462972ee0a..50d84df13ea0 100644 --- a/frontend/src/ts/input/handlers/before-delete.ts +++ b/frontend/src/ts/input/handlers/before-delete.ts @@ -50,9 +50,10 @@ export function onBeforeDelete(event: InputEvent): void { } const confidence = Config.confidenceMode; + const previousWord = TestWords.words.get(TestState.activeWordIndex - 1); const previousWordCorrect = getInputForWord(TestState.activeWordIndex - 1) === - TestWords.words.getText(TestState.activeWordIndex - 1); + previousWord?.textWithCommit; if (confidence === "on" && inputIsEmpty && !previousWordCorrect) { event.preventDefault(); diff --git a/frontend/src/ts/input/handlers/before-insert-text.ts b/frontend/src/ts/input/handlers/before-insert-text.ts index 3c5f56419e6e..63bd9be0beb6 100644 --- a/frontend/src/ts/input/handlers/before-insert-text.ts +++ b/frontend/src/ts/input/handlers/before-insert-text.ts @@ -30,11 +30,13 @@ export function onBeforeInsertText(data: string): boolean { } const { inputValue } = getInputElementValue(); + const currentWordTextWithCommit = + TestWords.words.getCurrent()?.textWithCommit ?? ""; const dataIsSpace = isSpace(data); const shouldInsertSpaceAsCharacter = shouldInsertSpaceCharacter({ data, inputValue, - targetWord: TestWords.words.getCurrentText(), + targetWord: currentWordTextWithCommit, }); //prevent space from being inserted if input is empty @@ -60,7 +62,7 @@ export function onBeforeInsertText(data: string): boolean { // block input if the word is too long const inputLimit = - Config.mode === "zen" ? 30 : TestWords.words.getCurrentText().length + 20; + Config.mode === "zen" ? 30 : currentWordTextWithCommit.length + 20; const overLimit = getCurrentInput().length >= inputLimit; if (overLimit && (shouldInsertSpaceAsCharacter === true || !dataIsSpace)) { console.error("Hitting word limit"); @@ -71,7 +73,7 @@ export function onBeforeInsertText(data: string): boolean { // this will not work for the first word of each line, but that has a low chance of happening const dataIsNotFalsy = data !== null && data !== ""; const inputIsLongerThanOrEqualToWord = - getCurrentInput().length >= TestWords.words.getCurrentText().length; + getCurrentInput().length >= currentWordTextWithCommit.length; if ( !SlowTimer.get() && // don't do this check if slow timer is active diff --git a/frontend/src/ts/input/handlers/delete.ts b/frontend/src/ts/input/handlers/delete.ts index 8eedfb2c1a31..14717b4daa58 100644 --- a/frontend/src/ts/input/handlers/delete.ts +++ b/frontend/src/ts/input/handlers/delete.ts @@ -18,8 +18,8 @@ export function onDelete(inputType: DeleteInputType, now: number): void { const beforeDeleteOnlyTabs = /^\t*$/.test(inputBeforeDelete); const allTabsCorrect = TestWords.words - .getCurrentText() - .startsWith(inputAfterDelete); + .getCurrent() + ?.textWithCommit.startsWith(inputAfterDelete); //special check for code languages if ( diff --git a/frontend/src/ts/input/handlers/insert-text.ts b/frontend/src/ts/input/handlers/insert-text.ts index b5a9b4f13a0a..03289668628b 100644 --- a/frontend/src/ts/input/handlers/insert-text.ts +++ b/frontend/src/ts/input/handlers/insert-text.ts @@ -87,7 +87,8 @@ export async function onInsertText(options: OnInsertTextParams): Promise { const charOverride = charOverrides.get(options.data); if ( charOverride !== undefined && - TestWords.words.getCurrentText()[getCurrentInput().length] !== options.data + TestWords.words.getCurrent()?.textWithCommit[getCurrentInput().length] !== + options.data ) { // replace the data with the override setInputElementValue( @@ -107,8 +108,9 @@ export async function onInsertText(options: OnInsertTextParams): Promise { for (const [targetChar, overrideChar] of languageOverrides) { if ( options.data === targetChar && - TestWords.words.getCurrentText()[getCurrentInput().length] !== - options.data + TestWords.words.getCurrent()?.textWithCommit[ + getCurrentInput().length + ] !== options.data ) { // replace the data with the override setInputElementValue( @@ -125,7 +127,7 @@ export async function onInsertText(options: OnInsertTextParams): Promise { // input and target word const testInput = getCurrentInput(); - const currentWord = TestWords.words.getCurrentText(); + const currentWord = TestWords.words.getCurrent()?.textWithCommit ?? ""; // if the character is visually equal, replace it with the target character // this ensures all future equivalence checks work correctly @@ -167,7 +169,8 @@ export async function onInsertText(options: OnInsertTextParams): Promise { // word navigation check const noSpaceForce = isFunboxActiveWithProperty("nospace") && - (testInput + data).length === TestWords.words.getCurrentText().length; + (testInput + data).length === + TestWords.words.getCurrent()?.textWithCommit.length; // does this input try to move to the next word (before removeLastChar can block it) const goingToNextWord = ((charIsSpace || charIsNewline) && !shouldInsertSpace) || noSpaceForce; @@ -273,7 +276,7 @@ export async function onInsertText(options: OnInsertTextParams): Promise { */ //this COULD be the next word because we are awaiting goToNextWord - const nextWord = TestWords.words.getCurrentText(); + const nextWord = TestWords.words.getCurrent()?.textWithCommit ?? ""; const doesNextWordHaveTab = /^\t+/.test(nextWord); const isCurrentCharTab = nextWord[getCurrentInput().length] === "\t"; diff --git a/frontend/src/ts/input/helpers/word-navigation.ts b/frontend/src/ts/input/helpers/word-navigation.ts index ba34c4267c7a..cad34a02e7ab 100644 --- a/frontend/src/ts/input/helpers/word-navigation.ts +++ b/frontend/src/ts/input/helpers/word-navigation.ts @@ -55,9 +55,12 @@ export async function goToNextWord({ ret.lastBurst = burst; } - PaceCaret.handleSpace(correctInsert, TestWords.words.getCurrentText()); + PaceCaret.handleSpace( + correctInsert, + TestWords.words.getCurrent()?.textWithCommit ?? "", + ); - const nextWord = TestWords.words.getText(TestState.activeWordIndex + 1); + const nextWord = TestWords.words.get(TestState.activeWordIndex + 1)?.text; if (nextWord !== undefined) Funbox.toggleScript(nextWord); const lastWord = TestState.activeWordIndex >= TestWords.words.length - 1; @@ -98,7 +101,7 @@ export function goToPreviousWord( TestState.decreaseActiveWordIndex(); - const word = TestWords.words.getText(TestState.activeWordIndex); + const word = TestWords.words.get(TestState.activeWordIndex)?.text; if (word !== undefined) Funbox.toggleScript(word); const nospaceEnabled = isFunboxActiveWithProperty("nospace"); diff --git a/frontend/src/ts/input/listeners/input.ts b/frontend/src/ts/input/listeners/input.ts index c080c0dee8d2..02948c590545 100644 --- a/frontend/src/ts/input/listeners/input.ts +++ b/frontend/src/ts/input/listeners/input.ts @@ -129,7 +129,7 @@ inputEl.addEventListener("input", async (event) => { const inputPlusComposition = getCurrentInput() + (CompositionState.getData() ?? ""); const inputPlusCompositionIsCorrect = - TestWords.words.getCurrentText() === inputPlusComposition; + TestWords.words.getCurrent()?.textWithCommit === inputPlusComposition; // composition quick end // if the user typed the entire word correctly but is still in composition diff --git a/frontend/src/ts/test/events/data.ts b/frontend/src/ts/test/events/data.ts index 91884985bec0..03d7a81c8628 100644 --- a/frontend/src/ts/test/events/data.ts +++ b/frontend/src/ts/test/events/data.ts @@ -35,7 +35,7 @@ import { isFunboxActiveWithProperty } from "../funbox/active"; export function buildEventLog(): EventLog { const context = { - targetWords: [...TestWords.words.list], + targetWords: [...TestWords.words.get().map((w) => w.textWithCommit)], mode: Config.mode, mode2: getMode2(Config, getCurrentQuote()), koreanStatus: koreanStatus, diff --git a/frontend/src/ts/test/funbox/funbox-functions.ts b/frontend/src/ts/test/funbox/funbox-functions.ts index 73d6422c6cfb..0824ddf34f2e 100644 --- a/frontend/src/ts/test/funbox/funbox-functions.ts +++ b/frontend/src/ts/test/funbox/funbox-functions.ts @@ -52,10 +52,17 @@ export type FunboxFunctions = { async function readAheadHandleKeydown(event: KeyboardEvent): Promise { const currentInput = getCurrentInput(); + const currentWord = TestWords.words.getCurrent(); + + if (!currentWord) { + return; + } + const inputCurrentChar = currentInput.slice(-1); - const wordCurrentChar = TestWords.words - .getCurrentText() - .slice(currentInput.length - 1, currentInput.length); + const wordCurrentChar = currentWord.display.slice( + currentInput.length - 1, + currentInput.length, + ); const isCorrect = inputCurrentChar === wordCurrentChar; if ( @@ -63,7 +70,7 @@ async function readAheadHandleKeydown(event: KeyboardEvent): Promise { !isCorrect && (currentInput !== "" || getInputForWord(TestState.activeWordIndex - 1) !== - TestWords.words.getText(TestState.activeWordIndex - 1) || + TestWords.words.get(TestState.activeWordIndex - 1)?.display || Config.freedomMode) ) { qs("#words")?.addClass("read_ahead_disabled"); @@ -416,7 +423,9 @@ const list: Partial> = { } setTimeout(() => { highlight( - TestWords.words.getCurrentText().charAt(getCurrentInput().length), + TestWords.words + .getCurrent() + ?.text.charAt(getCurrentInput().length) ?? "", ); }, 1); } diff --git a/frontend/src/ts/test/pace-caret.ts b/frontend/src/ts/test/pace-caret.ts index 28f962156672..f6047268d80d 100644 --- a/frontend/src/ts/test/pace-caret.ts +++ b/frontend/src/ts/test/pace-caret.ts @@ -181,7 +181,7 @@ function incrementLetterIndex(): void { if ( settings.currentLetterIndex >= // oxlint-disable-next-line typescript/no-non-null-assertion let it throw if undefined - TestWords.words.getText(settings.currentWordIndex)!.length + 1 + TestWords.words.get(settings.currentWordIndex)!.display.length + 1 ) { //go to the next word settings.currentLetterIndex = 0; @@ -195,8 +195,8 @@ function incrementLetterIndex(): void { //go to the previous word settings.currentLetterIndex = // oxlint-disable-next-line typescript/no-non-null-assertion let it throw if undefined - TestWords.words.getText(settings.currentWordIndex - 1)!.length - - 1; + TestWords.words.get(settings.currentWordIndex - 1)!.display + .length - 1; settings.currentWordIndex--; } settings.correction++; @@ -207,7 +207,7 @@ function incrementLetterIndex(): void { if ( settings.currentLetterIndex >= // oxlint-disable-next-line typescript/no-non-null-assertion let it throw if undefined - TestWords.words.getText(settings.currentWordIndex)!.length + TestWords.words.get(settings.currentWordIndex)!.display.length ) { //go to the next word settings.currentLetterIndex = 0; diff --git a/frontend/src/ts/test/practise-words.ts b/frontend/src/ts/test/practise-words.ts index 978304c28f60..ecc2a1162474 100644 --- a/frontend/src/ts/test/practise-words.ts +++ b/frontend/src/ts/test/practise-words.ts @@ -63,7 +63,7 @@ export function init( let sortableMissedBiwords: [string, string, number][] = []; if (missed === "biwords") { for (let i = 0; i < TestWords.words.length; i++) { - const missedWord = TestWords.words.getText(i); + const missedWord = TestWords.words.get(i)?.text; if (missedWord === undefined) continue; // won't happen, but ts complains @@ -71,7 +71,7 @@ export function init( if (missedWordCount !== undefined) { sortableMissedBiwords.push([ missedWord, - TestWords.words.getText(i - 1) ?? "", + TestWords.words.get(i - 1)?.text ?? "", missedWordCount, ]); } @@ -94,8 +94,9 @@ export function init( let sortableSlowWords: [string, number][] = []; if (slow) { const typedWords = TestWords.words - .getText() - .slice(0, getInputHistory(lastEventLog).length - 1); + .get() + .slice(0, getInputHistory(lastEventLog).length - 1) + .map((word) => word.text); const burstHistory = getWordBurstHistory(lastEventLog); diff --git a/frontend/src/ts/test/replay-ui.ts b/frontend/src/ts/test/replay-ui.ts index ac0e43ada6f6..e1efe68bcb0f 100644 --- a/frontend/src/ts/test/replay-ui.ts +++ b/frontend/src/ts/test/replay-ui.ts @@ -41,7 +41,10 @@ const replayEl = qsr(".pageTest #resultReplay"); function getWordsList(): string[] { if (Config.mode === "zen") return getInputHistory(buildEventLog()); - return TestWords.words.list.slice(); + return TestWords.words + .get() + .slice() + .map((word) => word.text); } function deriveReplayActions(): Replay[] { @@ -59,7 +62,7 @@ function deriveReplayActions(): Replay[] { const target = Config.mode === "zen" ? typed - : TestWords.words.getText(prevWordIndex); + : TestWords.words.get(prevWordIndex)?.text; const correct = typed === target; actions.push({ action: correct ? "submitCorrectWord" : "submitErrorWord", diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index e5b47d0eb492..79e7192db7a9 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -2,7 +2,6 @@ import Ape from "../ape"; import * as TestUI from "./test-ui"; import * as Strings from "../utils/strings"; import * as Misc from "../utils/misc"; -import * as Arrays from "../utils/arrays"; import * as JSONData from "../utils/json-data"; import * as Numbers from "@monkeytype/util/numbers"; import { @@ -610,17 +609,10 @@ async function init(): Promise { } if (Config.keymapMode === "next" && Config.mode !== "zen") { - highlight( - Arrays.nthElementFromArray( - // ignoring for now but this might need a different approach - // oxlint-disable-next-line no-misused-spread - [...TestWords.words.getCurrentText()], - 0, - ) as string, - ); + highlight(TestWords.words.getCurrent()?.text[0] ?? ""); } - Funbox.toggleScript(TestWords.words.getCurrentText()); + Funbox.toggleScript(TestWords.words.getCurrent()?.text ?? ""); TestUI.setJoiningClass(allJoiningScript ?? language.joiningScript ?? false); const isLanguageRTL = allRightToLeft ?? language.rightToLeft ?? false; @@ -716,8 +708,8 @@ export async function addWord(): Promise { const randomWord = await WordsGenerator.getNextWord( TestWords.words.length, bound, - TestWords.words.getText(TestWords.words.length - 1), - TestWords.words.getText(TestWords.words.length - 2), + TestWords.words.get(TestWords.words.length - 1)?.text ?? "", + TestWords.words.get(TestWords.words.length - 2)?.text, ); TestWords.words.push(randomWord.word, randomWord.sectionIndex); @@ -1080,7 +1072,7 @@ export async function finish(difficultyFailed = false): Promise { const lastWordInputLength = history[wordIndex]?.length ?? 0; if ( - lastWordInputLength < (TestWords.words.getText(wordIndex)?.length ?? 0) + lastWordInputLength < (TestWords.words.get(wordIndex)?.text.length ?? 0) ) { historyLength--; } @@ -1420,14 +1412,7 @@ configEvent.subscribe(({ key, newValue, nosave }) => { if (key === "keymapMode" && newValue === "next" && Config.mode !== "zen") { setTimeout(() => { - highlight( - Arrays.nthElementFromArray( - // ignoring for now but this might need a different approach - // oxlint-disable-next-line no-misused-spread - [...TestWords.words.getCurrentText()], - 0, - ) as string, - ); + highlight(TestWords.words.getCurrent()?.text[0] ?? ""); }, 0); } if ( diff --git a/frontend/src/ts/test/test-timer.ts b/frontend/src/ts/test/test-timer.ts index 6a1b45dda7a4..022f3673bd3f 100644 --- a/frontend/src/ts/test/test-timer.ts +++ b/frontend/src/ts/test/test-timer.ts @@ -188,7 +188,9 @@ function layoutfluid(): void { if (Config.keymapMode === "next") { setTimeout(() => { highlight( - TestWords.words.getCurrentText().charAt(getCurrentInput().length), + TestWords.words + .getCurrent() + ?.text.charAt(getCurrentInput().length) ?? "", ); }, 1); } diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts index 89f919421701..cbfed884ea3f 100644 --- a/frontend/src/ts/test/test-ui.ts +++ b/frontend/src/ts/test/test-ui.ts @@ -227,8 +227,11 @@ async function joinOverlappingHints( activeWordLetters: ElementsWithUtils, hintElements: HTMLCollection, ): Promise { + const currentWord = TestWords.words.getCurrent(); + if (currentWord === undefined) return; + const [isWordRightToLeft] = Strings.isWordRightToLeft( - TestWords.words.getCurrentText(), + currentWord.text, TestState.isLanguageRightToLeft, TestState.isDirectionReversed, ); @@ -501,9 +504,9 @@ function showWords(): void { } else { let wordsHTML = ""; for (let i = 0; i < TestWords.words.length; i++) { - const word = TestWords.words.getText(i); + const word = TestWords.words.get(i); if (word === undefined) continue; // won't happen, but ts complains - wordsHTML += buildWordHTML(word, i); + wordsHTML += buildWordHTML(word.display, i); } wordsEl.setHtml(wordsHTML); } @@ -738,7 +741,7 @@ export async function updateWordLetters({ `test-ui.updateWordLetters.${wordIndex}`, async () => { pendingWordData.delete(wordIndex); - const currentWord = TestWords.words.getText(wordIndex); + const currentWord = TestWords.words.get(wordIndex)?.display; if (currentWord === undefined && Config.mode !== "zen") return; let ret = ""; const wordAtIndex = getWordElement(wordIndex); @@ -1335,7 +1338,7 @@ async function loadWordsHistory(): Promise { for (let i = 0; i < inputHistoryLength + 2; i++) { const input = inputHistory[i]; const corrected = correctedHistory[i]; - const word = TestWords.words.getText(i) ?? ""; + const word = TestWords.words.get(i)?.text ?? ""; const koreanRegex = /[\uac00-\ud7af]|[\u1100-\u11ff]|[\u3130-\u318f]|[\ua960-\ua97f]|[\ud7b0-\ud7ff]/; const containsKorean = @@ -1770,7 +1773,7 @@ function afterAnyTestInput( if (Config.keymapMode === "next") { highlight( - TestWords.words.getCurrentText().charAt(getCurrentInput().length), + TestWords.words.getCurrent()?.text.charAt(getCurrentInput().length) ?? "", ); } @@ -1966,8 +1969,9 @@ qs(".pageTest #copyWordsListButton")?.on("click", async () => { words = getInputHistory(TestState.lastEventLog).join(""); } else { words = TestWords.words - .getText() + .get() .slice(0, getInputHistory(TestState.lastEventLog).length) + .map((w) => w.text) .join(" "); } await copyToClipboard(words); diff --git a/frontend/src/ts/test/test-words.ts b/frontend/src/ts/test/test-words.ts index 63b30a92b7f1..9a0379d3037d 100644 --- a/frontend/src/ts/test/test-words.ts +++ b/frontend/src/ts/test/test-words.ts @@ -1,57 +1,76 @@ import * as TestState from "./test-state"; +type CommitChar = " " | "\n" | ""; + +type Word = { + text: string; + textWithCommit: string; + commit: CommitChar; + display: string; + sectionIndex: number; +}; + +const commitCharsToDisplay: Set = new Set(["\n"]); + class Words { - public list: string[]; - public sectionIndexList: number[]; + private list: Word[]; public length: number; constructor() { this.list = []; - this.sectionIndexList = []; this.length = 0; } - getText(i?: undefined, raw?: boolean): string[]; - getText(i: number, raw?: boolean): string | undefined; - getText(i?: number, raw = false): string | string[] | undefined { + get(i?: undefined, raw?: boolean): Word[]; + get(i: number, raw?: boolean): Word | undefined; + get(i?: number, raw = false): Word | Word[] | undefined { if (i === undefined) { return this.list; } else { + const word = this.list[i]; + if (!word) { + return undefined; + } if (raw) { - return this.list[i]?.replace(/[.?!":\-,]/g, "")?.toLowerCase(); + const text = word.text.replace(/[.?!":\-,]/g, "")?.toLowerCase(); + return { + text, + textWithCommit: text + word.commit, + display: + text + (commitCharsToDisplay.has(word.commit) ? word.commit : ""), + commit: word.commit, + sectionIndex: word.sectionIndex, + }; } else { - return this.list[i]; + return word; } } } - getCurrentText(): string { - return this.list[TestState.activeWordIndex] ?? ""; - } - getLast(): string { - return this.list[this.list.length - 1] as string; + getCurrent(): Word | undefined { + return this.list[TestState.activeWordIndex]; } push(word: string, sectionIndex: number): void { - this.list.push(word); - this.sectionIndexList.push(sectionIndex); + let commit: CommitChar = ""; + if (word.endsWith(" ")) { + commit = " "; + word = word.slice(0, -1); + } else if (word.endsWith("\n")) { + commit = "\n"; + word = word.slice(0, -1); + } + this.list.push({ + text: word, + textWithCommit: word + commit, + commit, + display: word + (commitCharsToDisplay.has(commit) ? commit : ""), + sectionIndex, + }); this.length = this.list.length; } reset(): void { this.list = []; - this.sectionIndexList = []; - this.length = this.list.length; - } - clean(): void { - for (const s of this.list) { - if (/ +/.test(s)) { - const id = this.list.indexOf(s); - const tempList = s.split(" "); - this.list.splice(id, 1); - for (let i = 0; i < tempList.length; i++) { - this.list.splice(id + i, 0, tempList[i] as string); - } - } - } + this.length = 0; } } diff --git a/frontend/src/ts/test/timer-progress.ts b/frontend/src/ts/test/timer-progress.ts index ac19c3b6a198..4e8063cd0752 100644 --- a/frontend/src/ts/test/timer-progress.ts +++ b/frontend/src/ts/test/timer-progress.ts @@ -106,10 +106,14 @@ export function instantHide(): void { function getCurrentCount(): number { if (Config.mode === "custom" && CustomText.getLimitMode() === "section") { - return ( - (TestWords.words.sectionIndexList[TestState.activeWordIndex] as number) - - 1 - ); + const currentSectionIndex = TestWords.words.get( + TestState.activeWordIndex, + )?.sectionIndex; + + if (currentSectionIndex === undefined) { + return 0; + } + return currentSectionIndex - 1; } else { return TestState.activeWordIndex; }