diff options
author | Jack <[email protected]> | 2024-12-04 16:11:07 +0100 |
---|---|---|
committer | GitHub <[email protected]> | 2024-12-04 16:11:07 +0100 |
commit | fdadb4ae83cfa16b2d8f8666d265b705b71071e7 (patch) | |
tree | bc910de0fe58c9d338d910feb71097aee83c6e06 /frontend/src | |
parent | a75f0d3b306134e48e45463b14157b40eb451882 (diff) | |
download | monkeytype-fdadb4ae83cfa16b2d8f8666d265b705b71071e7.tar.gz monkeytype-fdadb4ae83cfa16b2d8f8666d265b705b71071e7.zip |
refactor: move funboxes to a shared package (@miodec) (#6063)
Diffstat (limited to 'frontend/src')
21 files changed, 997 insertions, 1502 deletions
diff --git a/frontend/src/ts/commandline/lists.ts b/frontend/src/ts/commandline/lists.ts index 485e08bec..87358a2f9 100644 --- a/frontend/src/ts/commandline/lists.ts +++ b/frontend/src/ts/commandline/lists.ts @@ -77,7 +77,7 @@ import PresetsCommands from "./lists/presets"; import LayoutsCommands, { update as updateLayoutsCommands, } from "./lists/layouts"; -import FunboxCommands, { update as updateFunboxCommands } from "./lists/funbox"; +import FunboxCommands from "./lists/funbox"; import ThemesCommands, { update as updateThemesCommands } from "./lists/themes"; import LoadChallengeCommands, { update as updateLoadChallengeCommands, @@ -131,22 +131,6 @@ languagesPromise ); }); -const funboxPromise = JSONData.getFunboxList(); -funboxPromise - .then((funboxes) => { - updateFunboxCommands(funboxes); - if (FunboxCommands[0]?.subgroup) { - FunboxCommands[0].subgroup.beforeList = (): void => { - updateFunboxCommands(funboxes); - }; - } - }) - .catch((e: unknown) => { - console.error( - Misc.createErrorMessage(e, "Failed to update funbox commands") - ); - }); - const fontsPromise = JSONData.getFontsList(); fontsPromise .then((fonts) => { @@ -517,7 +501,6 @@ export async function getList( await Promise.allSettled([ layoutsPromise, languagesPromise, - funboxPromise, fontsPromise, themesPromise, challengesPromise, @@ -565,7 +548,6 @@ export async function getSingleSubgroup(): Promise<CommandsSubgroup> { await Promise.allSettled([ layoutsPromise, languagesPromise, - funboxPromise, fontsPromise, themesPromise, challengesPromise, diff --git a/frontend/src/ts/commandline/lists/funbox.ts b/frontend/src/ts/commandline/lists/funbox.ts index 704d0d4d3..f8c984bc3 100644 --- a/frontend/src/ts/commandline/lists/funbox.ts +++ b/frontend/src/ts/commandline/lists/funbox.ts @@ -1,42 +1,12 @@ import * as Funbox from "../../test/funbox/funbox"; import * as TestLogic from "../../test/test-logic"; import * as ManualRestart from "../../test/manual-restart-tracker"; -import Config from "../../config"; -import { areFunboxesCompatible } from "../../test/funbox/funbox-validation"; -import { FunboxMetadata } from "../../utils/json-data"; +import { getAllFunboxes, checkCompatibility } from "@monkeytype/funbox"; import { Command, CommandsSubgroup } from "../types"; +import { getActiveFunboxNames } from "../../test/funbox/list"; -const subgroup: CommandsSubgroup = { - title: "Funbox...", - configKey: "funbox", - list: [ - { - id: "changeFunboxNone", - display: "none", - configValue: "none", - alias: "off", - exec: (): void => { - if (Funbox.setFunbox("none")) { - TestLogic.restart(); - } - }, - }, - ], -}; - -const commands: Command[] = [ +const list: Command[] = [ { - id: "changeFunbox", - display: "Funbox...", - alias: "fun box", - icon: "fa-gamepad", - subgroup, - }, -]; - -function update(funboxes: FunboxMetadata[]): void { - subgroup.list = []; - subgroup.list.push({ id: "changeFunboxNone", display: "none", configValue: "none", @@ -48,27 +18,44 @@ function update(funboxes: FunboxMetadata[]): void { TestLogic.restart(); } }, + }, +]; + +for (const funbox of getAllFunboxes()) { + list.push({ + id: "changeFunbox" + funbox.name, + display: funbox.name.replace(/_/g, " "), + available: () => { + const activeNames = getActiveFunboxNames(); + if (activeNames.includes(funbox.name)) return true; + return checkCompatibility(activeNames, funbox.name); + }, + sticky: true, + alias: funbox.alias, + configValue: funbox.name, + configValueMode: "include", + exec: (): void => { + Funbox.toggleFunbox(funbox.name); + ManualRestart.set(); + TestLogic.restart(); + }, }); - for (const funbox of funboxes) { - subgroup.list.push({ - id: "changeFunbox" + funbox.name, - display: funbox.name.replace(/_/g, " "), - available: () => { - if (Config.funbox.split("#").includes(funbox.name)) return true; - return areFunboxesCompatible(Config.funbox, funbox.name); - }, - sticky: true, - alias: funbox.alias, - configValue: funbox.name, - configValueMode: "include", - exec: (): void => { - Funbox.toggleFunbox(funbox.name); - ManualRestart.set(); - TestLogic.restart(); - }, - }); - } } +const subgroup: CommandsSubgroup = { + title: "Funbox...", + configKey: "funbox", + list, +}; + +const commands: Command[] = [ + { + id: "changeFunbox", + display: "Funbox...", + alias: "fun box", + icon: "fa-gamepad", + subgroup, + }, +]; + export default commands; -export { update }; diff --git a/frontend/src/ts/controllers/account-controller.ts b/frontend/src/ts/controllers/account-controller.ts index 78291adf4..70191802c 100644 --- a/frontend/src/ts/controllers/account-controller.ts +++ b/frontend/src/ts/controllers/account-controller.ts @@ -20,6 +20,7 @@ import * as URLHandler from "../utils/url-handler"; import * as Account from "../pages/account"; import * as Alerts from "../elements/alerts"; import * as AccountSettings from "../pages/account-settings"; +import { getAllFunboxes } from "@monkeytype/funbox"; import { GoogleAuthProvider, GithubAuthProvider, @@ -129,7 +130,7 @@ async function getDataAndInit(): Promise<boolean> { ResultFilters.loadTags(snapshot.tags); - Promise.all([JSONData.getLanguageList(), JSONData.getFunboxList()]) + Promise.all([JSONData.getLanguageList(), getAllFunboxes()]) .then((values) => { const [languages, funboxes] = values; languages.forEach((language) => { diff --git a/frontend/src/ts/controllers/input-controller.ts b/frontend/src/ts/controllers/input-controller.ts index d28b4281a..dcb263ca9 100644 --- a/frontend/src/ts/controllers/input-controller.ts +++ b/frontend/src/ts/controllers/input-controller.ts @@ -29,13 +29,13 @@ import * as TestInput from "../test/test-input"; import * as TestWords from "../test/test-words"; import * as Hangul from "hangul-js"; import * as CustomTextState from "../states/custom-text-name"; -import * as FunboxList from "../test/funbox/funbox-list"; import * as KeymapEvent from "../observables/keymap-event"; import { IgnoredKeys } from "../constants/ignored-keys"; import { ModifierKeys } from "../constants/modifier-keys"; import { navigate } from "./route-controller"; import * as Loader from "../elements/loader"; import * as KeyConverter from "../utils/key-converter"; +import { getActiveFunboxes } from "../test/funbox/list"; let dontInsertSpace = false; let correctShiftUsed = true; @@ -145,9 +145,7 @@ function backspaceToPrevious(): void { TestInput.input.current = TestInput.input.popHistory(); TestInput.corrected.popHistory(); - if ( - FunboxList.get(Config.funbox).find((f) => f.properties?.includes("nospace")) - ) { + if (getActiveFunboxes().find((f) => f.properties?.includes("nospace"))) { TestInput.input.current = TestInput.input.current.slice(0, -1); setWordsInput(" " + TestInput.input.current + " "); } @@ -196,10 +194,8 @@ async function handleSpace(): Promise<void> { const currentWord: string = TestWords.words.getCurrent(); - for (const f of FunboxList.get(Config.funbox)) { - if (f.functions?.handleSpace) { - f.functions.handleSpace(); - } + for (const fb of getActiveFunboxes()) { + fb.functions?.handleSpace?.(); } dontInsertSpace = true; @@ -209,9 +205,8 @@ async function handleSpace(): Promise<void> { TestInput.pushBurstToHistory(burst); const nospace = - FunboxList.get(Config.funbox).find((f) => - f.properties?.includes("nospace") - ) !== undefined; + getActiveFunboxes().find((f) => f.properties?.includes("nospace")) !== + undefined; //correct word or in zen mode const isWordCorrect: boolean = @@ -411,9 +406,7 @@ function isCharCorrect(char: string, charIndex: number): boolean { return true; } - const funbox = FunboxList.get(Config.funbox).find( - (f) => f.functions?.isCharCorrect - ); + const funbox = getActiveFunboxes().find((fb) => fb.functions?.isCharCorrect); if (funbox?.functions?.isCharCorrect) { return funbox.functions.isCharCorrect(char, originalChar); } @@ -497,14 +490,15 @@ function handleChar( const isCharKorean: boolean = TestInput.input.getKoreanStatus(); - for (const f of FunboxList.get(Config.funbox)) { - if (f.functions?.handleChar) char = f.functions.handleChar(char); + for (const fb of getActiveFunboxes()) { + if (fb.functions?.handleChar) { + char = fb.functions.handleChar(char); + } } const nospace = - FunboxList.get(Config.funbox).find((f) => - f.properties?.includes("nospace") - ) !== undefined; + getActiveFunboxes().find((f) => f.properties?.includes("nospace")) !== + undefined; if (char !== "\n" && char !== "\t" && /\s/.test(char)) { if (nospace) return; @@ -908,11 +902,11 @@ $(document).on("keydown", async (event) => { return; } - FunboxList.get(Config.funbox).forEach((value) => { - if (value.functions?.handleKeydown) { - void value.functions?.handleKeydown(event); + for (const fb of getActiveFunboxes()) { + if (fb.functions?.handleKeydown) { + void fb.functions.handleKeydown(event); } - }); + } //autofocus const wordsFocused: boolean = $("#wordsInput").is(":focus"); @@ -1161,21 +1155,20 @@ $(document).on("keydown", async (event) => { } } - const funbox = FunboxList.get(Config.funbox).find( - (f) => f.functions?.preventDefaultEvent - ); - if (funbox?.functions?.preventDefaultEvent) { - if ( - await funbox.functions.preventDefaultEvent( - //i cant figure this type out, but it works fine - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - event as JQuery.KeyDownEvent - ) - ) { - event.preventDefault(); - handleChar(event.key, TestInput.input.current.length); - updateUI(); - setWordsInput(" " + TestInput.input.current); + for (const fb of getActiveFunboxes()) { + if (fb.functions?.preventDefaultEvent) { + if ( + await fb.functions.preventDefaultEvent( + //i cant figure this type out, but it works fine + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + event as JQuery.KeyDownEvent + ) + ) { + event.preventDefault(); + handleChar(event.key, TestInput.input.current.length); + updateUI(); + setWordsInput(" " + TestInput.input.current); + } } } diff --git a/frontend/src/ts/db.ts b/frontend/src/ts/db.ts index bc5a619b7..ff437e07f 100644 --- a/frontend/src/ts/db.ts +++ b/frontend/src/ts/db.ts @@ -5,7 +5,6 @@ import DefaultConfig from "./constants/default-config"; import { isAuthenticated } from "./firebase"; import * as ConnectionState from "./states/connection"; import { lastElementFromArray } from "./utils/arrays"; -import { getFunboxList } from "./utils/json-data"; import { migrateConfig } from "./utils/config"; import * as Dates from "date-fns"; import { @@ -32,6 +31,7 @@ import { import { Preset } from "@monkeytype/contracts/schemas/presets"; import defaultSnapshot from "./constants/default-snapshot"; import { Result } from "@monkeytype/contracts/schemas/results"; +import { FunboxMetadata } from "../../../packages/funbox/src/types"; export type SnapshotUserTag = UserTag & { active?: boolean; @@ -704,12 +704,8 @@ export async function getLocalPB<M extends Mode>( language: string, difficulty: Difficulty, lazyMode: boolean, - funbox: string + funboxes: FunboxMetadata[] ): Promise<PersonalBest | undefined> { - const funboxes = (await getFunboxList()).filter((fb) => { - return funbox?.split("#").includes(fb.name); - }); - if (!funboxes.every((f) => f.canGetPb)) { return undefined; } diff --git a/frontend/src/ts/elements/account/result-filters.ts b/frontend/src/ts/elements/account/result-filters.ts index d29c61a93..7c685a3a5 100644 --- a/frontend/src/ts/elements/account/result-filters.ts +++ b/frontend/src/ts/elements/account/result-filters.ts @@ -16,6 +16,7 @@ import { } from "@monkeytype/contracts/schemas/users"; import { LocalStorageWithSchema } from "../../utils/local-storage-with-schema"; import defaultResultFilters from "../../constants/default-result-filters"; +import { getAllFunboxes } from "@monkeytype/funbox"; export function mergeWithDefaultFilters( filters: Partial<ResultFilters> @@ -801,61 +802,48 @@ export async function appendButtons( } } - let funboxList; - try { - funboxList = await JSONData.getFunboxList(); - } catch (e) { - console.error( - Misc.createErrorMessage(e, "Failed to append funbox buttons") - ); - } - if (funboxList) { - let html = ""; + let html = ""; - html += - "<select class='funboxSelect' group='funbox' placeholder='select a funbox' multiple>"; + html += + "<select class='funboxSelect' group='funbox' placeholder='select a funbox' multiple>"; - html += "<option value='all'>all</option>"; - html += "<option value='none'>no funbox</option>"; + html += "<option value='all'>all</option>"; + html += "<option value='none'>no funbox</option>"; - for (const funbox of funboxList) { - html += `<option value="${funbox.name}" filter="${ - funbox.name - }">${funbox.name.replace(/_/g, " ")}</option>`; - } + for (const funbox of getAllFunboxes()) { + html += `<option value="${funbox.name}" filter="${ + funbox.name + }">${funbox.name.replace(/_/g, " ")}</option>`; + } - html += "</select>"; + html += "</select>"; - const el = document.querySelector( - ".pageAccount .content .filterButtons .buttonsAndTitle.funbox .select" - ); - if (el) { - el.innerHTML = html; - groupSelects["funbox"] = new SlimSelect({ - select: el.querySelector(".funboxSelect") as HTMLSelectElement, - settings: { - showSearch: true, - placeholderText: "select a funbox", - allowDeselect: true, - closeOnSelect: false, - }, - events: { - beforeChange: ( + const el = document.querySelector( + ".pageAccount .content .filterButtons .buttonsAndTitle.funbox .select" + ); + if (el) { + el.innerHTML = html; + groupSelects["funbox"] = new SlimSelect({ + select: el.querySelector(".funboxSelect") as HTMLSelectElement, + settings: { + showSearch: true, + placeholderText: "select a funbox", + allowDeselect: true, + closeOnSelect: false, + }, + events: { + beforeChange: (selectedOptions, oldSelectedOptions): void | boolean => { + return selectBeforeChangeFn( + "funbox", selectedOptions, oldSelectedOptions - ): void | boolean => { - return selectBeforeChangeFn( - "funbox", - selectedOptions, - oldSelectedOptions - ); - }, - beforeOpen: (): void => { - adjustScrollposition("funbox"); - }, + ); }, - }); - } + beforeOpen: (): void => { + adjustScrollposition("funbox"); + }, + }, + }); } const snapshot = DB.getSnapshot(); diff --git a/frontend/src/ts/pages/settings.ts b/frontend/src/ts/pages/settings.ts index 2581cfb73..5b7e507d6 100644 --- a/frontend/src/ts/pages/settings.ts +++ b/frontend/src/ts/pages/settings.ts @@ -5,7 +5,7 @@ import * as Misc from "../utils/misc"; import * as Strings from "../utils/strings"; import * as JSONData from "../utils/json-data"; import * as DB from "../db"; -import { toggleFunbox } from "../test/funbox/funbox"; +import * as Funbox from "../test/funbox/funbox"; import * as TagController from "../controllers/tag-controller"; import * as PresetController from "../controllers/preset-controller"; import * as ThemePicker from "../elements/settings/theme-picker"; @@ -15,7 +15,6 @@ import * as ConfigEvent from "../observables/config-event"; import * as ActivePage from "../states/active-page"; import Page from "./page"; import { isAuthenticated } from "../firebase"; -import { areFunboxesCompatible } from "../test/funbox/funbox-validation"; import { get as getTypingSpeedUnit } from "../utils/typing-speed-units"; import SlimSelect from "slim-select"; @@ -25,6 +24,12 @@ import { ConfigValue, CustomLayoutFluid, } from "@monkeytype/contracts/schemas/configs"; +import { + getAllFunboxes, + FunboxName, + checkCompatibility, +} from "@monkeytype/funbox"; +import { getActiveFunboxNames } from "../test/funbox/list"; type SettingsGroups<T extends ConfigValue> = Record<string, SettingsGroup<T>>; @@ -588,46 +593,37 @@ async function fillSettingsPage(): Promise<void> { funboxEl.innerHTML = `<div class="funbox button" data-config-value='none'>none</div>`; let funboxElHTML = ""; - let funboxList; - try { - funboxList = await JSONData.getFunboxList(); - } catch (e) { - console.error(Misc.createErrorMessage(e, "Failed to get funbox list")); - } - - if (funboxList) { - for (const funbox of funboxList) { - if (funbox.name === "mirror") { - funboxElHTML += `<div class="funbox button" data-config-value='${ - funbox.name - }' aria-label="${ - funbox.info - }" data-balloon-pos="up" data-balloon-length="fit" style="transform:scaleX(-1);">${funbox.name.replace( - /_/g, - " " - )}</div>`; - } else if (funbox.name === "upside_down") { - funboxElHTML += `<div class="funbox button" data-config-value='${ - funbox.name - }' aria-label="${ - funbox.info - }" data-balloon-pos="up" data-balloon-length="fit" style="transform:scaleX(-1) scaleY(-1); z-index:1;">${funbox.name.replace( - /_/g, - " " - )}</div>`; - } else { - funboxElHTML += `<div class="funbox button" data-config-value='${ - funbox.name - }' aria-label="${ - funbox.info - }" data-balloon-pos="up" data-balloon-length="fit">${funbox.name.replace( - /_/g, - " " - )}</div>`; - } + for (const funbox of getAllFunboxes()) { + if (funbox.name === "mirror") { + funboxElHTML += `<div class="funbox button" data-config-value='${ + funbox.name + }' aria-label="${ + funbox.description + }" data-balloon-pos="up" data-balloon-length="fit" style="transform:scaleX(-1);">${funbox.name.replace( + /_/g, + " " + )}</div>`; + } else if (funbox.name === "upside_down") { + funboxElHTML += `<div class="funbox button" data-config-value='${ + funbox.name + }' aria-label="${ + funbox.description + }" data-balloon-pos="up" data-balloon-length="fit" style="transform:scaleX(-1) scaleY(-1); z-index:1;">${funbox.name.replace( + /_/g, + " " + )}</div>`; + } else { + funboxElHTML += `<div class="funbox button" data-config-value='${ + funbox.name + }' aria-label="${ + funbox.description + }" data-balloon-pos="up" data-balloon-length="fit">${funbox.name.replace( + /_/g, + " " + )}</div>`; } - funboxEl.innerHTML = funboxElHTML; } + funboxEl.innerHTML = funboxElHTML; let isCustomFont = true; const fontsEl = document.querySelector( @@ -728,26 +724,16 @@ function setActiveFunboxButton(): void { $(`.pageSettings .section[data-config-name='funbox'] .button`).removeClass( "disabled" ); - JSONData.getFunboxList() - .then((funboxModes) => { - funboxModes.forEach((funbox) => { - if ( - !areFunboxesCompatible(Config.funbox, funbox.name) && - !Config.funbox.split("#").includes(funbox.name) - ) { - $( - `.pageSettings .section[data-config-name='funbox'] .button[data-config-value='${funbox.name}']` - ).addClass("disabled"); - } - }); - }) - .catch((e: unknown) => { - const message = Misc.createErrorMessage( - e, - "Failed to update funbox buttons" - ); - Notifications.add(message, -1); - }); + getAllFunboxes().forEach((funbox) => { + if ( + !checkCompatibility(getActiveFunboxNames(), funbox.name) && + !Config.funbox.split("#").includes(funbox.name) + ) { + $( + `.pageSettings .section[data-config-name='funbox'] .button[data-config-value='${funbox.name}']` + ).addClass("disabled"); + } + }); Config.funbox.split("#").forEach((funbox) => { $( `.pageSettings .section[data-config-name='funbox'] .button[data-config-value='${funbox}']` @@ -1057,8 +1043,8 @@ $(".pageSettings .section[data-config-name='funbox']").on( "click", ".button", (e) => { - const funbox = $(e.currentTarget).attr("data-config-value") as string; - toggleFunbox(funbox); + const funbox = $(e.currentTarget).attr("data-config-value") as FunboxName; + Funbox.toggleFunbox(funbox); setActiveFunboxButton(); } ); diff --git a/frontend/src/ts/ready.ts b/frontend/src/ts/ready.ts index 5ae62c550..e1b923688 100644 --- a/frontend/src/ts/ready.ts +++ b/frontend/src/ts/ready.ts @@ -1,14 +1,13 @@ -import Config from "./config"; import * as Misc from "./utils/misc"; import * as MonkeyPower from "./elements/monkey-power"; import * as MerchBanner from "./elements/merch-banner"; import * as CookiesModal from "./modals/cookies"; import * as ConnectionState from "./states/connection"; import * as AccountButton from "./elements/account-button"; -import * as FunboxList from "./test/funbox/funbox-list"; //@ts-expect-error import Konami from "konami"; import * as ServerConfiguration from "./ape/server-configuration"; +import { getActiveFunboxes } from "./test/funbox/list"; $((): void => { Misc.loadCSS("/css/slimselect.min.css", true); @@ -21,9 +20,9 @@ $((): void => { $("body").css("transition", "background .25s, transform .05s"); MerchBanner.showIfNotClosedBefore(); setTimeout(() => { - FunboxList.get(Config.funbox).forEach((it) => - it.functions?.applyGlobalCSS?.() - ); + for (const fb of getActiveFunboxes()) { + fb.functions?.applyGlobalCSS?.(); + } }, 500); //this approach will probably bite me in the ass at some point $("#app") diff --git a/frontend/src/ts/test/funbox/funbox-functions.ts b/frontend/src/ts/test/funbox/funbox-functions.ts new file mode 100644 index 000000000..73d606979 --- /dev/null +++ b/frontend/src/ts/test/funbox/funbox-functions.ts @@ -0,0 +1,627 @@ +import { Section } from "../../utils/json-data"; +import { FunboxWordsFrequency, Wordset } from "../wordset"; +import * as GetText from "../../utils/generate"; +import Config, * as UpdateConfig from "../../config"; +import * as Misc from "../../utils/misc"; +import * as Strings from "../../utils/strings"; +import { randomIntFromRange } from "@monkeytype/util/numbers"; +import * as Arrays from "../../utils/arrays"; +import { save } from "./funbox-memory"; +import { type FunboxName } from "@monkeytype/funbox"; +import * as TTSEvent from "../../observables/tts-event"; +import * as Notifications from "../../elements/notifications"; +import * as DDR from "../../utils/ddr"; +import * as TestWords from "../test-words"; +import * as TestInput from "../test-input"; +import * as LayoutfluidFunboxTimer from "./layoutfluid-funbox-timer"; +import * as KeymapEvent from "../../observables/keymap-event"; +import * as MemoryTimer from "./memory-funbox-timer"; +import { getPoem } from "../poetry"; +import * as JSONData from "../../utils/json-data"; +import { getSection } from "../wikipedia"; +import * as WeakSpot from "../weak-spot"; +import * as IPAddresses from "../../utils/ip-addresses"; + +export type FunboxFunctions = { + getWord?: (wordset?: Wordset, wordIndex?: number) => string; + punctuateWord?: (word: string) => string; + withWords?: (words?: string[]) => Promise<Wordset>; + alterText?: (word: string) => string; + applyConfig?: () => void; + applyGlobalCSS?: () => void; + clearGlobal?: () => void; + rememberSettings?: () => void; + toggleScript?: (params: string[]) => void; + pullSection?: (language?: string) => Promise<Section | false>; + handleSpace?: () => void; + handleChar?: (char: string) => string; + isCharCorrect?: (char: string, originalChar: string) => boolean; + preventDefaultEvent?: ( + event: JQuery.KeyDownEvent<Document, null, Document, Document> + ) => Promise<boolean>; + handleKeydown?: ( + event: JQuery.KeyDownEvent<Document, undefined, Document, Document> + ) => Promise<void>; + getResultContent?: () => string; + start?: () => void; + restart?: () => void; + getWordHtml?: (char: string, letterTag?: boolean) => string; + getWordsFrequencyMode?: () => FunboxWordsFrequency; +}; + +async function readAheadHandleKeydown( + event: JQuery.KeyDownEvent<Document, undefined, Document, Document> +): Promise<void> { + const inputCurrentChar = (TestInput.input.current ?? "").slice(-1); + const wordCurrentChar = TestWords.words + .getCurrent() + .slice(TestInput.input.current.length - 1, TestInput.input.current.length); + const isCorrect = inputCurrentChar === wordCurrentChar; + + if ( + event.key == "Backspace" && + !isCorrect && + (TestInput.input.current != "" || + TestInput.input.history[TestWords.words.currentIndex - 1] != + TestWords.words.get(TestWords.words.currentIndex - 1) || + Config.freedomMode) + ) { + $("#words").addClass("read_ahead_disabled"); + } else if (event.key == " ") { + $("#words").removeClass("read_ahead_disabled"); + } +} + +//todo move to its own file +class CharDistribution { + public chars: Record<string, number>; + public count: number; + constructor() { + this.chars = {}; + this.count = 0; + } + + public addChar(char: string): void { + this.count++; + if (char in this.chars) { + (this.chars[char] as number)++; + } else { + this.chars[char] = 1; + } + } + + public randomChar(): string { + const randomIndex = randomIntFromRange(0, this.count - 1); + let runningCount = 0; + for (const [char, charCount] of Object.entries(this.chars)) { + runningCount += charCount; + if (runningCount > randomIndex) { + return char; + } + } + + return Object.keys(this.chars)[0] as string; + } +} +const prefixSize = 2; +class PseudolangWordGenerator extends Wordset { + public ngrams: Record<string, CharDistribution> = {}; + constructor(words: string[]) { + super(words); + // Can generate an unbounded number of words in theory. + this.length = Infinity; + + for (let word of words) { + // Mark the end of each word with a space. + word += " "; + let prefix = ""; + for (const c of word) { + // Add `c` to the distribution of chars that can come after `prefix`. + if (!(prefix in this.ngrams)) { + this.ngrams[prefix] = new CharDistribution(); + } + (this.ngrams[prefix] as CharDistribution).addChar(c); + prefix = (prefix + c).slice(-prefixSize); + } + } + } + + public override randomWord(): string { + let word = ""; + for (;;) { + const prefix = word.slice(-prefixSize); + const charDistribution = this.ngrams[prefix]; + if (!charDistribution) { + // This shouldn't happen if this.ngrams is complete. If it does + // somehow, start generating a new word. + word = ""; + continue; + } + // Pick a random char from the distribution that comes after `prefix`. + const nextChar = charDistribution.randomChar(); + if (nextChar === " ") { + // A space marks the end of the word, so stop generating and return. + break; + } + word += nextChar; + } + return word; + } +} + +const list: Partial<Record<FunboxName, FunboxFunctions>> = { + "58008": { + getWord(): string { + let num = GetText.getNumbers(7); + if (Config.language.startsWith("kurdish")) { + num = Misc.convertNumberToArabic(num); + } else if (Config.language.startsWith("nepali")) { + num = Misc.convertNumberToNepali(num); + } + return num; + }, + punctuateWord(word: string): string { + if (word.length > 3) { + if (Math.random() < 0.5) { + word = Strings.replaceCharAt( + word, + randomIntFromRange(1, word.length - 2), + "." + ); + } + if (Math.random() < 0.75) { + const index = randomIntFromRange(1, word.length - 2); + if ( + word[index - 1] !== "." && + word[index + 1] !== "." && + word[index + 1] !== "0" + ) { + const special = Arrays.randomElementFromArray(["/", "*", "-", "+"]); + word = Strings.replaceCharAt(word, index, special); + } + } + } + return word; + }, + rememberSettings(): void { + save("numbers", Config.numbers, UpdateConfig.setNumbers); + }, + handleChar(char: string): string { + if (char === "\n") { + return " "; + } + return char; + }, + }, + simon_says: { + applyConfig(): void { + UpdateConfig.setKeymapMode("next", true); + }, + rememberSettings(): void { + save("keymapMode", Config.keymapMode, UpdateConfig.setKeymapMode); + }, + }, + tts: { + applyConfig(): void { + UpdateConfig.setKeymapMode("off", true); + }, + rememberSettings(): void { + save("keymapMode", Config.keymapMode, UpdateConfig.setKeymapMode); + }, + toggleScript(params: string[]): void { + if (window.speechSynthesis === undefined) { + Notifications.add("Failed to load text-to-speech script", -1); + return; + } + if (params[0] !== undefined) void TTSEvent.dispatch(params[0]); + }, + }, + arrows: { + getWord(_wordset, wordIndex): string { + return DDR.chart2Word(wordIndex === 0); + }, + rememberSettings(): void { + save( + "highlightMode", + Config.highlightMode, + UpdateConfig.setHighlightMode + ); + }, + handleChar(char: string): string { + if (char === "a" || char === "ArrowLeft" || char === "j") { + return "←"; + } + if (char === "s" || char === "ArrowDown" || char === "k") { + return "↓"; + } + if (char === "w" || char === "ArrowUp" || char === "i") { + return "↑"; + } + if (char === "d" || char === "ArrowRight" || char === "l") { + return "→"; + } + return char; + }, + isCharCorrect(char: string, originalChar: string): boolean { + if ( + (char === "a" || char === "ArrowLeft" || char === "j") && + originalChar === "←" + ) { + return true; + } + if ( + (char === "s" || char === "ArrowDown" || char === "k") && + originalChar === "↓" + ) { + return true; + } + if ( + (char === "w" || char === "ArrowUp" || char === "i") && + originalChar === "↑" + ) { + return true; + } + if ( + (char === "d" || char === "ArrowRight" || char === "l") && + originalChar === "→" + ) { + return true; + } + return false; + }, + async preventDefaultEvent(event: JQuery.KeyDownEvent): Promise<boolean> { + return ["ArrowLeft", "ArrowUp", "ArrowRight", "ArrowDown"].includes( + event.key + ); + }, + getWordHtml(char: string, letterTag?: boolean): string { + let retval = ""; + if (char === "↑") { + if (letterTag) retval += `<letter>`; + retval += `<i class="fas fa-arrow-up"></i>`; + if (letterTag) retval += `</letter>`; + } + if (char === "↓") { + if (letterTag) retval += `<letter>`; + retval += `<i class="fas fa-arrow-down"></i>`; + if (letterTag) retval += `</letter>`; + } + if (char === "←") { + if (letterTag) retval += `<letter>`; + retval += `<i class="fas fa-arrow-left"></i>`; + if (letterTag) retval += `</letter>`; + } + if (char === "→") { + if (letterTag) retval += `<letter>`; + retval += `<i class="fas fa-arrow-right"></i>`; + if (letterTag) retval += `</letter>`; + } + return retval; + }, + }, + rAnDoMcAsE: { + alterText(word: string): string { + let randomcaseword = word[0] as string; + for (let i = 1; i < word.length; i++) { + if ( + randomcaseword[i - 1] === + (randomcaseword[i - 1] as string).toUpperCase() + ) { + randomcaseword += (word[i] as string).toLowerCase(); + } else { + randomcaseword += (word[i] as string).toUpperCase(); + } + } + return randomcaseword; + }, + }, + backwards: { + alterText(word: string): string { + return word.split("").reverse().join(""); + }, + }, + capitals: { + alterText(word: string): string { + return Strings.capitalizeFirstLetterOfEachWord(word); + }, + }, + layoutfluid: { + applyConfig(): void { + const layout = Config.customLayoutfluid.split("#")[0] ?? "qwerty"; + + UpdateConfig.setLayout(layout, true); + UpdateConfig.setKeymapLayout(layout, true); + }, + rememberSettings(): void { + save("keymapMode", Config.keymapMode, UpdateConfig.setKeymapMode); + save("layout", Config.layout, UpdateConfig.setLayout); + save("keymapLayout", Config.keymapLayout, UpdateConfig.setKeymapLayout); + }, + handleSpace(): void { + if (Config.mode !== "time") { + // here I need to check if Config.customLayoutFluid exists because of my + // scuffed solution of returning whenever value is undefined in the setCustomLayoutfluid function + const layouts: string[] = Config.customLayoutfluid + ? Config.customLayoutfluid.split("#") + : ["qwerty", "dvorak", "colemak"]; + const outOf: number = TestWords.words.length; + const wordsPerLayout = Math.floor(outOf / layouts.length); + const index = Math.floor( + (TestInput.input.history.length + 1) / wordsPerLayout + ); + const mod = + wordsPerLayout - + ((TestWords.words.currentIndex + 1) % wordsPerLayout); + + if (layouts[index] as string) { + if (mod <= 3 && (layouts[index + 1] as string)) { + LayoutfluidFunboxTimer.show(); + LayoutfluidFunboxTimer.updateWords( + mod, + layouts[index + 1] as string + ); + } else { + LayoutfluidFunboxTimer.hide(); + } + if (mod === wordsPerLayout) { + UpdateConfig.setLayout(layouts[index] as string); + UpdateConfig.setKeymapLayout(layouts[index] as string); + if (mod > 3) { + LayoutfluidFunboxTimer.hide(); + } + } + } else { + LayoutfluidFunboxTimer.hide(); + } + setTimeout(() => { + void KeymapEvent.highlight( + TestWords.words + .getCurrent() + .charAt(TestInput.input.current.length) + .toString() + ); + }, 1); + } + }, + getResultContent(): string { + return Config.customLayoutfluid.replace(/#/g, " "); + }, + restart(): void { + if (this.applyConfig) this.applyConfig(); + setTimeout(() => { + void KeymapEvent.highlight( + TestWords.words + .getCurrent() + .substring( + TestInput.input.current.length, + TestInput.input.current.length + 1 + ) + .toString() + ); + }, 1); + }, + }, + gibberish: { + getWord(): string { + return GetText.getGibberish(); + }, + }, + ascii: { + getWord(): string { + return GetText.getASCII(); + }, + }, + specials: { + getWord(): string { + return GetText.getSpecials(); + }, + }, + read_ahead_easy: { + rememberSettings(): void { + save( + "highlightMode", + Config.highlightMode, + UpdateConfig.setHighlightMode + ); + }, + async handleKeydown(event): Promise<void> { + await readAheadHandleKeydown(event); + }, + }, + read_ahead: { + rememberSettings(): void { + save( + "highlightMode", + Config.highlightMode, + UpdateConfig.setHighlightMode + ); + }, + async handleKeydown(event): Promise<void> { + await readAheadHandleKeydown(event); + }, + }, + read_ahead_hard: { + rememberSettings(): void { + save( + "highlightMode", + Config.highlightMode, + UpdateConfig.setHighlightMode + ); + }, + async handleKeydown(event): Promise<void> { + await readAheadHandleKeydown(event); + }, + }, + memory: { + applyConfig(): void { + $("#wordsWrapper").addClass("hidden"); + UpdateConfig.setShowAllLines(true, true); + if (Config.keymapMode === "next") { + UpdateConfig.setKeymapMode("react", true); + } + }, + rememberSettings(): void { + save("mode", Config.mode, UpdateConfig.setMode); + save("showAllLines", Config.showAllLines, UpdateConfig.setShowAllLines); + if (Config.keymapMode === "next") { + save("keymapMode", Config.keymapMode, UpdateConfig.setKeymapMode); + } + }, + start(): void { + MemoryTimer.reset(); + $("#words").addClass("hidden"); + }, + restart(): void { + MemoryTimer.start(Math.round(Math.pow(TestWords.words.length, 1.2))); + $("#words").removeClass("hidden"); + if (Config.keymapMode === "next") { + UpdateConfig.setKeymapMode("react"); + } + }, + }, + nospace: { + rememberSettings(): void { + save( + "highlightMode", + Config.highlightMode, + UpdateConfig.setHighlightMode + ); + }, + }, + poetry: { + async pullSection(): Promise<JSONData.Section | false> { + return getPoem(); + }, + }, + wikipedia: { + async pullSection(lang?: string): Promise<JSONData.Section | false> { + return getSection((lang ?? "") || "english"); + }, + }, + weakspot: { + getWord(wordset?: Wordset): string { + if (wordset !== undefined) return WeakSpot.getWord(wordset); + else return ""; + }, + }, + pseudolang: { + async withWords(words?: string[]): Promise<Wordset> { + if (words !== undefined) return new PseudolangWordGenerator(words); + return new Wordset([]); + }, + }, + IPv4: { + getWord(): string { + return IPAddresses.getRandomIPv4address(); + }, + punctuateWord(word: string): string { + let w = word; + if (Math.random() < 0.25) { + w = IPAddresses.addressToCIDR(word); + } + return w; + }, + rememberSettings(): void { + save("numbers", Config.numbers, UpdateConfig.setNumbers); + }, + }, + IPv6: { + getWord(): string { + return IPAddresses.getRandomIPv6address(); + }, + punctuateWord(word: string): string { + let w = word; + if (Math.random() < 0.25) { + w = IPAddresses.addressToCIDR(word); + } + // Compress + if (w.includes(":")) { + w = IPAddresses.compressIpv6(w); + } + return w; + }, + rememberSettings(): void { + save("numbers", Config.numbers, UpdateConfig.setNumbers); + }, + }, + binary: { + getWord(): string { + return GetText.getBinary(); + }, + }, + hexadecimal: { + getWord(): string { + return GetText.getHexadecimal(); + }, + punctuateWord(word: string): string { + return `0x${word}`; + }, + rememberSettings(): void { + save("punctuation", Config.punctuation, UpdateConfig.setPunctuation); + }, + }, + zipf: { + getWordsFrequencyMode(): FunboxWordsFrequency { + return "zipf"; + }, + }, + ddoouubblleedd: { + alterText(word: string): string { + return word.replace(/./gu, "$&$&"); + }, + }, + instant_messaging: { + alterText(word: string): string { + return word + .toLowerCase() + .replace(/[.!?]$/g, "\n") //replace .?! with enter + .replace(/[().'"]/g, "") //remove special characters + .replace(/\n+/g, "\n"); //make sure there is only one enter + }, + }, + morse: { + alterText(word: string): string { + return GetText.getMorse(word); + }, + }, + crt: { + applyGlobalCSS(): void { + const isSafari = /^((?!chrome|android).)*safari/i.test( + navigator.userAgent + ); + if (isSafari) { + //Workaround for bug https://bugs.webkit.org/show_bug.cgi?id=256171 in Safari 16.5 or earlier + const versionMatch = navigator.userAgent.match( + /.*Version\/([0-9]*)\.([0-9]*).*/ + ); + const mainVersion = + versionMatch !== null ? parseInt(versionMatch[1] ?? "0") : 0; + const minorVersion = + versionMatch !== null ? parseInt(versionMatch[2] ?? "0") : 0; + if (mainVersion <= 16 && minorVersion <= 5) { + Notifications.add( + "CRT is not available on Safari 16.5 or earlier.", + 0, + { + duration: 5, + } + ); + UpdateConfig.toggleFunbox("crt"); + return; + } + } + $("body").append('<div id="scanline" />'); + $("body").addClass("crtmode"); + $("#globalFunBoxTheme").attr("href", `funbox/crt.css`); + }, + clearGlobal(): void { + $("#scanline").remove(); + $("body").removeClass("crtmode"); + $("#globalFunBoxTheme").attr("href", ``); + }, + }, +}; + +export function getFunboxFunctions(): Record<FunboxName, FunboxFunctions> { + return list as Record<FunboxName, FunboxFunctions>; +} diff --git a/frontend/src/ts/test/funbox/funbox-list.ts b/frontend/src/ts/test/funbox/funbox-list.ts deleted file mode 100644 index 9090c7a74..000000000 --- a/frontend/src/ts/test/funbox/funbox-list.ts +++ /dev/null @@ -1,302 +0,0 @@ -import { FunboxFunctions, FunboxMetadata } from "../../utils/json-data"; - -const list: FunboxMetadata[] = [ - { - name: "nausea", - info: "I think I'm gonna be sick.", - hasCSS: true, - }, - { - name: "round_round_baby", - info: "...right round, like a record baby. Right, round round round.", - hasCSS: true, - }, - { - name: "simon_says", - info: "Type what simon says.", - properties: ["changesWordsVisibility", "usesLayout"], - forcedConfig: { - highlightMode: ["letter", "off"], - }, - hasCSS: true, - }, - { - name: "mirror", - info: "Everything is mirrored!", - hasCSS: true, - }, - { - name: "upside_down", - info: "Everything is upside down!", - hasCSS: true, - }, - { - name: "tts", - info: "Listen closely.", - properties: ["changesWordsVisibility", "speaks"], - forcedConfig: { - highlightMode: ["letter", "off"], - }, - hasCSS: true, - }, - { - name: "choo_choo", - info: "All the letters are spinning!", - properties: ["noLigatures", "conflictsWithSymmetricChars"], - hasCSS: true, - }, - { - name: "arrows", - info: "Play it on a pad!", - properties: [ - "ignoresLanguage", - "ignoresLayout", - "nospace", - "noLetters", - "symmetricChars", - ], - forcedConfig: { - punctuation: [false], - numbers: [false], - highlightMode: ["letter", "off"], - }, - }, - { - name: "rAnDoMcAsE", - info: "I kInDa LiKe HoW iNeFfIcIeNt QwErTy Is.", - properties: ["changesCapitalisation"], - }, - { - name: "capitals", - info: "Capitalize Every Word.", - properties: ["changesCapitalisation"], - }, - { - name: "layoutfluid", - info: "Switch between layouts specified below proportionately to the length of the test.", - properties: ["changesLayout", "noInfiniteDuration"], - }, - { - name: "earthquake", - info: "Everybody get down! The words are shaking!", - properties: ["noLigatures"], - hasCSS: true, - }, - { - name: "space_balls", - info: "In a galaxy far far away.", - hasCSS: true, - }, - { - name: "gibberish", - info: "Anvbuefl dizzs eoos alsb?", - properties: ["ignoresLanguage", "unspeakable"], - }, - { - name: "58008", - alias: "numbers", - info: "A special mode for accountants.", - properties: ["ignoresLanguage", "ignoresLayout", "noLetters"], - forcedConfig: { - numbers: [false], - }, - }, - { - name: "ascii", - info: "Where was the ampersand again?. Only ASCII characters.", - properties: ["ignoresLanguage", "noLetters", "unspeakable"], - forcedConfig: { - numbers: [false], - }, - }, - { - name: "specials", - info: "!@#$%^&*. Only special characters.", - properties: ["ignoresLanguage", "noLetters", "unspeakable"], - forcedConfig: { - punctuation: [false], - numbers: [false], - }, - }, - { - name: "plus_zero", - info: "React quickly! Only the current word is visible.", - properties: ["changesWordsVisibility", "toPush:1", "noInfiniteDuration"], - }, - { - name: "plus_one", - info: "Only one future word is visible.", - properties: ["changesWordsVisibility", "toPush:2", "noInfiniteDuration"], - }, - { - name: "plus_two", - info: "Only two future words are visible.", - properties: ["changesWordsVisibility", "toPush:3", "noInfiniteDuration"], - }, - { - name: "plus_three", - info: "Only three future words are visible.", - properties: ["changesWordsVisibility", "toPush:4", "noInfiniteDuration"], - }, - { - name: "read_ahead_easy", - info: "Only the current word is invisible.", - properties: ["changesWordsVisibility"], - forcedConfig: { - highlightMode: ["letter", "off"], - }, - hasCSS: true, - }, - { - name: "read_ahead", - info: "Current and the next word are invisible!", - properties: ["changesWordsVisibility"], - forcedConfig: { - highlightMode: ["letter", "off"], - }, - hasCSS: true, - }, - { - name: "read_ahead_hard", - info: "Current and the next two words are invisible!", - properties: ["changesWordsVisibility"], - forcedConfig: { - highlightMode: ["letter", "off"], - }, - hasCSS: true, - }, - { - name: "memory", - info: "Test your memory. Remember the words and type them blind.", - properties: ["changesWordsVisibility", "noInfiniteDuration"], - forcedConfig: { - mode: ["words", "quote", "custom"], - }, - }, - { - name: "nospace", - info: "Whoneedsspacesanyway?", - properties: ["nospace"], - forcedConfig: { - highlightMode: ["letter", "off"], - }, - }, - { - name: "poetry", - info: "Practice typing some beautiful prose.", - properties: ["noInfiniteDuration", "ignoresLanguage"], - forcedConfig: { - punctuation: [false], - numbers: [false], - }, - }, - { - name: "wikipedia", - info: "Practice typing wikipedia sections.", - properties: ["noInfiniteDuration", "ignoresLanguage"], - forcedConfig: { - punctuation: [false], - numbers: [false], - }, - }, - { - name: "weakspot", - info: "Focus on slow and mistyped letters.", - properties: ["changesWordsFrequency"], - }, - { - name: "pseudolang", - info: "Nonsense words that look like the current language.", - properties: ["unspeakable", "ignoresLanguage"], - }, - { - name: "IPv4", - alias: "network", - info: "For sysadmins.", - properties: ["ignoresLanguage", "ignoresLayout", "noLetters"], - forcedConfig: { - numbers: [false], - }, - }, - { - name: "IPv6", - alias: "network", - info: "For sysadmins with a long beard.", - properties: ["ignoresLanguage", "ignoresLayout", "noLetters"], - forcedConfig: { - numbers: [false], - }, - }, - { - name: "binary", - alias: "numbers", - info: "01000010 01100101 01100101 01110000 00100000 01100010 01101111 01101111 01110000 00101110", - properties: ["ignoresLanguage", "ignoresLayout", "noLetters"], - forcedConfig: { - numbers: [false], - punctuation: [false], - }, - }, - { - name: "hexadecimal", - info: "0x38 0x20 0x74 0x69 0x6D 0x65 0x73 0x20 0x6D 0x6F 0x72 0x65 0x20 0x62 0x6F 0x6F 0x70 0x21", - properties: ["ignoresLanguage", "ignoresLayout", "noLetters"], - forcedConfig: { - numbers: [false], - }, - }, - { - name: "zipf", - alias: "frequency", - info: "Words are generated according to Zipf's law. (not all languages will produce Zipfy results, use with caution)", - properties: ["changesWordsFrequency"], - }, - { - name: "morse", - info: "-.../././.--./ -.../---/---/.--./-.-.--/ ", - properties: ["ignoresLanguage", "ignoresLayout", "noLetters", "nospace"], - }, - { - name: "crt", - info: "Go back to the 1980s", - properties: ["noLigatures"], - }, - { - name: "backwards", - info: "...sdrawkcab epyt ot yrt woN", - properties: [ - "noLigatures", - "conflictsWithSymmetricChars", - "wordOrder:reverse", - ], - }, - { - name: "ddoouubblleedd", - info: "TTyyppee eevveerryytthhiinngg ttwwiiccee..", - properties: ["noLigatures"], - }, - { - name: "instant_messaging", - info: "Who needs shift anyway?", - properties: ["changesCapitalisation"], - }, -]; - -export function getAll(): FunboxMetadata[] { - return list; -} - -export function get(config: string): FunboxMetadata[] { - const funboxes: FunboxMetadata[] = []; - for (const i of config.split("#")) { - const f = list.find((f) => f.name === i); - if (f) funboxes.push(f); - } - return funboxes; -} - -export function setFunboxFunctions(name: string, obj: FunboxFunctions): void { - const fb = list.find((f) => f.name === name); - if (!fb) throw new Error(`Funbox ${name} not found.`); - fb.functions = obj; -} diff --git a/frontend/src/ts/test/funbox/funbox-validation.ts b/frontend/src/ts/test/funbox/funbox-validation.ts index 0f0bf1cfd..eb0510736 100644 --- a/frontend/src/ts/test/funbox/funbox-validation.ts +++ b/frontend/src/ts/test/funbox/funbox-validation.ts @@ -1,26 +1,24 @@ -import * as FunboxList from "./funbox-list"; import * as Notifications from "../../elements/notifications"; import * as Strings from "../../utils/strings"; import { Config, ConfigValue } from "@monkeytype/contracts/schemas/configs"; +import { FunboxMetadata, getFunboxesFromString } from "@monkeytype/funbox"; import { intersect } from "@monkeytype/util/arrays"; -import { FunboxForcedConfig, FunboxMetadata } from "../../utils/json-data"; -export function checkFunboxForcedConfigs( +export function checkForcedConfig( key: string, value: ConfigValue, - funbox: string + funboxes: FunboxMetadata[] ): { result: boolean; forcedConfigs?: ConfigValue[]; } { - if (FunboxList.get(funbox).length === 0) return { result: true }; + if (funboxes.length === 0) { + return { result: true }; + } if (key === "words" || key === "time") { if (value === 0) { - if (funbox === "nospace") { - console.log("break"); - } - const fb = FunboxList.get(funbox).filter((f) => + const fb = funboxes.filter((f) => f.properties?.includes("noInfiniteDuration") ); if (fb.length > 0) { @@ -37,16 +35,16 @@ export function checkFunboxForcedConfigs( } else { const forcedConfigs: Record<string, ConfigValue[]> = {}; // collect all forced configs - for (const fb of FunboxList.get(funbox)) { - if (fb.forcedConfig) { + for (const fb of funboxes) { + if (fb.frontendForcedConfig) { //push keys to forcedConfigs, if they don't exist. if they do, intersect the values - for (const key in fb.forcedConfig) { + for (const key in fb.frontendForcedConfig) { if (forcedConfigs[key] === undefined) { - forcedConfigs[key] = fb.forcedConfig[key] as ConfigValue[]; + forcedConfigs[key] = fb.frontendForcedConfig[key] as ConfigValue[]; } else { forcedConfigs[key] = intersect( forcedConfigs[key], - fb.forcedConfig[key] as ConfigValue[], + fb.frontendForcedConfig[key] as ConfigValue[], true ); } @@ -80,22 +78,19 @@ export function canSetConfigWithCurrentFunboxes( ): boolean { let errorCount = 0; if (key === "mode") { - let fb: FunboxMetadata[] = []; - fb = fb.concat( - FunboxList.get(funbox).filter( - (f) => - f.forcedConfig?.["mode"] !== undefined && - !f.forcedConfig?.["mode"].includes(value) - ) + let fb = getFunboxesFromString(funbox).filter( + (f) => + f.frontendForcedConfig?.["mode"] !== undefined && + !(f.frontendForcedConfig["mode"] as ConfigValue[]).includes(value) ); if (value === "zen") { fb = fb.concat( - FunboxList.get(funbox).filter( - (f) => - f.functions?.getWord ?? - f.functions?.pullSection ?? - f.functions?.alterText ?? - f.functions?.withWords ?? + getFunboxesFromString(funbox).filter((f) => { + return ( + f.frontendFunctions?.includes("getWord") ?? + f.frontendFunctions?.includes("pullSection") ?? + f.frontendFunctions?.includes("alterText") ?? + f.frontendFunctions?.includes("withWords") ?? f.properties?.includes("changesCapitalisation") ?? f.properties?.includes("nospace") ?? f.properties?.find((fp) => fp.startsWith("toPush:")) ?? @@ -103,18 +98,20 @@ export function canSetConfigWithCurrentFunboxes( f.properties?.includes("speaks") ?? f.properties?.includes("changesLayout") ?? f.properties?.includes("changesWordsFrequency") - ) + ); + }) ); } if (value === "quote" || value === "custom") { fb = fb.concat( - FunboxList.get(funbox).filter( - (f) => - f.functions?.getWord ?? - f.functions?.pullSection ?? - f.functions?.withWords ?? + getFunboxesFromString(funbox).filter((f) => { + return ( + f.frontendFunctions?.includes("getWord") ?? + f.frontendFunctions?.includes("pullSection") ?? + f.frontendFunctions?.includes("withWords") ?? f.properties?.includes("changesWordsFrequency") - ) + ); + }) ); } @@ -123,7 +120,7 @@ export function canSetConfigWithCurrentFunboxes( } } if (key === "words" || key === "time") { - if (!checkFunboxForcedConfigs(key, value, funbox).result) { + if (!checkForcedConfig(key, value, getFunboxesFromString(funbox)).result) { if (!noNotification) { Notifications.add("Active funboxes do not support infinite tests", 0); return false; @@ -131,7 +128,9 @@ export function canSetConfigWithCurrentFunboxes( errorCount += 1; } } - } else if (!checkFunboxForcedConfigs(key, value, funbox).result) { + } else if ( + !checkForcedConfig(key, value, getFunboxesFromString(funbox)).result + ) { errorCount += 1; } @@ -204,142 +203,3 @@ export function canSetFunboxWithConfig( return true; } } - -export function areFunboxesCompatible( - funboxes: string, - withFunbox?: string -): boolean { - if (withFunbox === "none" || funboxes === "none") return true; - let funboxesToCheck = FunboxList.get(funboxes); - if (withFunbox !== undefined) { - funboxesToCheck = funboxesToCheck.concat( - FunboxList.getAll().filter((f) => f.name === withFunbox) - ); - } - - const allFunboxesAreValid = - FunboxList.get(funboxes).filter( - (f) => funboxes.split("#").find((cf) => cf === f.name) !== undefined - ).length === funboxes.split("#").length; - const oneWordModifierMax = - funboxesToCheck.filter( - (f) => - f.functions?.getWord ?? - f.functions?.pullSection ?? - f.functions?.withWords - ).length <= 1; - const layoutUsability = - funboxesToCheck.filter((f) => - f.properties?.find((fp) => fp === "changesLayout") - ).length === 0 || - funboxesToCheck.filter((f) => - f.properties?.find((fp) => fp === "ignoresLayout" || fp === "usesLayout") - ).length === 0; - const oneNospaceOrToPushMax = - funboxesToCheck.filter((f) => - f.properties?.find((fp) => fp === "nospace" || fp.startsWith("toPush")) - ).length <= 1; - const oneChangesWordsVisibilityMax = - funboxesToCheck.filter((f) => - f.properties?.find((fp) => fp === "changesWordsVisibility") - ).length <= 1; - const oneFrequencyChangesMax = - funboxesToCheck.filter((f) => - f.properties?.find((fp) => fp === "changesWordsFrequency") - ).length <= 1; - const noFrequencyChangesConflicts = - funboxesToCheck.filter((f) => - f.properties?.find((fp) => fp === "changesWordsFrequency") - ).length === 0 || - funboxesToCheck.filter((f) => - f.properties?.find((fp) => fp === "ignoresLanguage") - ).length === 0; - const capitalisationChangePosibility = - funboxesToCheck.filter((f) => - f.properties?.find((fp) => fp === "noLetters") - ).length === 0 || - funboxesToCheck.filter((f) => - f.properties?.find((fp) => fp === "changesCapitalisation") - ).length === 0; - const noConflictsWithSymmetricChars = - funboxesToCheck.filter((f) => - f.properties?.find((fp) => fp === "conflictsWithSymmetricChars") - ).length === 0 || - funboxesToCheck.filter((f) => - f.properties?.find((fp) => fp === "symmetricChars") - ).length === 0; - const oneCanSpeakMax = - funboxesToCheck.filter((f) => f.properties?.find((fp) => fp === "speaks")) - .length <= 1; - const hasLanguageToSpeakAndNoUnspeakable = - funboxesToCheck.filter((f) => f.properties?.find((fp) => fp === "speaks")) - .length === 0 || - (funboxesToCheck.filter((f) => f.properties?.find((fp) => fp === "speaks")) - .length === 1 && - funboxesToCheck.filter((f) => - f.properties?.find((fp) => fp === "unspeakable") - ).length === 0) || - funboxesToCheck.filter((f) => - f.properties?.find((fp) => fp === "ignoresLanguage") - ).length === 0; - const oneToPushOrPullSectionMax = - funboxesToCheck.filter( - (f) => - (f.properties?.find((fp) => fp.startsWith("toPush:")) ?? "") || - f.functions?.pullSection - ).length <= 1; - const oneApplyCSSMax = - funboxesToCheck.filter((f) => f.hasCSS == true).length <= 1; - const onePunctuateWordMax = - funboxesToCheck.filter((f) => f.functions?.punctuateWord).length <= 1; - const oneCharCheckerMax = - funboxesToCheck.filter((f) => f.functions?.isCharCorrect).length <= 1; - const oneCharReplacerMax = - funboxesToCheck.filter((f) => f.functions?.getWordHtml).length <= 1; - const oneChangesCapitalisationMax = - funboxesToCheck.filter((f) => - f.properties?.find((fp) => fp === "changesCapitalisation") - ).length <= 1; - const allowedConfig = {} as FunboxForcedConfig; - let noConfigConflicts = true; - for (const f of funboxesToCheck) { - if (!f.forcedConfig) continue; - for (const key in f.forcedConfig) { - if (allowedConfig[key]) { - if ( - intersect( - allowedConfig[key], - f.forcedConfig[key] as ConfigValue[], - true - ).length === 0 - ) { - noConfigConflicts = false; - break; - } - } else { - allowedConfig[key] = f.forcedConfig[key] as ConfigValue[]; - } - } - } - - return ( - allFunboxesAreValid && - oneWordModifierMax && - layoutUsability && - oneNospaceOrToPushMax && - oneChangesWordsVisibilityMax && - oneFrequencyChangesMax && - noFrequencyChangesConflicts && - capitalisationChangePosibility && - noConflictsWithSymmetricChars && - oneCanSpeakMax && - hasLanguageToSpeakAndNoUnspeakable && - oneToPushOrPullSectionMax && - oneApplyCSSMax && - onePunctuateWordMax && - oneCharCheckerMax && - oneCharReplacerMax && - oneChangesCapitalisationMax && - noConfigConflicts - ); -} diff --git a/frontend/src/ts/test/funbox/funbox.ts b/frontend/src/ts/test/funbox/funbox.ts index 3974bc5d8..b84423257 100644 --- a/frontend/src/ts/test/funbox/funbox.ts +++ b/frontend/src/ts/test/funbox/funbox.ts @@ -1,589 +1,43 @@ import * as Notifications from "../../elements/notifications"; import * as Misc from "../../utils/misc"; import * as JSONData from "../../utils/json-data"; -import * as GetText from "../../utils/generate"; -import * as Arrays from "../../utils/arrays"; import * as Strings from "../../utils/strings"; import * as ManualRestart from "../manual-restart-tracker"; import Config, * as UpdateConfig from "../../config"; import * as MemoryTimer from "./memory-funbox-timer"; import * as FunboxMemory from "./funbox-memory"; -import * as FunboxList from "./funbox-list"; -import { save } from "./funbox-memory"; -import * as TTSEvent from "../../observables/tts-event"; -import * as KeymapEvent from "../../observables/keymap-event"; -import * as TestWords from "../test-words"; -import * as TestInput from "../test-input"; -import * as WeakSpot from "../weak-spot"; -import { getPoem } from "../poetry"; -import { getSection } from "../wikipedia"; -import * as IPAddresses from "../../utils/ip-addresses"; -import { - areFunboxesCompatible, - checkFunboxForcedConfigs, -} from "./funbox-validation"; -import { FunboxWordsFrequency, Wordset } from "../wordset"; -import * as LayoutfluidFunboxTimer from "./layoutfluid-funbox-timer"; -import * as DDR from "../../utils/ddr"; import { HighlightMode } from "@monkeytype/contracts/schemas/configs"; import { Mode } from "@monkeytype/contracts/schemas/shared"; -import { randomIntFromRange } from "@monkeytype/util/numbers"; +import { FunboxName, checkCompatibility } from "@monkeytype/funbox"; +import { getActiveFunboxes, getActiveFunboxNames } from "./list"; +import { checkForcedConfig } from "./funbox-validation"; -const prefixSize = 2; - -class CharDistribution { - public chars: Record<string, number>; - public count: number; - constructor() { - this.chars = {}; - this.count = 0; - } - - public addChar(char: string): void { - this.count++; - if (char in this.chars) { - (this.chars[char] as number)++; - } else { - this.chars[char] = 1; - } - } - - public randomChar(): string { - const randomIndex = randomIntFromRange(0, this.count - 1); - let runningCount = 0; - for (const [char, charCount] of Object.entries(this.chars)) { - runningCount += charCount; - if (runningCount > randomIndex) { - return char; - } - } - - return Object.keys(this.chars)[0] as string; - } -} - -class PseudolangWordGenerator extends Wordset { - public ngrams: Record<string, CharDistribution> = {}; - constructor(words: string[]) { - super(words); - // Can generate an unbounded number of words in theory. - this.length = Infinity; - - for (let word of words) { - // Mark the end of each word with a space. - word += " "; - let prefix = ""; - for (const c of word) { - // Add `c` to the distribution of chars that can come after `prefix`. - if (!(prefix in this.ngrams)) { - this.ngrams[prefix] = new CharDistribution(); - } - (this.ngrams[prefix] as CharDistribution).addChar(c); - prefix = (prefix + c).slice(-prefixSize); - } - } - } - - public override randomWord(): string { - let word = ""; - for (;;) { - const prefix = word.slice(-prefixSize); - const charDistribution = this.ngrams[prefix]; - if (!charDistribution) { - // This shouldn't happen if this.ngrams is complete. If it does - // somehow, start generating a new word. - word = ""; - continue; - } - // Pick a random char from the distribution that comes after `prefix`. - const nextChar = charDistribution.randomChar(); - if (nextChar === " ") { - // A space marks the end of the word, so stop generating and return. - break; - } - word += nextChar; - } - return word; - } -} - -FunboxList.setFunboxFunctions("simon_says", { - applyConfig(): void { - UpdateConfig.setKeymapMode("next", true); - }, - rememberSettings(): void { - save("keymapMode", Config.keymapMode, UpdateConfig.setKeymapMode); - }, -}); - -FunboxList.setFunboxFunctions("tts", { - applyConfig(): void { - UpdateConfig.setKeymapMode("off", true); - }, - rememberSettings(): void { - save("keymapMode", Config.keymapMode, UpdateConfig.setKeymapMode); - }, - toggleScript(params: string[]): void { - if (window.speechSynthesis === undefined) { - Notifications.add("Failed to load text-to-speech script", -1); - return; - } - if (params[0] !== undefined) void TTSEvent.dispatch(params[0]); - }, -}); - -FunboxList.setFunboxFunctions("arrows", { - getWord(_wordset, wordIndex): string { - return DDR.chart2Word(wordIndex === 0); - }, - rememberSettings(): void { - save("highlightMode", Config.highlightMode, UpdateConfig.setHighlightMode); - }, - handleChar(char: string): string { - if (char === "a" || char === "ArrowLeft" || char === "j") { - return "←"; - } - if (char === "s" || char === "ArrowDown" || char === "k") { - return "↓"; - } - if (char === "w" || char === "ArrowUp" || char === "i") { - return "↑"; - } - if (char === "d" || char === "ArrowRight" || char === "l") { - return "→"; - } - return char; - }, - isCharCorrect(char: string, originalChar: string): boolean { - if ( - (char === "a" || char === "ArrowLeft" || char === "j") && - originalChar === "←" - ) { - return true; - } - if ( - (char === "s" || char === "ArrowDown" || char === "k") && - originalChar === "↓" - ) { - return true; - } - if ( - (char === "w" || char === "ArrowUp" || char === "i") && - originalChar === "↑" - ) { - return true; - } - if ( - (char === "d" || char === "ArrowRight" || char === "l") && - originalChar === "→" - ) { - return true; - } - return false; - }, - async preventDefaultEvent(event: JQuery.KeyDownEvent): Promise<boolean> { - return ["ArrowLeft", "ArrowUp", "ArrowRight", "ArrowDown"].includes( - event.key - ); - }, - getWordHtml(char: string, letterTag?: boolean): string { - let retval = ""; - if (char === "↑") { - if (letterTag) retval += `<letter>`; - retval += `<i class="fas fa-arrow-up"></i>`; - if (letterTag) retval += `</letter>`; - } - if (char === "↓") { - if (letterTag) retval += `<letter>`; - retval += `<i class="fas fa-arrow-down"></i>`; - if (letterTag) retval += `</letter>`; - } - if (char === "←") { - if (letterTag) retval += `<letter>`; - retval += `<i class="fas fa-arrow-left"></i>`; - if (letterTag) retval += `</letter>`; - } - if (char === "→") { - if (letterTag) retval += `<letter>`; - retval += `<i class="fas fa-arrow-right"></i>`; - if (letterTag) retval += `</letter>`; - } - return retval; - }, -}); - -FunboxList.setFunboxFunctions("rAnDoMcAsE", { - alterText(word: string): string { - let randomcaseword = word[0] as string; - for (let i = 1; i < word.length; i++) { - if ( - randomcaseword[i - 1] === - (randomcaseword[i - 1] as string).toUpperCase() - ) { - randomcaseword += (word[i] as string).toLowerCase(); - } else { - randomcaseword += (word[i] as string).toUpperCase(); - } - } - return randomcaseword; - }, -}); - -FunboxList.setFunboxFunctions("backwards", { - alterText(word: string): string { - return word.split("").reverse().join(""); - }, -}); - -FunboxList.setFunboxFunctions("capitals", { - alterText(word: string): string { - return Strings.capitalizeFirstLetterOfEachWord(word); - }, -}); - -FunboxList.setFunboxFunctions("layoutfluid", { - applyConfig(): void { - const layout = Config.customLayoutfluid.split("#")[0] ?? "qwerty"; - - UpdateConfig.setLayout(layout, true); - UpdateConfig.setKeymapLayout(layout, true); - }, - rememberSettings(): void { - save("keymapMode", Config.keymapMode, UpdateConfig.setKeymapMode); - save("layout", Config.layout, UpdateConfig.setLayout); - save("keymapLayout", Config.keymapLayout, UpdateConfig.setKeymapLayout); - }, - handleSpace(): void { - if (Config.mode !== "time") { - // here I need to check if Config.customLayoutFluid exists because of my - // scuffed solution of returning whenever value is undefined in the setCustomLayoutfluid function - const layouts: string[] = Config.customLayoutfluid - ? Config.customLayoutfluid.split("#") - : ["qwerty", "dvorak", "colemak"]; - const outOf: number = TestWords.words.length; - const wordsPerLayout = Math.floor(outOf / layouts.length); - const index = Math.floor( - (TestInput.input.history.length + 1) / wordsPerLayout - ); - const mod = - wordsPerLayout - ((TestWords.words.currentIndex + 1) % wordsPerLayout); - - if (layouts[index] as string) { - if (mod <= 3 && (layouts[index + 1] as string)) { - LayoutfluidFunboxTimer.show(); - LayoutfluidFunboxTimer.updateWords(mod, layouts[index + 1] as string); - } else { - LayoutfluidFunboxTimer.hide(); - } - if (mod === wordsPerLayout) { - UpdateConfig.setLayout(layouts[index] as string); - UpdateConfig.setKeymapLayout(layouts[index] as string); - if (mod > 3) { - LayoutfluidFunboxTimer.hide(); - } - } - } else { - LayoutfluidFunboxTimer.hide(); - } - setTimeout(() => { - void KeymapEvent.highlight( - TestWords.words - .getCurrent() - .charAt(TestInput.input.current.length) - .toString() - ); - }, 1); - } - }, - getResultContent(): string { - return Config.customLayoutfluid.replace(/#/g, " "); - }, - restart(): void { - if (this.applyConfig) this.applyConfig(); - setTimeout(() => { - void KeymapEvent.highlight( - TestWords.words - .getCurrent() - .substring( - TestInput.input.current.length, - TestInput.input.current.length + 1 - ) - .toString() - ); - }, 1); - }, -}); - -FunboxList.setFunboxFunctions("gibberish", { - getWord(): string { - return GetText.getGibberish(); - }, -}); - -FunboxList.setFunboxFunctions("58008", { - getWord(): string { - let num = GetText.getNumbers(7); - if (Config.language.startsWith("kurdish")) { - num = Misc.convertNumberToArabic(num); - } else if (Config.language.startsWith("nepali")) { - num = Misc.convertNumberToNepali(num); - } - return num; - }, - punctuateWord(word: string): string { - if (word.length > 3) { - if (Math.random() < 0.5) { - word = Strings.replaceCharAt( - word, - randomIntFromRange(1, word.length - 2), - "." - ); - } - if (Math.random() < 0.75) { - const index = randomIntFromRange(1, word.length - 2); - if ( - word[index - 1] !== "." && - word[index + 1] !== "." && - word[index + 1] !== "0" - ) { - const special = Arrays.randomElementFromArray(["/", "*", "-", "+"]); - word = Strings.replaceCharAt(word, index, special); - } - } - } - return word; - }, - rememberSettings(): void { - save("numbers", Config.numbers, UpdateConfig.setNumbers); - }, - handleChar(char: string): string { - if (char === "\n") { - return " "; - } - return char; - }, -}); - -FunboxList.setFunboxFunctions("ascii", { - getWord(): string { - return GetText.getASCII(); - }, - punctuateWord(word: string): string { - return word; - }, -}); - -FunboxList.setFunboxFunctions("specials", { - getWord(): string { - return GetText.getSpecials(); - }, -}); - -async function readAheadHandleKeydown( - event: JQuery.KeyDownEvent<Document, undefined, Document, Document> -): Promise<void> { - const inputCurrentChar = (TestInput.input.current ?? "").slice(-1); - const wordCurrentChar = TestWords.words - .getCurrent() - .slice(TestInput.input.current.length - 1, TestInput.input.current.length); - const isCorrect = inputCurrentChar === wordCurrentChar; +export function toggleScript(...params: string[]): void { + if (Config.funbox === "none") return; - if ( - event.key == "Backspace" && - !isCorrect && - (TestInput.input.current != "" || - TestInput.input.history[TestWords.words.currentIndex - 1] != - TestWords.words.get(TestWords.words.currentIndex - 1) || - Config.freedomMode) - ) { - $("#words").addClass("read_ahead_disabled"); - } else if (event.key == " ") { - $("#words").removeClass("read_ahead_disabled"); + for (const fb of getActiveFunboxes()) { + fb.functions?.toggleScript?.(params); } } -FunboxList.setFunboxFunctions("read_ahead_easy", { - rememberSettings(): void { - save("highlightMode", Config.highlightMode, UpdateConfig.setHighlightMode); - }, - async handleKeydown(event): Promise<void> { - await readAheadHandleKeydown(event); - }, -}); - -FunboxList.setFunboxFunctions("read_ahead", { - rememberSettings(): void { - save("highlightMode", Config.highlightMode, UpdateConfig.setHighlightMode); - }, - async handleKeydown(event): Promise<void> { - await readAheadHandleKeydown(event); - }, -}); - -FunboxList.setFunboxFunctions("read_ahead_hard", { - rememberSettings(): void { - save("highlightMode", Config.highlightMode, UpdateConfig.setHighlightMode); - }, - async handleKeydown(event): Promise<void> { - await readAheadHandleKeydown(event); - }, -}); - -FunboxList.setFunboxFunctions("memory", { - applyConfig(): void { - $("#wordsWrapper").addClass("hidden"); - UpdateConfig.setShowAllLines(true, true); - if (Config.keymapMode === "next") { - UpdateConfig.setKeymapMode("react", true); - } - }, - rememberSettings(): void { - save("mode", Config.mode, UpdateConfig.setMode); - save("showAllLines", Config.showAllLines, UpdateConfig.setShowAllLines); - if (Config.keymapMode === "next") { - save("keymapMode", Config.keymapMode, UpdateConfig.setKeymapMode); - } - }, - start(): void { - MemoryTimer.reset(); - $("#words").addClass("hidden"); - }, - restart(): void { - MemoryTimer.start(); - $("#words").removeClass("hidden"); - if (Config.keymapMode === "next") { - UpdateConfig.setKeymapMode("react"); - } - }, -}); - -FunboxList.setFunboxFunctions("nospace", { - rememberSettings(): void { - save("highlightMode", Config.highlightMode, UpdateConfig.setHighlightMode); - }, -}); - -FunboxList.setFunboxFunctions("poetry", { - async pullSection(): Promise<JSONData.Section | false> { - return getPoem(); - }, -}); - -FunboxList.setFunboxFunctions("wikipedia", { - async pullSection(lang?: string): Promise<JSONData.Section | false> { - return getSection((lang ?? "") || "english"); - }, -}); - -FunboxList.setFunboxFunctions("weakspot", { - getWord(wordset?: Wordset): string { - if (wordset !== undefined) return WeakSpot.getWord(wordset); - else return ""; - }, -}); - -FunboxList.setFunboxFunctions("pseudolang", { - async withWords(words?: string[]): Promise<Wordset> { - if (words !== undefined) return new PseudolangWordGenerator(words); - return new Wordset([]); - }, -}); - -FunboxList.setFunboxFunctions("IPv4", { - getWord(): string { - return IPAddresses.getRandomIPv4address(); - }, - punctuateWord(word: string): string { - let w = word; - if (Math.random() < 0.25) { - w = IPAddresses.addressToCIDR(word); - } - return w; - }, - rememberSettings(): void { - save("numbers", Config.numbers, UpdateConfig.setNumbers); - }, -}); - -FunboxList.setFunboxFunctions("IPv6", { - getWord(): string { - return IPAddresses.getRandomIPv6address(); - }, - punctuateWord(word: string): string { - let w = word; - if (Math.random() < 0.25) { - w = IPAddresses.addressToCIDR(word); - } - // Compress - if (w.includes(":")) { - w = IPAddresses.compressIpv6(w); - } - return w; - }, - rememberSettings(): void { - save("numbers", Config.numbers, UpdateConfig.setNumbers); - }, -}); - -FunboxList.setFunboxFunctions("binary", { - getWord(): string { - return GetText.getBinary(); - }, -}); - -FunboxList.setFunboxFunctions("hexadecimal", { - getWord(): string { - return GetText.getHexadecimal(); - }, - punctuateWord(word: string): string { - return `0x${word}`; - }, - rememberSettings(): void { - save("punctuation", Config.punctuation, UpdateConfig.setPunctuation); - }, -}); - -FunboxList.setFunboxFunctions("zipf", { - getWordsFrequencyMode(): FunboxWordsFrequency { - return "zipf"; - }, -}); - -FunboxList.setFunboxFunctions("ddoouubblleedd", { - alterText(word: string): string { - return word.replace(/./gu, "$&$&"); - }, -}); - -FunboxList.setFunboxFunctions("instant_messaging", { - alterText(word: string): string { - return word - .toLowerCase() - .replace(/[.!?]$/g, "\n") //replace .?! with enter - .replace(/[().'"]/g, "") //remove special characters - .replace(/\n+/g, "\n"); //make sure there is only one enter - }, -}); - -export function toggleScript(...params: string[]): void { - FunboxList.get(Config.funbox).forEach((funbox) => { - if (funbox.functions?.toggleScript) funbox.functions.toggleScript(params); - }); -} - export function setFunbox(funbox: string): boolean { if (funbox === "none") { - FunboxList.get(Config.funbox).forEach((f) => f.functions?.clearGlobal?.()); + for (const fb of getActiveFunboxes()) { + fb.functions?.clearGlobal?.(); + } } FunboxMemory.load(); UpdateConfig.setFunbox(funbox, false); return true; } -export function toggleFunbox(funbox: string): boolean { +export function toggleFunbox(funbox: "none" | FunboxName): boolean { if (funbox === "none") setFunbox("none"); if ( - !areFunboxesCompatible(Config.funbox, funbox) && + !checkCompatibility( + getActiveFunboxNames(), + funbox === "none" ? undefined : funbox + ) && !Config.funbox.split("#").includes(funbox) ) { Notifications.add( @@ -597,10 +51,12 @@ export function toggleFunbox(funbox: string): boolean { FunboxMemory.load(); const e = UpdateConfig.toggleFunbox(funbox, false); - if (!Config.funbox.includes(funbox)) { - FunboxList.get(funbox).forEach((f) => f.functions?.clearGlobal?.()); - } else { - FunboxList.get(funbox).forEach((f) => f.functions?.applyGlobalCSS?.()); + for (const fb of getActiveFunboxes()) { + if (!Config.funbox.includes(funbox)) { + fb.functions?.clearGlobal?.(); + } else { + fb.functions?.applyGlobalCSS?.(); + } } //todo find out what the hell this means @@ -635,7 +91,7 @@ export async function activate(funbox?: string): Promise<boolean | undefined> { // The configuration might be edited with dev tools, // so we need to double check its validity - if (!areFunboxesCompatible(Config.funbox)) { + if (!checkCompatibility(getActiveFunboxNames())) { Notifications.add( Misc.createErrorMessage( undefined, @@ -672,9 +128,7 @@ export async function activate(funbox?: string): Promise<boolean | undefined> { if (language.ligatures) { if ( - FunboxList.get(Config.funbox).find((f) => - f.properties?.includes("noLigatures") - ) + getActiveFunboxes().find((f) => f.properties?.includes("noLigatures")) ) { Notifications.add( "Current language does not support this funbox mode", @@ -689,10 +143,10 @@ export async function activate(funbox?: string): Promise<boolean | undefined> { let canSetSoFar = true; for (const [configKey, configValue] of Object.entries(Config)) { - const check = checkFunboxForcedConfigs( + const check = checkForcedConfig( configKey, configValue, - Config.funbox + getActiveFunboxes() ); if (check.result) continue; if (!check.result) { @@ -742,65 +196,24 @@ export async function activate(funbox?: string): Promise<boolean | undefined> { } ManualRestart.set(); - FunboxList.get(Config.funbox).forEach(async (funbox) => { - funbox.functions?.applyConfig?.(); - }); + for (const fb of getActiveFunboxes()) { + fb.functions?.applyConfig?.(); + } // ModesNotice.update(); return true; } export async function rememberSettings(): Promise<void> { - FunboxList.get(Config.funbox).forEach(async (funbox) => { - if (funbox.functions?.rememberSettings) funbox.functions.rememberSettings(); - }); + for (const fb of getActiveFunboxes()) { + fb.functions?.rememberSettings?.(); + } } -FunboxList.setFunboxFunctions("morse", { - alterText(word: string): string { - return GetText.getMorse(word); - }, -}); - -FunboxList.setFunboxFunctions("crt", { - applyGlobalCSS(): void { - const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); - if (isSafari) { - //Workaround for bug https://bugs.webkit.org/show_bug.cgi?id=256171 in Safari 16.5 or earlier - const versionMatch = navigator.userAgent.match( - /.*Version\/([0-9]*)\.([0-9]*).*/ - ); - const mainVersion = - versionMatch !== null ? parseInt(versionMatch[1] ?? "0") : 0; - const minorVersion = - versionMatch !== null ? parseInt(versionMatch[2] ?? "0") : 0; - if (mainVersion <= 16 && minorVersion <= 5) { - Notifications.add( - "CRT is not available on Safari 16.5 or earlier.", - 0, - { - duration: 5, - } - ); - toggleFunbox("crt"); - return; - } - } - $("body").append('<div id="scanline" />'); - $("body").addClass("crtmode"); - $("#globalFunBoxTheme").attr("href", `funbox/crt.css`); - }, - clearGlobal(): void { - $("#scanline").remove(); - $("body").removeClass("crtmode"); - $("#globalFunBoxTheme").attr("href", ``); - }, -}); - async function setFunboxBodyClasses(): Promise<boolean> { const $body = $("body"); - const activeFbClasses = FunboxList.get(Config.funbox).map( - (it) => "fb-" + it.name.replaceAll("_", "-") + const activeFbClasses = getActiveFunboxNames().map( + (name) => "fb-" + name.replaceAll("_", "-") ); const currentClasses = @@ -818,8 +231,8 @@ async function applyFunboxCSS(): Promise<boolean> { const $theme = $("#funBoxTheme"); //currently we only support one active funbox with hasCSS - const activeFunboxWithTheme = FunboxList.get(Config.funbox).find( - (it) => it.hasCSS == true + const activeFunboxWithTheme = getActiveFunboxes().find((fb) => + fb?.properties?.includes("hasCssFile") ); const activeTheme = diff --git a/frontend/src/ts/test/funbox/list.ts b/frontend/src/ts/test/funbox/list.ts new file mode 100644 index 000000000..48f45f4a8 --- /dev/null +++ b/frontend/src/ts/test/funbox/list.ts @@ -0,0 +1,72 @@ +import Config from "../../config"; +import { + FunboxName, + stringToFunboxNames, + FunboxMetadata, + getFunboxObject, + FunboxProperty, +} from "@monkeytype/funbox"; + +import { FunboxFunctions, getFunboxFunctions } from "./funbox-functions"; + +type FunboxMetadataWithFunctions = FunboxMetadata & { + functions?: FunboxFunctions; +}; + +const metadata = getFunboxObject(); +const functions = getFunboxFunctions(); + +const metadataWithFunctions = {} as Record< + FunboxName, + FunboxMetadataWithFunctions +>; + +for (const [name, data] of Object.entries(metadata)) { + metadataWithFunctions[name as FunboxName] = { + ...data, + functions: functions[name as FunboxName], + }; +} + +export function get(funboxName: FunboxName): FunboxMetadataWithFunctions; +export function get(funboxNames: FunboxName[]): FunboxMetadataWithFunctions[]; +export function get( + funboxNameOrNames: FunboxName | FunboxName[] +): FunboxMetadataWithFunctions | FunboxMetadataWithFunctions[] { + if (Array.isArray(funboxNameOrNames)) { + const fns = funboxNameOrNames.map((name) => metadataWithFunctions[name]); + return fns; + } else { + return metadataWithFunctions[funboxNameOrNames]; + } +} + +export function getAllFunboxes(): FunboxMetadataWithFunctions[] { + return Object.values(metadataWithFunctions); +} + +export function getFromString( + hashSeparatedFunboxes: string +): FunboxMetadataWithFunctions[] { + return get(stringToFunboxNames(hashSeparatedFunboxes)); +} + +export function getActiveFunboxes(): FunboxMetadataWithFunctions[] { + return get(stringToFunboxNames(Config.funbox)); +} + +export function getActiveFunboxNames(): FunboxName[] { + return stringToFunboxNames(Config.funbox); +} + +export function getActiveFunboxesWithProperty( + property: FunboxProperty +): FunboxMetadataWithFunctions[] { + return getActiveFunboxes().filter((fb) => fb.properties?.includes(property)); +} + +export function getActiveFunboxesWithFunction( + functionName: keyof FunboxFunctions +): FunboxMetadataWithFunctions[] { + return getActiveFunboxes().filter((fb) => fb.functions?.[functionName]); +} diff --git a/frontend/src/ts/test/funbox/memory-funbox-timer.ts b/frontend/src/ts/test/funbox/memory-funbox-timer.ts index df851e747..e09cdb1d2 100644 --- a/frontend/src/ts/test/funbox/memory-funbox-timer.ts +++ b/frontend/src/ts/test/funbox/memory-funbox-timer.ts @@ -1,5 +1,3 @@ -import * as TestWords from "../test-words"; - let memoryTimer: number | null = null; let memoryInterval: NodeJS.Timeout | null = null; @@ -30,9 +28,9 @@ export function reset(): void { hide(); } -export function start(): void { +export function start(time: number): void { reset(); - memoryTimer = Math.round(Math.pow(TestWords.words.length, 1.2)); + memoryTimer = time; update(memoryTimer); show(); memoryInterval = setInterval(() => { diff --git a/frontend/src/ts/test/pace-caret.ts b/frontend/src/ts/test/pace-caret.ts index 13cf7a15f..e5a1ea09f 100644 --- a/frontend/src/ts/test/pace-caret.ts +++ b/frontend/src/ts/test/pace-caret.ts @@ -8,6 +8,7 @@ import * as JSONData from "../utils/json-data"; import * as TestState from "./test-state"; import * as ConfigEvent from "../observables/config-event"; import { convertRemToPixels } from "../utils/numbers"; +import { getActiveFunboxes } from "./funbox/list"; type Settings = { wpm: number; @@ -79,7 +80,7 @@ export async function init(): Promise<void> { Config.language, Config.difficulty, Config.lazyMode, - Config.funbox + getActiveFunboxes() ) )?.wpm ?? 0; } else if (Config.paceCaret === "tagPb") { diff --git a/frontend/src/ts/test/result.ts b/frontend/src/ts/test/result.ts index 52a9b1cd7..6b87db26b 100644 --- a/frontend/src/ts/test/result.ts +++ b/frontend/src/ts/test/result.ts @@ -15,11 +15,9 @@ import * as SlowTimer from "../states/slow-timer"; import * as DateTime from "../utils/date-and-time"; import * as Misc from "../utils/misc"; import * as Strings from "../utils/strings"; -import * as JSONData from "../utils/json-data"; import * as Numbers from "@monkeytype/util/numbers"; import * as Arrays from "../utils/arrays"; import { get as getTypingSpeedUnit } from "../utils/typing-speed-units"; -import * as FunboxList from "./funbox/funbox-list"; import * as PbCrown from "./pb-crown"; import * as TestConfig from "./test-config"; import * as TestInput from "./test-input"; @@ -32,7 +30,6 @@ import * as CustomText from "./custom-text"; import * as CustomTextState from "./../states/custom-text-name"; import * as Funbox from "./funbox/funbox"; import Format from "../utils/format"; - import confetti from "canvas-confetti"; import type { AnnotationOptions, @@ -40,6 +37,8 @@ import type { } from "chartjs-plugin-annotation"; import Ape from "../ape"; import { CompletedEvent } from "@monkeytype/contracts/schemas/results"; +import { getActiveFunboxes, getFromString } from "./funbox/list"; +import { getFunboxesFromString } from "@monkeytype/funbox"; let result: CompletedEvent; let maxChartVal: number; @@ -127,10 +126,10 @@ async function updateGraph(): Promise<void> { const fc = await ThemeColors.get("sub"); if (Config.funbox !== "none") { let content = ""; - for (const f of FunboxList.get(Config.funbox)) { - content += f.name; - if (f.functions?.getResultContent) { - content += "(" + f.functions.getResultContent() + ")"; + for (const fb of getActiveFunboxes()) { + content += fb.name; + if (fb.functions?.getResultContent) { + content += "(" + fb.functions.getResultContent() + ")"; } content += " "; } @@ -180,7 +179,7 @@ export async function updateGraphPBLine(): Promise<void> { result.language, result.difficulty, result.lazyMode ?? false, - result.funbox ?? "none" + getFunboxesFromString(result.funbox ?? "none") ); const localPbWpm = localPb?.wpm ?? 0; if (localPbWpm === 0) return; @@ -403,7 +402,7 @@ export async function updateCrown(dontSave: boolean): Promise<void> { Config.language, Config.difficulty, Config.lazyMode, - Config.funbox + getActiveFunboxes() ); const localPbWpm = localPb?.wpm ?? 0; pbDiff = result.wpm - localPbWpm; @@ -425,7 +424,7 @@ export async function updateCrown(dontSave: boolean): Promise<void> { Config.language, Config.difficulty, Config.lazyMode, - "none" + [] ); const localPbWpm = localPb?.wpm ?? 0; pbDiff = result.wpm - localPbWpm; @@ -474,9 +473,7 @@ type CanGetPbObject = async function resultCanGetPb(): Promise<CanGetPbObject> { const funboxes = result.funbox?.split("#") ?? []; - const funboxObjects = await Promise.all( - funboxes.map(async (f) => JSONData.getFunbox(f)) - ); + const funboxObjects = getFromString(result.funbox); const allFunboxesCanGetPb = funboxObjects.every((f) => f?.canGetPb); const funboxesOk = @@ -678,7 +675,7 @@ function updateTestType(randomQuote: Quote | null): void { } } const ignoresLanguage = - FunboxList.get(Config.funbox).find((f) => + getActiveFunboxes().find((f) => f.properties?.includes("ignoresLanguage") ) !== undefined; if (Config.mode !== "custom" && !ignoresLanguage) { diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index a2aa2882c..3874fba2a 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -52,7 +52,6 @@ import { Auth, isAuthenticated } from "../firebase"; import * as AdController from "../controllers/ad-controller"; import * as TestConfig from "./test-config"; import * as ConnectionState from "../states/connection"; -import * as FunboxList from "./funbox/funbox-list"; import * as MemoryFunboxTimer from "./funbox/memory-funbox-timer"; import * as KeymapEvent from "../observables/keymap-event"; import * as LayoutfluidFunboxTimer from "../test/funbox/layoutfluid-funbox-timer"; @@ -65,6 +64,8 @@ import { CustomTextDataWithTextLen, } from "@monkeytype/contracts/schemas/results"; import * as XPBar from "../elements/xp-bar"; +import { getActiveFunboxes } from "./funbox/list"; +import { getFunboxesFromString } from "@monkeytype/funbox"; let failReason = ""; const koInputVisual = document.getElementById("koInputVisual") as HTMLElement; @@ -106,8 +107,8 @@ export function startTest(now: number): boolean { TestTimer.clear(); Monkey.show(); - for (const f of FunboxList.get(Config.funbox)) { - if (f.functions?.start) f.functions.start(); + for (const fb of getActiveFunboxes()) { + fb.functions?.start?.(); } try { @@ -328,8 +329,8 @@ export function restart(options = {} as RestartOptions): void { await init(); await PaceCaret.init(); - for (const f of FunboxList.get(Config.funbox)) { - if (f.functions?.restart) f.functions.restart(); + for (const fb of getActiveFunboxes()) { + fb.functions?.restart?.(); } if (Config.showAverage !== "off") { @@ -540,7 +541,7 @@ export function areAllTestWordsGenerated(): boolean { //add word during the test export async function addWord(): Promise<void> { let bound = 100; // how many extra words to aim for AFTER the current word - const funboxToPush = FunboxList.get(Config.funbox) + const funboxToPush = getActiveFunboxes() .find((f) => f.properties?.find((fp) => fp.startsWith("toPush"))) ?.properties?.find((fp) => fp.startsWith("toPush:")); const toPushCount = funboxToPush?.split(":")[1]; @@ -555,9 +556,10 @@ export async function addWord(): Promise<void> { return; } - const sectionFunbox = FunboxList.get(Config.funbox).find( + const sectionFunbox = getActiveFunboxes().find( (f) => f.functions?.pullSection ); + if (sectionFunbox?.functions?.pullSection) { if (TestWords.words.length - TestWords.words.currentIndex < 20) { const section = await sectionFunbox.functions.pullSection( @@ -1221,7 +1223,7 @@ async function saveResult( completedEvent.language, completedEvent.difficulty, completedEvent.lazyMode, - completedEvent.funbox + getFunboxesFromString(completedEvent.funbox) ); if (localPb !== undefined) { diff --git a/frontend/src/ts/test/test-stats.ts b/frontend/src/ts/test/test-stats.ts index 44fb8dcbd..b69cc6b97 100644 --- a/frontend/src/ts/test/test-stats.ts +++ b/frontend/src/ts/test/test-stats.ts @@ -3,13 +3,13 @@ import Config from "../config"; import * as Strings from "../utils/strings"; import * as TestInput from "./test-input"; import * as TestWords from "./test-words"; -import * as FunboxList from "./funbox/funbox-list"; import * as TestState from "./test-state"; import * as Numbers from "@monkeytype/util/numbers"; import { CompletedEvent, IncompleteTest, } from "@monkeytype/contracts/schemas/results"; +import { getActiveFunboxes } from "./funbox/list"; type CharCount = { spaces: number; @@ -350,9 +350,7 @@ function countChars(): CharCount { spaces++; } } - if ( - FunboxList.get(Config.funbox).find((f) => f.properties?.includes("nospace")) - ) { + if (getActiveFunboxes().find((f) => f.properties?.includes("nospace"))) { spaces = 0; correctspaces = 0; } diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts index 25a0f9ee0..542dd3afb 100644 --- a/frontend/src/ts/test/test-ui.ts +++ b/frontend/src/ts/test/test-ui.ts @@ -19,7 +19,6 @@ import * as ConfigEvent from "../observables/config-event"; import * as Hangul from "hangul-js"; import { format } from "date-fns/format"; import { isAuthenticated } from "../firebase"; -import * as FunboxList from "./funbox/funbox-list"; import { debounce } from "throttle-debounce"; import * as ResultWordHighlight from "../elements/result-word-highlight"; import * as ActivePage from "../states/active-page"; @@ -31,6 +30,7 @@ import { TimerOpacity, } from "@monkeytype/contracts/schemas/configs"; import { convertRemToPixels } from "../utils/numbers"; +import { getActiveFunboxes } from "./funbox/list"; async function gethtml2canvas(): Promise<typeof import("html2canvas").default> { return (await import("html2canvas")).default; @@ -330,9 +330,8 @@ export async function updateHintsPosition(): Promise<void> { function getWordHTML(word: string): string { let newlineafter = false; let retval = `<div class='word'>`; - const funbox = FunboxList.get(Config.funbox).find( - (f) => f.functions?.getWordHtml - ); + + const funbox = getActiveFunboxes().find((f) => f.functions?.getWordHtml); const chars = Strings.splitIntoCharacters(word); for (const char of chars) { if (funbox?.functions?.getWordHtml) { @@ -648,9 +647,9 @@ export async function screenshot(): Promise<void> { } (document.querySelector("html") as HTMLElement).style.scrollBehavior = "smooth"; - FunboxList.get(Config.funbox).forEach((f) => - f.functions?.applyGlobalCSS?.() - ); + for (const fb of getActiveFunboxes()) { + fb.functions?.applyGlobalCSS?.(); + } } if (!$("#resultReplay").hasClass("hidden")) { @@ -690,7 +689,9 @@ export async function screenshot(): Promise<void> { $(".highlightContainer").addClass("hidden"); if (revertCookie) $("#cookiesModal").addClass("hidden"); - FunboxList.get(Config.funbox).forEach((f) => f.functions?.clearGlobal?.()); + for (const fb of getActiveFunboxes()) { + fb.functions?.clearGlobal?.(); + } (document.querySelector("html") as HTMLElement).style.scrollBehavior = "auto"; window.scrollTo({ @@ -837,9 +838,7 @@ export async function updateActiveWordLetters( } } - const funbox = FunboxList.get(Config.funbox).find( - (f) => f.functions?.getWordHtml - ); + const funbox = getActiveFunboxes().find((fb) => fb.functions?.getWordHtml); const inputChars = Strings.splitIntoCharacters(input); const currentWordChars = Strings.splitIntoCharacters(currentWord); @@ -850,7 +849,7 @@ export async function updateActiveWordLetters( let tabChar = ""; let nlChar = ""; if (funbox?.functions?.getWordHtml) { - const cl = funbox.functions.getWordHtml(currentLetter); + const cl = funbox.functions?.getWordHtml(currentLetter); if (cl !== "") { currentLetter = cl; } diff --git a/frontend/src/ts/test/words-generator.ts b/frontend/src/ts/test/words-generator.ts index 0b7237800..7dabe7ef3 100644 --- a/frontend/src/ts/test/words-generator.ts +++ b/frontend/src/ts/test/words-generator.ts @@ -1,5 +1,4 @@ import Config, * as UpdateConfig from "../config"; -import * as FunboxList from "./funbox/funbox-list"; import * as CustomText from "./custom-text"; import * as Wordset from "./wordset"; import QuotesController, { @@ -17,6 +16,7 @@ import * as Arrays from "../utils/arrays"; import * as TestState from "../test/test-state"; import * as GetText from "../utils/generate"; import { FunboxWordOrder, LanguageObject } from "../utils/json-data"; +import { getActiveFunboxes } from "./funbox/list"; function shouldCapitalize(lastChar: string): boolean { return /[?!.؟]/.test(lastChar); @@ -35,9 +35,8 @@ export async function punctuateWord( const lastChar = Strings.getLastChar(previousWord); - const funbox = FunboxList.get(Config.funbox).find( - (f) => f.functions?.punctuateWord - ); + const funbox = getActiveFunboxes()?.find((fb) => fb.functions?.punctuateWord); + if (funbox?.functions?.punctuateWord) { return funbox.functions.punctuateWord(word); } @@ -302,25 +301,25 @@ async function applyEnglishPunctuationToWord(word: string): Promise<string> { } function getFunboxWordsFrequency(): Wordset.FunboxWordsFrequency | undefined { - const wordFunbox = FunboxList.get(Config.funbox).find( - (f) => f.functions?.getWordsFrequencyMode + const funbox = getActiveFunboxes().find( + (fb) => fb.functions?.getWordsFrequencyMode ); - if (wordFunbox?.functions?.getWordsFrequencyMode) { - return wordFunbox.functions.getWordsFrequencyMode(); + if (funbox?.functions?.getWordsFrequencyMode) { + return funbox.functions.getWordsFrequencyMode(); } return undefined; } async function getFunboxSection(): Promise<string[]> { const ret = []; - const sectionFunbox = FunboxList.get(Config.funbox).find( - (f) => f.functions?.pullSection - ); - if (sectionFunbox?.functions?.pullSection) { - const section = await sectionFunbox.functions.pullSection(Config.language); + + const funbox = getActiveFunboxes().find((fb) => fb.functions?.pullSection); + + if (funbox?.functions?.pullSection) { + const section = await funbox.functions.pullSection(Config.language); if (section === false || section === undefined) { - UpdateConfig.toggleFunbox(sectionFunbox.name); + UpdateConfig.toggleFunbox(funbox.name); throw new Error("Failed to pull section"); } @@ -339,19 +338,18 @@ function getFunboxWord( wordIndex: number, wordset?: Wordset.Wordset ): string { - const wordFunbox = FunboxList.get(Config.funbox).find( - (f) => f.functions?.getWord - ); - if (wordFunbox?.functions?.getWord) { - word = wordFunbox.functions.getWord(wordset, wordIndex); + const funbox = getActiveFunboxes()?.find((fb) => fb.functions?.getWord); + + if (funbox?.functions?.getWord) { + word = funbox.functions.getWord(wordset, wordIndex); } return word; } function applyFunboxesToWord(word: string): string { - for (const f of FunboxList.get(Config.funbox)) { - if (f.functions?.alterText) { - word = f.functions.alterText(word); + for (const fb of getActiveFunboxes()) { + if (fb.functions?.alterText) { + word = fb.functions.alterText(word); } } return word; @@ -384,7 +382,7 @@ function applyLazyModeToWord(word: string, language: LanguageObject): string { export function getWordOrder(): FunboxWordOrder { const wordOrder = - FunboxList.get(Config.funbox) + getActiveFunboxes() .find((f) => f.properties?.find((fp) => fp.startsWith("wordOrder"))) ?.properties?.find((fp) => fp.startsWith("wordOrder")) ?? ""; @@ -409,7 +407,7 @@ export function getWordsLimit(): number { } const funboxToPush = - FunboxList.get(Config.funbox) + getActiveFunboxes() .find((f) => f.properties?.find((fp) => fp.startsWith("toPush"))) ?.properties?.find((fp) => fp.startsWith("toPush:")) ?? ""; @@ -607,8 +605,8 @@ export async function generateWords( hasNewline: false, }; - const sectionFunbox = FunboxList.get(Config.funbox).find( - (f) => f.functions?.pullSection + const sectionFunbox = getActiveFunboxes().find( + (fb) => fb.functions?.pullSection ); isCurrentlyUsingFunboxSection = sectionFunbox?.functions?.pullSection !== undefined; @@ -632,11 +630,9 @@ export async function generateWords( wordList = wordList.reverse(); } - const wordFunbox = FunboxList.get(Config.funbox).find( - (f) => f.functions?.withWords - ); - if (wordFunbox?.functions?.withWords) { - currentWordset = await wordFunbox.functions.withWords(wordList); + const funbox = getActiveFunboxes().find((fb) => fb.functions?.withWords); + if (funbox?.functions?.withWords) { + currentWordset = await funbox.functions.withWords(wordList); } else { currentWordset = await Wordset.withWords(wordList); } diff --git a/frontend/src/ts/utils/json-data.ts b/frontend/src/ts/utils/json-data.ts index 93580bf61..ef51dce6b 100644 --- a/frontend/src/ts/utils/json-data.ts +++ b/frontend/src/ts/utils/json-data.ts @@ -1,7 +1,5 @@ -import { ConfigValue } from "@monkeytype/contracts/schemas/configs"; import { Accents } from "../test/lazy-mode"; import { hexToHSL } from "./colors"; -import { FunboxWordsFrequency, Wordset } from "../test/wordset"; /** * Fetches JSON data from the specified URL using the fetch API. @@ -276,29 +274,6 @@ export async function getCurrentGroup( return retgroup; } -let funboxList: FunboxMetadata[] | undefined; - -/** - * Fetches the list of funbox metadata from the server. - * @returns A promise that resolves to the list of funbox metadata. - */ -export async function getFunboxList(): Promise<FunboxMetadata[]> { - if (!funboxList) { - let list = await cachedFetchJson<FunboxMetadata[]>("/funbox/_list.json"); - list = list.sort((a, b) => { - const nameA = a.name.toLowerCase(); - const nameB = b.name.toLowerCase(); - if (nameA < nameB) return -1; - if (nameA > nameB) return 1; - return 0; - }); - funboxList = list; - return funboxList; - } else { - return funboxList; - } -} - export class Section { public title: string; public author: string; @@ -310,81 +285,8 @@ export class Section { } } -export type FunboxMetadata = { - name: string; - info: string; - canGetPb?: boolean; - alias?: string; - forcedConfig?: FunboxForcedConfig; - properties?: FunboxProperty[]; - functions?: FunboxFunctions; - hasCSS?: boolean; -}; - export type FunboxWordOrder = "normal" | "reverse"; -type FunboxProperty = - | "symmetricChars" - | "conflictsWithSymmetricChars" - | "changesWordsVisibility" - | "speaks" - | "unspeakable" - | "changesLayout" - | "ignoresLayout" - | "usesLayout" - | "ignoresLanguage" - | "noLigatures" - | "noLetters" - | "changesCapitalisation" - | "nospace" - | `toPush:${number}` - | "noInfiniteDuration" - | "changesWordsFrequency" - | `wordOrder:${FunboxWordOrder}`; - -export type FunboxForcedConfig = Record<string, ConfigValue[]>; - -export type FunboxFunctions = { - getWord?: (wordset?: Wordset, wordIndex?: number) => string; - punctuateWord?: (word: string) => string; - withWords?: (words?: string[]) => Promise<Wordset>; - alterText?: (word: string) => string; - applyConfig?: () => void; - applyGlobalCSS?: () => void; - clearGlobal?: () => void; - rememberSettings?: () => void; - toggleScript?: (params: string[]) => void; - pullSection?: (language?: string) => Promise<Section | false>; - handleSpace?: () => void; - handleChar?: (char: string) => string; - isCharCorrect?: (char: string, originalChar: string) => boolean; - preventDefaultEvent?: ( - event: JQuery.KeyDownEvent<Document, null, Document, Document> - ) => Promise<boolean>; - handleKeydown?: ( - event: JQuery.KeyDownEvent<Document, undefined, Document, Document> - ) => Promise<void>; - getResultContent?: () => string; - start?: () => void; - restart?: () => void; - getWordHtml?: (char: string, letterTag?: boolean) => string; - getWordsFrequencyMode?: () => FunboxWordsFrequency; -}; - -/** - * Fetches the funbox metadata for a given funbox from the server. - * @param funbox The name of the funbox. - * @returns A promise that resolves to the funbox metadata. - */ -export async function getFunbox( - funbox: string -): Promise<FunboxMetadata | undefined> { - const list: FunboxMetadata[] = await getFunboxList(); - return list.find(function (element) { - return element.name === funbox; - }); -} - export type FontObject = { name: string; display?: string; |