From fdadb4ae83cfa16b2d8f8666d265b705b71071e7 Mon Sep 17 00:00:00 2001 From: Jack Date: Wed, 4 Dec 2024 16:11:07 +0100 Subject: refactor: move funboxes to a shared package (@miodec) (#6063) --- backend/package.json | 1 + backend/src/api/controllers/result.ts | 29 +- backend/src/constants/funbox-list.ts | 375 ------------ backend/src/utils/pb.ts | 23 +- backend/src/utils/validation.ts | 137 ----- frontend/__tests__/root/config.spec.ts | 6 +- frontend/__tests__/test/funbox.spec.ts | 24 + frontend/__tests__/tsconfig.json | 2 +- frontend/package.json | 1 + frontend/scripts/json-validation.cjs | 28 - frontend/src/ts/commandline/lists.ts | 20 +- frontend/src/ts/commandline/lists/funbox.ts | 93 ++- frontend/src/ts/controllers/account-controller.ts | 3 +- frontend/src/ts/controllers/input-controller.ts | 69 +-- frontend/src/ts/db.ts | 8 +- frontend/src/ts/elements/account/result-filters.ts | 82 ++- frontend/src/ts/pages/settings.ts | 110 ++-- frontend/src/ts/ready.ts | 9 +- frontend/src/ts/test/funbox/funbox-functions.ts | 627 ++++++++++++++++++++ frontend/src/ts/test/funbox/funbox-list.ts | 302 ---------- frontend/src/ts/test/funbox/funbox-validation.ts | 210 ++----- frontend/src/ts/test/funbox/funbox.ts | 657 ++------------------- frontend/src/ts/test/funbox/list.ts | 72 +++ frontend/src/ts/test/funbox/memory-funbox-timer.ts | 6 +- frontend/src/ts/test/pace-caret.ts | 3 +- frontend/src/ts/test/result.ts | 25 +- frontend/src/ts/test/test-logic.ts | 18 +- frontend/src/ts/test/test-stats.ts | 6 +- frontend/src/ts/test/test-ui.ts | 23 +- frontend/src/ts/test/words-generator.ts | 58 +- frontend/src/ts/utils/json-data.ts | 98 --- frontend/static/funbox/_list.json | 204 ------- frontend/vitest.config.js | 7 + packages/funbox/.eslintrc.cjs | 5 + packages/funbox/__test__/tsconfig.json | 12 + packages/funbox/package.json | 30 + packages/funbox/src/index.ts | 19 + packages/funbox/src/list.ts | 450 ++++++++++++++ packages/funbox/src/types.ts | 74 +++ packages/funbox/src/util.ts | 17 + packages/funbox/src/validation.ts | 154 +++++ packages/funbox/tsconfig.json | 15 + packages/funbox/vitest.config.js | 11 + pnpm-lock.yaml | 331 ++--------- 44 files changed, 1889 insertions(+), 2565 deletions(-) delete mode 100644 backend/src/constants/funbox-list.ts create mode 100644 frontend/__tests__/test/funbox.spec.ts create mode 100644 frontend/src/ts/test/funbox/funbox-functions.ts delete mode 100644 frontend/src/ts/test/funbox/funbox-list.ts create mode 100644 frontend/src/ts/test/funbox/list.ts delete mode 100644 frontend/static/funbox/_list.json create mode 100644 packages/funbox/.eslintrc.cjs create mode 100644 packages/funbox/__test__/tsconfig.json create mode 100644 packages/funbox/package.json create mode 100644 packages/funbox/src/index.ts create mode 100644 packages/funbox/src/list.ts create mode 100644 packages/funbox/src/types.ts create mode 100644 packages/funbox/src/util.ts create mode 100644 packages/funbox/src/validation.ts create mode 100644 packages/funbox/tsconfig.json create mode 100644 packages/funbox/vitest.config.js diff --git a/backend/package.json b/backend/package.json index d3e699820..bb64ab13f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -24,6 +24,7 @@ "dependencies": { "@date-fns/utc": "1.2.0", "@monkeytype/contracts": "workspace:*", + "@monkeytype/funbox": "workspace:*", "@monkeytype/util": "workspace:*", "@ts-rest/core": "3.51.0", "@ts-rest/express": "3.51.0", diff --git a/backend/src/api/controllers/result.ts b/backend/src/api/controllers/result.ts index 5ca243d27..b06cd0e23 100644 --- a/backend/src/api/controllers/result.ts +++ b/backend/src/api/controllers/result.ts @@ -6,7 +6,7 @@ import Logger from "../../utils/logger"; import "dotenv/config"; import { MonkeyResponse } from "../../utils/monkey-response"; import MonkeyError from "../../utils/error"; -import { areFunboxesCompatible, isTestTooShort } from "../../utils/validation"; +import { isTestTooShort } from "../../utils/validation"; import { implemented as anticheatImplemented, validateResult, @@ -22,7 +22,6 @@ import { getDailyLeaderboard } from "../../utils/daily-leaderboards"; import AutoRoleList from "../../constants/auto-roles"; import * as UserDAL from "../../dal/user"; import { buildMonkeyMail } from "../../utils/monkey-mail"; -import FunboxList from "../../constants/funbox-list"; import _, { omit } from "lodash"; import * as WeeklyXpLeaderboard from "../../services/weekly-xp-leaderboard"; import { UAParser } from "ua-parser-js"; @@ -57,6 +56,11 @@ import { getStartOfDayTimestamp, } from "@monkeytype/util/date-and-time"; import { MonkeyRequest } from "../types"; +import { + getFunbox, + checkCompatibility, + stringToFunboxNames, +} from "@monkeytype/funbox"; try { if (!anticheatImplemented()) throw new Error("undefined"); @@ -232,7 +236,9 @@ export async function addResult( } } - if (!areFunboxesCompatible(completedEvent.funbox ?? "")) { + const funboxNames = stringToFunboxNames(completedEvent.funbox ?? ""); + + if (!checkCompatibility(funboxNames)) { throw new MonkeyError(400, "Impossible funbox combination"); } @@ -660,7 +666,7 @@ async function calculateXp( charStats, punctuation, numbers, - funbox, + funbox: resultFunboxes, } = result; const { @@ -713,12 +719,15 @@ async function calculateXp( } } - if (funboxBonusConfiguration > 0) { - const funboxModifier = _.sumBy(funbox.split("#"), (funboxName) => { - const funbox = FunboxList.find((f) => f.name === funboxName); - const difficultyLevel = funbox?.difficultyLevel ?? 0; - return Math.max(difficultyLevel * funboxBonusConfiguration, 0); - }); + if (funboxBonusConfiguration > 0 && resultFunboxes !== "none") { + const funboxModifier = _.sumBy( + stringToFunboxNames(resultFunboxes), + (funboxName) => { + const funbox = getFunbox(funboxName); + const difficultyLevel = funbox?.difficultyLevel ?? 0; + return Math.max(difficultyLevel * funboxBonusConfiguration, 0); + } + ); if (funboxModifier > 0) { modifier += funboxModifier; breakdown.funbox = Math.round(baseXp * funboxModifier); diff --git a/backend/src/constants/funbox-list.ts b/backend/src/constants/funbox-list.ts deleted file mode 100644 index 6ee0bb06a..000000000 --- a/backend/src/constants/funbox-list.ts +++ /dev/null @@ -1,375 +0,0 @@ -export type FunboxMetadata = { - name: string; - canGetPb: boolean; - difficultyLevel: number; - properties?: string[]; - frontendForcedConfig?: Record; - frontendFunctions?: string[]; -}; - -const FunboxList: FunboxMetadata[] = [ - { - canGetPb: false, - difficultyLevel: 1, - properties: ["ignoresLanguage", "ignoresLayout", "noLetters"], - frontendForcedConfig: { - numbers: [false], - }, - frontendFunctions: [ - "getWord", - "punctuateWord", - "rememberSettings", - "handleChar", - ], - name: "58008", - }, - { - canGetPb: true, - difficultyLevel: 2, - frontendFunctions: ["applyCSS"], - name: "nausea", - }, - { - canGetPb: true, - difficultyLevel: 3, - frontendFunctions: ["applyCSS"], - name: "round_round_baby", - }, - { - canGetPb: true, - difficultyLevel: 1, - properties: ["changesWordsVisibility", "usesLayout"], - frontendForcedConfig: { - highlightMode: ["letter", "off"], - }, - frontendFunctions: ["applyCSS", "applyConfig", "rememberSettings"], - name: "simon_says", - }, - { - canGetPb: true, - difficultyLevel: 3, - frontendFunctions: ["applyCSS"], - name: "mirror", - }, - { - canGetPb: true, - difficultyLevel: 3, - frontendFunctions: ["applyCSS"], - name: "upside_down", - }, - { - canGetPb: true, - difficultyLevel: 1, - properties: ["changesWordsVisibility", "speaks"], - frontendForcedConfig: { - highlightMode: ["letter", "off"], - }, - frontendFunctions: [ - "applyCSS", - "applyConfig", - "rememberSettings", - "toggleScript", - ], - name: "tts", - }, - { - canGetPb: true, - difficultyLevel: 2, - properties: ["noLigatures", "conflictsWithSymmetricChars"], - frontendFunctions: ["applyCSS"], - name: "choo_choo", - }, - { - canGetPb: false, - difficultyLevel: 1, - properties: [ - "ignoresLanguage", - "ignoresLayout", - "nospace", - "noLetters", - "symmetricChars", - ], - frontendForcedConfig: { - punctuation: [false], - numbers: [false], - highlightMode: ["letter", "off"], - }, - frontendFunctions: [ - "getWord", - "applyConfig", - "rememberSettings", - "handleChar", - "isCharCorrect", - "preventDefaultEvent", - "getWordHtml", - ], - name: "arrows", - }, - { - canGetPb: false, - difficultyLevel: 2, - properties: ["changesCapitalisation"], - frontendFunctions: ["alterText"], - name: "rAnDoMcAsE", - }, - { - canGetPb: false, - difficultyLevel: 1, - properties: ["changesCapitalisation"], - frontendFunctions: ["alterText"], - name: "capitals", - }, - { - canGetPb: true, - difficultyLevel: 1, - properties: ["changesLayout", "noInfiniteDuration"], - frontendFunctions: [ - "applyConfig", - "rememberSettings", - "handleSpace", - "getResultContent", - "restart", - ], - name: "layoutfluid", - }, - { - canGetPb: true, - difficultyLevel: 1, - properties: ["noLigatures"], - frontendFunctions: ["applyCSS"], - name: "earthquake", - }, - { - canGetPb: true, - difficultyLevel: 0, - frontendFunctions: ["applyCSS"], - name: "space_balls", - }, - { - canGetPb: false, - difficultyLevel: 1, - properties: ["ignoresLanguage", "unspeakable"], - frontendFunctions: ["getWord"], - name: "gibberish", - }, - { - canGetPb: false, - difficultyLevel: 1, - properties: ["ignoresLanguage", "noLetters", "unspeakable"], - frontendForcedConfig: { - punctuation: [false], - numbers: [false], - }, - frontendFunctions: ["getWord"], - name: "ascii", - }, - { - canGetPb: false, - difficultyLevel: 1, - properties: ["ignoresLanguage", "noLetters", "unspeakable"], - frontendForcedConfig: { - punctuation: [false], - numbers: [false], - }, - frontendFunctions: ["getWord"], - name: "specials", - }, - { - canGetPb: true, - difficultyLevel: 0, - properties: ["changesWordsVisibility", "toPush:2", "noInfiniteDuration"], - name: "plus_one", - }, - { - canGetPb: true, - difficultyLevel: 1, - properties: ["changesWordsVisibility", "toPush:1", "noInfiniteDuration"], - name: "plus_zero", - }, - { - canGetPb: true, - difficultyLevel: 0, - properties: ["changesWordsVisibility", "toPush:3", "noInfiniteDuration"], - name: "plus_two", - }, - { - canGetPb: true, - difficultyLevel: 0, - properties: ["changesWordsVisibility", "toPush:4", "noInfiniteDuration"], - name: "plus_three", - }, - { - canGetPb: true, - difficultyLevel: 1, - properties: ["changesWordsVisibility"], - frontendForcedConfig: { - highlightMode: ["letter", "off"], - }, - frontendFunctions: ["applyCSS", "rememberSettings", "handleKeydown"], - name: "read_ahead_easy", - }, - { - canGetPb: true, - difficultyLevel: 2, - properties: ["changesWordsVisibility"], - frontendForcedConfig: { - highlightMode: ["letter", "off"], - }, - frontendFunctions: ["applyCSS", "rememberSettings", "handleKeydown"], - name: "read_ahead", - }, - { - canGetPb: true, - difficultyLevel: 3, - properties: ["changesWordsVisibility"], - frontendForcedConfig: { - highlightMode: ["letter", "off"], - }, - frontendFunctions: ["applyCSS", "rememberSettings", "handleKeydown"], - name: "read_ahead_hard", - }, - { - canGetPb: true, - difficultyLevel: 3, - properties: ["changesWordsVisibility", "noInfiniteDuration"], - frontendForcedConfig: { - mode: ["words", "quote", "custom"], - }, - frontendFunctions: ["applyConfig", "rememberSettings", "start", "restart"], - name: "memory", - }, - { - canGetPb: false, - difficultyLevel: 0, - properties: ["nospace"], - frontendForcedConfig: { - highlightMode: ["letter", "off"], - }, - frontendFunctions: ["applyConfig", "rememberSettings"], - name: "nospace", - }, - { - canGetPb: false, - difficultyLevel: 0, - properties: ["noInfiniteDuration", "ignoresLanguage"], - frontendForcedConfig: { - punctuation: [false], - numbers: [false], - }, - frontendFunctions: ["pullSection"], - name: "poetry", - }, - { - canGetPb: false, - difficultyLevel: 0, - properties: ["noInfiniteDuration", "ignoresLanguage"], - frontendForcedConfig: { - punctuation: [false], - numbers: [false], - }, - frontendFunctions: ["pullSection"], - name: "wikipedia", - }, - { - canGetPb: false, - difficultyLevel: 0, - properties: ["changesWordsFrequency"], - frontendFunctions: ["getWord"], - name: "weakspot", - }, - { - canGetPb: false, - difficultyLevel: 0, - properties: ["unspeakable", "ignoresLanguage"], - frontendFunctions: ["withWords"], - name: "pseudolang", - }, - { - canGetPb: false, - difficultyLevel: 1, - properties: ["ignoresLanguage", "ignoresLayout", "noLetters"], - frontendForcedConfig: { - numbers: [false], - }, - frontendFunctions: ["getWord", "punctuateWord", "rememberSettings"], - name: "IPv4", - }, - { - canGetPb: false, - difficultyLevel: 1, - properties: ["ignoresLanguage", "ignoresLayout", "noLetters"], - frontendForcedConfig: { - numbers: [false], - }, - frontendFunctions: ["getWord", "punctuateWord", "rememberSettings"], - name: "IPv6", - }, - { - canGetPb: false, - difficultyLevel: 1, - properties: ["ignoresLanguage", "ignoresLayout", "noLetters"], - frontendForcedConfig: { - numbers: [false], - punctuation: [false], - }, - frontendFunctions: ["getWord"], - name: "binary", - }, - { - canGetPb: false, - difficultyLevel: 1, - properties: ["ignoresLanguage", "ignoresLayout", "noLetters"], - frontendForcedConfig: { - numbers: [false], - }, - frontendFunctions: ["getWord", "punctuateWord", "rememberSettings"], - name: "hexadecimal", - }, - { - canGetPb: false, - difficultyLevel: 0, - properties: ["changesWordsFrequency"], - frontendFunctions: ["getWordsFrequencyMode"], - name: "zipf", - }, - { - canGetPb: false, - difficultyLevel: 1, - properties: ["ignoresLanguage", "ignoresLayout", "noLetters", "noSpace"], - frontendFunctions: ["alterText"], - name: "morse", - }, - { - canGetPb: true, - difficultyLevel: 0, - properties: ["noLigatures"], - name: "crt", - }, - { - name: "backwards", - properties: [ - "noLigatures", - "conflictsWithSymmetricChars", - "wordOrder:reverse", - ], - frontendFunctions: ["applyCSS"], - canGetPb: true, - difficultyLevel: 3, - }, - { - canGetPb: true, - difficultyLevel: 1, - properties: ["noLigatures"], - frontendFunctions: ["alterText"], - name: "ddoouubblleedd", - }, - { - canGetPb: false, - difficultyLevel: 1, - properties: ["changesCapitalisation"], - frontendFunctions: ["alterText"], - name: "instant_messaging", - }, -]; - -export default FunboxList; diff --git a/backend/src/utils/pb.ts b/backend/src/utils/pb.ts index e733e25c7..fcc9702eb 100644 --- a/backend/src/utils/pb.ts +++ b/backend/src/utils/pb.ts @@ -1,12 +1,11 @@ import _ from "lodash"; -import FunboxList from "../constants/funbox-list"; - import { Mode, PersonalBest, PersonalBests, } from "@monkeytype/contracts/schemas/shared"; import { Result as ResultType } from "@monkeytype/contracts/schemas/results"; +import { getFunboxesFromString } from "@monkeytype/funbox"; export type LbPersonalBests = { time: Record>; @@ -21,20 +20,16 @@ type CheckAndUpdatePbResult = { type Result = Omit, "_id" | "name">; export function canFunboxGetPb(result: Result): boolean { - const funbox = result.funbox; - if (funbox === undefined || funbox === "" || funbox === "none") return true; - - let ret = true; - const resultFunboxes = funbox.split("#"); - for (const funbox of FunboxList) { - if (resultFunboxes.includes(funbox.name)) { - if (!funbox.canGetPb) { - ret = false; - } - } + const funboxString = result.funbox; + if ( + funboxString === undefined || + funboxString === "" || + funboxString === "none" + ) { + return true; } - return ret; + return getFunboxesFromString(funboxString).every((f) => f.canGetPb); } export function checkAndUpdatePb( diff --git a/backend/src/utils/validation.ts b/backend/src/utils/validation.ts index 401f4bb24..12a958a3c 100644 --- a/backend/src/utils/validation.ts +++ b/backend/src/utils/validation.ts @@ -1,7 +1,5 @@ import _ from "lodash"; -import { default as FunboxList } from "../constants/funbox-list"; import { CompletedEvent } from "@monkeytype/contracts/schemas/results"; -import { intersect } from "@monkeytype/util/arrays"; export function isTestTooShort(result: CompletedEvent): boolean { const { mode, mode2, customText, testDuration, bailedOut } = result; @@ -48,138 +46,3 @@ export function isTestTooShort(result: CompletedEvent): boolean { return false; } - -export function areFunboxesCompatible(funboxesString: string): boolean { - const funboxes = funboxesString.split("#").filter((f) => f !== "none"); - - const funboxesToCheck = FunboxList.filter((f) => funboxes.includes(f.name)); - - const allFunboxesAreValid = funboxesToCheck.length === funboxes.length; - const oneWordModifierMax = - funboxesToCheck.filter( - (f) => - f.frontendFunctions?.includes("getWord") ?? - f.frontendFunctions?.includes("pullSection") ?? - f.frontendFunctions?.includes("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 oneWordOrderMax = - funboxesToCheck.filter((f) => - f.properties?.find((fp) => fp.startsWith("wordOrder")) - ).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 canSpeak = - funboxesToCheck.filter((f) => - f.properties?.find((fp) => fp === "speaks" || fp === "unspeakable") - ).length <= 1; - const hasLanguageToSpeak = - funboxesToCheck.filter((f) => f.properties?.find((fp) => fp === "speaks")) - .length === 0 || - funboxesToCheck.filter((f) => - f.properties?.find((fp) => fp === "ignoresLanguage") - ).length === 0; - const oneToPushOrPullSectionMax = - funboxesToCheck.filter( - (f) => - f.properties?.some((fp) => fp.startsWith("toPush:")) ?? - f.frontendFunctions?.includes("pullSection") - ).length <= 1; - // const oneApplyCSSMax = - // funboxesToCheck.filter((f) => f.frontendFunctions?.includes("applyCSS")) - // .length <= 1; //todo: move all funbox stuff to the shared package, this is ok to remove for now - const onePunctuateWordMax = - funboxesToCheck.filter((f) => - f.frontendFunctions?.includes("punctuateWord") - ).length <= 1; - const oneCharCheckerMax = - funboxesToCheck.filter((f) => - f.frontendFunctions?.includes("isCharCorrect") - ).length <= 1; - const oneCharReplacerMax = - funboxesToCheck.filter((f) => f.frontendFunctions?.includes("getWordHtml")) - .length <= 1; - const oneChangesCapitalisationMax = - funboxesToCheck.filter((f) => - f.properties?.find((fp) => fp === "changesCapitalisation") - ).length <= 1; - const allowedConfig = {} as Record; - let noConfigConflicts = true; - for (const f of funboxesToCheck) { - if (!f.frontendForcedConfig) continue; - for (const key in f.frontendForcedConfig) { - const allowedConfigValue = allowedConfig[key]; - const funboxValue = f.frontendForcedConfig[key]; - if (allowedConfigValue !== undefined && funboxValue !== undefined) { - if ( - intersect(allowedConfigValue, funboxValue, true) - .length === 0 - ) { - noConfigConflicts = false; - break; - } - } else if (funboxValue !== undefined) { - allowedConfig[key] = funboxValue; - } - } - } - - return ( - allFunboxesAreValid && - oneWordModifierMax && - layoutUsability && - oneNospaceOrToPushMax && - oneChangesWordsVisibilityMax && - oneFrequencyChangesMax && - noFrequencyChangesConflicts && - capitalisationChangePosibility && - noConflictsWithSymmetricChars && - canSpeak && - hasLanguageToSpeak && - oneToPushOrPullSectionMax && - // oneApplyCSSMax && - onePunctuateWordMax && - oneCharCheckerMax && - oneCharReplacerMax && - oneChangesCapitalisationMax && - noConfigConflicts && - oneWordOrderMax - ); -} diff --git a/frontend/__tests__/root/config.spec.ts b/frontend/__tests__/root/config.spec.ts index d878344c6..76683304b 100644 --- a/frontend/__tests__/root/config.spec.ts +++ b/frontend/__tests__/root/config.spec.ts @@ -332,10 +332,8 @@ describe("Config", () => { expect(Config.setFavThemes([stringOfLength(51)])).toBe(false); }); it("setFunbox", () => { - expect(Config.setFunbox("one")).toBe(true); - expect(Config.setFunbox("one#two")).toBe(true); - expect(Config.setFunbox("one#two#")).toBe(true); - expect(Config.setFunbox(stringOfLength(100))).toBe(true); + expect(Config.setFunbox("mirror")).toBe(true); + expect(Config.setFunbox("mirror#58008")).toBe(true); expect(Config.setFunbox(stringOfLength(101))).toBe(false); }); diff --git a/frontend/__tests__/test/funbox.spec.ts b/frontend/__tests__/test/funbox.spec.ts new file mode 100644 index 000000000..57c0061c5 --- /dev/null +++ b/frontend/__tests__/test/funbox.spec.ts @@ -0,0 +1,24 @@ +import { getAllFunboxes } from "../../src/ts/test/funbox/list"; + +describe("funbox", () => { + describe("list", () => { + it("should have every frontendFunctions function defined", () => { + for (const funbox of getAllFunboxes()) { + const packageFunctions = (funbox.frontendFunctions ?? []).sort(); + const implementations = Object.keys(funbox.functions ?? {}).sort(); + + let message = "has mismatched functions"; + + if (packageFunctions.length > implementations.length) { + message = `missing function implementation in frontend`; + } else if (implementations.length > packageFunctions.length) { + message = `missing properties in frontendFunctions in the package`; + } + + expect(packageFunctions, `Funbox ${funbox.name} ${message}`).toEqual( + implementations + ); + } + }); + }); +}); diff --git a/frontend/__tests__/tsconfig.json b/frontend/__tests__/tsconfig.json index b1d8a156f..14bb27809 100644 --- a/frontend/__tests__/tsconfig.json +++ b/frontend/__tests__/tsconfig.json @@ -9,6 +9,6 @@ "ts-node": { "files": true }, - "files": ["../src/ts/types/types.d.ts", "vitest.d.ts"], + "files": ["vitest.d.ts"], "include": ["./**/*.spec.ts", "./setup-tests.ts"] } diff --git a/frontend/package.json b/frontend/package.json index 0bede596a..f928fbd2c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -70,6 +70,7 @@ "dependencies": { "@date-fns/utc": "1.2.0", "@monkeytype/contracts": "workspace:*", + "@monkeytype/funbox": "workspace:*", "@monkeytype/util": "workspace:*", "@ts-rest/core": "3.51.0", "canvas-confetti": "1.5.1", diff --git a/frontend/scripts/json-validation.cjs b/frontend/scripts/json-validation.cjs index 67ec3e375..a70413fd4 100644 --- a/frontend/scripts/json-validation.cjs +++ b/frontend/scripts/json-validation.cjs @@ -46,34 +46,6 @@ function validateOthers() { return reject(new Error(fontsValidator.errors[0].message)); } - //funbox - const funboxData = JSON.parse( - fs.readFileSync("./static/funbox/_list.json", { - encoding: "utf8", - flag: "r", - }) - ); - const funboxSchema = { - type: "array", - items: { - type: "object", - properties: { - name: { type: "string" }, - info: { type: "string" }, - canGetPb: { type: "boolean" }, - alias: { type: "string" }, - }, - required: ["name", "info", "canGetPb"], - }, - }; - const funboxValidator = ajv.compile(funboxSchema); - if (funboxValidator(funboxData)) { - console.log("Funbox list JSON schema is \u001b[32mvalid\u001b[0m"); - } else { - console.log("Funbox list JSON schema is \u001b[31minvalid\u001b[0m"); - return reject(new Error(funboxValidator.errors[0].message)); - } - //themes const themesData = JSON.parse( fs.readFileSync("./static/themes/_list.json", { 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 { 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 { 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 { 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 { 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( language: string, difficulty: Difficulty, lazyMode: boolean, - funbox: string + funboxes: FunboxMetadata[] ): Promise { - 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 @@ -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 += - ""; - html += ""; - html += ""; + html += ""; + html += ""; - for (const funbox of funboxList) { - html += ``; - } + for (const funbox of getAllFunboxes()) { + html += ``; + } - html += ""; + html += ""; - 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 = Record>; @@ -588,46 +593,37 @@ async function fillSettingsPage(): Promise { funboxEl.innerHTML = `
none
`; 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 += `
${funbox.name.replace( - /_/g, - " " - )}
`; - } else if (funbox.name === "upside_down") { - funboxElHTML += `
${funbox.name.replace( - /_/g, - " " - )}
`; - } else { - funboxElHTML += `
${funbox.name.replace( - /_/g, - " " - )}
`; - } + for (const funbox of getAllFunboxes()) { + if (funbox.name === "mirror") { + funboxElHTML += `
${funbox.name.replace( + /_/g, + " " + )}
`; + } else if (funbox.name === "upside_down") { + funboxElHTML += `
${funbox.name.replace( + /_/g, + " " + )}
`; + } else { + funboxElHTML += `
${funbox.name.replace( + /_/g, + " " + )}
`; } - 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; + alterText?: (word: string) => string; + applyConfig?: () => void; + applyGlobalCSS?: () => void; + clearGlobal?: () => void; + rememberSettings?: () => void; + toggleScript?: (params: string[]) => void; + pullSection?: (language?: string) => Promise
; + handleSpace?: () => void; + handleChar?: (char: string) => string; + isCharCorrect?: (char: string, originalChar: string) => boolean; + preventDefaultEvent?: ( + event: JQuery.KeyDownEvent + ) => Promise; + handleKeydown?: ( + event: JQuery.KeyDownEvent + ) => Promise; + getResultContent?: () => string; + start?: () => void; + restart?: () => void; + getWordHtml?: (char: string, letterTag?: boolean) => string; + getWordsFrequencyMode?: () => FunboxWordsFrequency; +}; + +async function readAheadHandleKeydown( + event: JQuery.KeyDownEvent +): Promise { + 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; + 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 = {}; + 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> = { + "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 { + return ["ArrowLeft", "ArrowUp", "ArrowRight", "ArrowDown"].includes( + event.key + ); + }, + getWordHtml(char: string, letterTag?: boolean): string { + let retval = ""; + if (char === "↑") { + if (letterTag) retval += ``; + retval += ``; + if (letterTag) retval += ``; + } + if (char === "↓") { + if (letterTag) retval += ``; + retval += ``; + if (letterTag) retval += ``; + } + if (char === "←") { + if (letterTag) retval += ``; + retval += ``; + if (letterTag) retval += ``; + } + if (char === "→") { + if (letterTag) retval += ``; + retval += ``; + if (letterTag) retval += ``; + } + 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 { + await readAheadHandleKeydown(event); + }, + }, + read_ahead: { + rememberSettings(): void { + save( + "highlightMode", + Config.highlightMode, + UpdateConfig.setHighlightMode + ); + }, + async handleKeydown(event): Promise { + await readAheadHandleKeydown(event); + }, + }, + read_ahead_hard: { + rememberSettings(): void { + save( + "highlightMode", + Config.highlightMode, + UpdateConfig.setHighlightMode + ); + }, + async handleKeydown(event): Promise { + 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 { + return getPoem(); + }, + }, + wikipedia: { + async pullSection(lang?: string): Promise { + return getSection((lang ?? "") || "english"); + }, + }, + weakspot: { + getWord(wordset?: Wordset): string { + if (wordset !== undefined) return WeakSpot.getWord(wordset); + else return ""; + }, + }, + pseudolang: { + async withWords(words?: string[]): Promise { + 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('
'); + $("body").addClass("crtmode"); + $("#globalFunBoxTheme").attr("href", `funbox/crt.css`); + }, + clearGlobal(): void { + $("#scanline").remove(); + $("body").removeClass("crtmode"); + $("#globalFunBoxTheme").attr("href", ``); + }, + }, +}; + +export function getFunboxFunctions(): Record { + return list as Record; +} 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 = {}; // 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; - 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 = {}; - 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 { - return ["ArrowLeft", "ArrowUp", "ArrowRight", "ArrowDown"].includes( - event.key - ); - }, - getWordHtml(char: string, letterTag?: boolean): string { - let retval = ""; - if (char === "↑") { - if (letterTag) retval += ``; - retval += ``; - if (letterTag) retval += ``; - } - if (char === "↓") { - if (letterTag) retval += ``; - retval += ``; - if (letterTag) retval += ``; - } - if (char === "←") { - if (letterTag) retval += ``; - retval += ``; - if (letterTag) retval += ``; - } - if (char === "→") { - if (letterTag) retval += ``; - retval += ``; - if (letterTag) retval += ``; - } - 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 -): Promise { - 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 { - await readAheadHandleKeydown(event); - }, -}); - -FunboxList.setFunboxFunctions("read_ahead", { - rememberSettings(): void { - save("highlightMode", Config.highlightMode, UpdateConfig.setHighlightMode); - }, - async handleKeydown(event): Promise { - await readAheadHandleKeydown(event); - }, -}); - -FunboxList.setFunboxFunctions("read_ahead_hard", { - rememberSettings(): void { - save("highlightMode", Config.highlightMode, UpdateConfig.setHighlightMode); - }, - async handleKeydown(event): Promise { - 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 { - return getPoem(); - }, -}); - -FunboxList.setFunboxFunctions("wikipedia", { - async pullSection(lang?: string): Promise { - 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 { - 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 { // 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 { 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 { 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 { } 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 { - 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('
'); - $("body").addClass("crtmode"); - $("#globalFunBoxTheme").attr("href", `funbox/crt.css`); - }, - clearGlobal(): void { - $("#scanline").remove(); - $("body").removeClass("crtmode"); - $("#globalFunBoxTheme").attr("href", ``); - }, -}); - async function setFunboxBodyClasses(): Promise { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { return (await import("html2canvas")).default; @@ -330,9 +330,8 @@ export async function updateHintsPosition(): Promise { function getWordHTML(word: string): string { let newlineafter = false; let retval = `
`; - 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 { } (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 { $(".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 { } 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 { 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 { - if (!funboxList) { - let list = await cachedFetchJson("/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; - -export type FunboxFunctions = { - getWord?: (wordset?: Wordset, wordIndex?: number) => string; - punctuateWord?: (word: string) => string; - withWords?: (words?: string[]) => Promise; - alterText?: (word: string) => string; - applyConfig?: () => void; - applyGlobalCSS?: () => void; - clearGlobal?: () => void; - rememberSettings?: () => void; - toggleScript?: (params: string[]) => void; - pullSection?: (language?: string) => Promise
; - handleSpace?: () => void; - handleChar?: (char: string) => string; - isCharCorrect?: (char: string, originalChar: string) => boolean; - preventDefaultEvent?: ( - event: JQuery.KeyDownEvent - ) => Promise; - handleKeydown?: ( - event: JQuery.KeyDownEvent - ) => Promise; - 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 { - const list: FunboxMetadata[] = await getFunboxList(); - return list.find(function (element) { - return element.name === funbox; - }); -} - export type FontObject = { name: string; display?: string; diff --git a/frontend/static/funbox/_list.json b/frontend/static/funbox/_list.json deleted file mode 100644 index bbebd6527..000000000 --- a/frontend/static/funbox/_list.json +++ /dev/null @@ -1,204 +0,0 @@ -[ - { - "name": "nausea", - "info": "I think I'm gonna be sick.", - "canGetPb": true - }, - { - "name": "round_round_baby", - "info": "...right round, like a record baby. Right, round round round.", - "canGetPb": true - }, - { - "name": "simon_says", - "info": "Type what simon says.", - "canGetPb": true - }, - { - "name": "mirror", - "info": "Everything is mirrored!", - "canGetPb": true - }, - {"name": "upside_down", - "info": "Everything is upside down!", - "canGetPb": true - }, - { - "name": "tts", - "info": "Listen closely.", - "canGetPb": true - }, - { - "name": "choo_choo", - "info": "All the letters are spinning!", - "canGetPb": true - }, - { - "name": "arrows", - "info": "Play it on a pad!", - "canGetPb": false - }, - { - "name": "rAnDoMcAsE", - "info": "I kInDa LiKe HoW iNeFfIcIeNt QwErTy Is.", - "canGetPb": false - }, - { - "name": "capitals", - "info": "Capitalize Every Word.", - "canGetPb": false - }, - { - "name": "layoutfluid", - "info": "Switch between layouts specified below proportionately to the length of the test.", - "canGetPb": true - }, - { - "name": "earthquake", - "info": "Everybody get down! The words are shaking!", - "canGetPb": true - }, - { - "name": "space_balls", - "info": "In a galaxy far far away.", - "canGetPb": true - }, - { - "name": "gibberish", - "info": "Anvbuefl dizzs eoos alsb?", - "canGetPb": false - }, - { - "name": "58008", - "alias": "numbers", - "info": "A special mode for accountants.", - "canGetPb": false - }, - { - "name": "ascii", - "info": "Where was the ampersand again?. Only ASCII characters.", - "canGetPb": false - }, - { - "name": "specials", - "info": "!@#$%^&*. Only special characters.", - "canGetPb": false - }, - { - "name": "plus_zero", - "info": "React quickly! Only the current word is visible.", - "canGetPb": true - }, - { - "name": "plus_one", - "info": "Only one future word is visible.", - "canGetPb": true - }, - { - "name": "plus_two", - "info": "Only two future words are visible.", - "canGetPb": true - }, - { - "name": "plus_three", - "info": "Only three future words are visible.", - "canGetPb": true - }, - { - "name": "read_ahead_easy", - "info": "Only the current word is invisible.", - "canGetPb": true - }, - { - "name": "read_ahead", - "info": "Current and the next word are invisible!", - "canGetPb": true - }, - { - "name": "read_ahead_hard", - "info": "Current and the next two words are invisible!", - "canGetPb": true - }, - { - "name": "memory", - "info": "Test your memory. Remember the words and type them blind.", - "canGetPb": true - }, - { - "name": "nospace", - "info": "Whoneedsspacesanyway?", - "canGetPb": false - }, - { - "name": "poetry", - "info": "Practice typing some beautiful prose.", - "canGetPb": false - }, - { - "name": "wikipedia", - "info": "Practice typing wikipedia sections.", - "canGetPb": false - }, - { - "name": "weakspot", - "info": "Focus on slow and mistyped letters.", - "canGetPb": false - }, - { - "name": "pseudolang", - "info": "Nonsense words that look like the current language.", - "canGetPb": false - }, - { - "name": "IPv4", - "alias": "network", - "info": "For sysadmins.", - "canGetPb": false - }, - { - "name": "IPv6", - "alias": "network", - "info": "For sysadmins with a long beard.", - "canGetPb": false - }, - { - "name": "binary", - "info": "01000010 01100101 01100101 01110000 00100000 01100010 01101111 01101111 01110000 00101110", - "canGetPb": false - }, - { - "name": "hexadecimal", - "info": "0x38 0x20 0x74 0x69 0x6D 0x65 0x73 0x20 0x6D 0x6F 0x72 0x65 0x20 0x62 0x6F 0x6F 0x70 0x21", - "canGetPb": false - }, - { - "name": "zipf", - "info": "Words are generated according to Zipf's law. (not all languages will produce Zipfy results, use with caution)", - "canGetPb": false - }, - { - "name": "morse", - "info": "-.../././.--./ -.../---/---/.--./-.-.--/ ", - "canGetPb": false - }, - { - "name": "crt", - "info": "Go back to the 1980s", - "canGetPb": true - }, - { - "name": "backwards", - "info": "...sdrawkcab epyt ot yrt woN", - "canGetPb": true - }, - { - "name": "ddoouubblleedd", - "info": "TTyyppee eevveerryytthhiinngg ttwwiiccee..", - "canGetPb": true - }, - { - "name": "instant_messaging", - "info": "Who needs shift anyway?", - "canGetPb": false - } -] diff --git a/frontend/vitest.config.js b/frontend/vitest.config.js index 3d607161a..8ab452026 100644 --- a/frontend/vitest.config.js +++ b/frontend/vitest.config.js @@ -10,5 +10,12 @@ export default defineConfig({ coverage: { include: ["**/*.ts"], }, + deps: { + optimizer: { + web: { + include: ["@monkeytype/funbox"], + }, + }, + }, }, }); diff --git a/packages/funbox/.eslintrc.cjs b/packages/funbox/.eslintrc.cjs new file mode 100644 index 000000000..922de4abe --- /dev/null +++ b/packages/funbox/.eslintrc.cjs @@ -0,0 +1,5 @@ +/** @type {import("eslint").Linter.Config} */ +module.exports = { + root: true, + extends: ["@monkeytype/eslint-config"], +}; diff --git a/packages/funbox/__test__/tsconfig.json b/packages/funbox/__test__/tsconfig.json new file mode 100644 index 000000000..8d8a39621 --- /dev/null +++ b/packages/funbox/__test__/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@monkeytype/typescript-config/base.json", + "compilerOptions": { + "noEmit": true, + "types": ["vitest/globals"] + }, + "ts-node": { + "files": true + }, + // "files": ["../src/types/types.d.ts"], + "include": ["./**/*.ts", "./**/*.spec.ts", "./setup-tests.ts"] +} diff --git a/packages/funbox/package.json b/packages/funbox/package.json new file mode 100644 index 000000000..525e3b83d --- /dev/null +++ b/packages/funbox/package.json @@ -0,0 +1,30 @@ +{ + "name": "@monkeytype/funbox", + "private": true, + "scripts": { + "dev": "rimraf ./dist && monkeytype-esbuild --watch", + "build": "rimraf ./dist && npm run madge && monkeytype-esbuild", + "madge": " madge --circular --extensions ts ./src", + "ts-check": "tsc --noEmit", + "lint": "eslint \"./**/*.ts\"" + }, + "devDependencies": { + "@monkeytype/util": "workspace:*", + "@monkeytype/esbuild": "workspace:*", + "@monkeytype/eslint-config": "workspace:*", + "@monkeytype/typescript-config": "workspace:*", + "chokidar": "3.6.0", + "eslint": "8.57.0", + "madge": "8.0.0", + "rimraf": "6.0.1", + "typescript": "5.5.4", + "vitest": "2.0.5" + }, + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.cjs" + } + } +} diff --git a/packages/funbox/src/index.ts b/packages/funbox/src/index.ts new file mode 100644 index 000000000..dc36d1f68 --- /dev/null +++ b/packages/funbox/src/index.ts @@ -0,0 +1,19 @@ +import { getList, getFunbox, getObject } from "./list"; +import { FunboxMetadata, FunboxName, FunboxProperty } from "./types"; +import { stringToFunboxNames } from "./util"; +import { checkCompatibility } from "./validation"; + +export type { FunboxName, FunboxMetadata, FunboxProperty }; +export { checkCompatibility, stringToFunboxNames, getFunbox }; + +export function getFunboxesFromString(names: string): FunboxMetadata[] { + return getFunbox(stringToFunboxNames(names)); +} + +export function getAllFunboxes(): FunboxMetadata[] { + return getList(); +} + +export function getFunboxObject(): Record { + return getObject(); +} diff --git a/packages/funbox/src/list.ts b/packages/funbox/src/list.ts new file mode 100644 index 000000000..06a5055f5 --- /dev/null +++ b/packages/funbox/src/list.ts @@ -0,0 +1,450 @@ +import { FunboxMetadata, FunboxName } from "./types"; + +const list: Record = { + "58008": { + description: "A special mode for accountants.", + canGetPb: false, + difficultyLevel: 1, + properties: ["ignoresLanguage", "ignoresLayout", "noLetters"], + frontendForcedConfig: { + numbers: [false], + }, + frontendFunctions: [ + "getWord", + "punctuateWord", + "rememberSettings", + "handleChar", + ], + name: "58008", + alias: "numbers", + }, + mirror: { + name: "mirror", + description: "Everything is mirrored!", + properties: ["hasCssFile"], + canGetPb: true, + difficultyLevel: 3, + }, + upside_down: { + name: "upside_down", + description: "Everything is upside down!", + properties: ["hasCssFile"], + canGetPb: true, + difficultyLevel: 3, + }, + nausea: { + name: "nausea", + description: "I think I'm gonna be sick.", + canGetPb: true, + difficultyLevel: 2, + properties: ["hasCssFile"], + }, + round_round_baby: { + name: "round_round_baby", + description: + "...right round, like a record baby. Right, round round round.", + canGetPb: true, + difficultyLevel: 3, + properties: ["hasCssFile"], + }, + simon_says: { + name: "simon_says", + description: "Type what simon says.", + canGetPb: true, + difficultyLevel: 1, + properties: ["hasCssFile", "changesWordsVisibility", "usesLayout"], + frontendForcedConfig: { + highlightMode: ["letter", "off"], + }, + frontendFunctions: ["applyConfig", "rememberSettings"], + }, + + tts: { + canGetPb: true, + difficultyLevel: 1, + properties: ["hasCssFile", "changesWordsVisibility", "speaks"], + frontendForcedConfig: { + highlightMode: ["letter", "off"], + }, + frontendFunctions: ["applyConfig", "rememberSettings", "toggleScript"], + name: "tts", + description: "Listen closely.", + }, + choo_choo: { + canGetPb: true, + difficultyLevel: 2, + properties: ["hasCssFile", "noLigatures", "conflictsWithSymmetricChars"], + name: "choo_choo", + description: "All the letters are spinning!", + }, + arrows: { + description: "Play it on a pad!", + canGetPb: false, + difficultyLevel: 1, + properties: [ + "ignoresLanguage", + "ignoresLayout", + "nospace", + "noLetters", + "symmetricChars", + ], + frontendForcedConfig: { + punctuation: [false], + numbers: [false], + highlightMode: ["letter", "off"], + }, + frontendFunctions: [ + "getWord", + "rememberSettings", + "handleChar", + "isCharCorrect", + "preventDefaultEvent", + "getWordHtml", + ], + name: "arrows", + }, + rAnDoMcAsE: { + description: "I kInDa LiKe HoW iNeFfIcIeNt QwErTy Is.", + canGetPb: false, + difficultyLevel: 2, + properties: ["changesCapitalisation"], + frontendFunctions: ["alterText"], + name: "rAnDoMcAsE", + }, + capitals: { + description: "Capitalize Every Word.", + canGetPb: false, + difficultyLevel: 1, + properties: ["changesCapitalisation"], + frontendFunctions: ["alterText"], + name: "capitals", + }, + layoutfluid: { + description: + "Switch between layouts specified below proportionately to the length of the test.", + canGetPb: true, + difficultyLevel: 1, + properties: ["changesLayout", "noInfiniteDuration"], + frontendFunctions: [ + "applyConfig", + "rememberSettings", + "handleSpace", + "getResultContent", + "restart", + ], + name: "layoutfluid", + }, + earthquake: { + description: "Everybody get down! The words are shaking!", + canGetPb: true, + difficultyLevel: 1, + properties: ["hasCssFile", "noLigatures"], + name: "earthquake", + }, + space_balls: { + description: "In a galaxy far far away.", + canGetPb: true, + difficultyLevel: 0, + properties: ["hasCssFile"], + name: "space_balls", + }, + gibberish: { + description: "Anvbuefl dizzs eoos alsb?", + canGetPb: false, + difficultyLevel: 1, + properties: ["ignoresLanguage", "unspeakable"], + frontendFunctions: ["getWord"], + name: "gibberish", + }, + ascii: { + description: "Where was the ampersand again?. Only ASCII characters.", + canGetPb: false, + difficultyLevel: 1, + properties: ["ignoresLanguage", "noLetters", "unspeakable"], + frontendForcedConfig: { + punctuation: [false], + numbers: [false], + }, + frontendFunctions: ["getWord"], + name: "ascii", + }, + specials: { + description: "!@#$%^&*. Only special characters.", + canGetPb: false, + difficultyLevel: 1, + properties: ["ignoresLanguage", "noLetters", "unspeakable"], + frontendForcedConfig: { + punctuation: [false], + numbers: [false], + }, + frontendFunctions: ["getWord"], + name: "specials", + }, + plus_one: { + description: "Only one future word is visible.", + canGetPb: true, + difficultyLevel: 0, + properties: ["changesWordsVisibility", "toPush:2", "noInfiniteDuration"], + name: "plus_one", + }, + plus_zero: { + description: "React quickly! Only the current word is visible.", + canGetPb: true, + difficultyLevel: 1, + properties: ["changesWordsVisibility", "toPush:1", "noInfiniteDuration"], + name: "plus_zero", + }, + plus_two: { + description: "Only two future words are visible.", + canGetPb: true, + difficultyLevel: 0, + properties: ["changesWordsVisibility", "toPush:3", "noInfiniteDuration"], + name: "plus_two", + }, + plus_three: { + description: "Only three future words are visible.", + canGetPb: true, + difficultyLevel: 0, + properties: ["changesWordsVisibility", "toPush:4", "noInfiniteDuration"], + name: "plus_three", + }, + read_ahead_easy: { + description: "Only the current word is invisible.", + canGetPb: true, + difficultyLevel: 1, + properties: ["changesWordsVisibility", "hasCssFile"], + frontendForcedConfig: { + highlightMode: ["letter", "off"], + }, + frontendFunctions: ["rememberSettings", "handleKeydown"], + name: "read_ahead_easy", + }, + read_ahead: { + description: "Current and the next word are invisible!", + canGetPb: true, + difficultyLevel: 2, + properties: ["changesWordsVisibility", "hasCssFile"], + frontendForcedConfig: { + highlightMode: ["letter", "off"], + }, + frontendFunctions: ["rememberSettings", "handleKeydown"], + name: "read_ahead", + }, + read_ahead_hard: { + description: "Current and the next two words are invisible!", + canGetPb: true, + difficultyLevel: 3, + properties: ["changesWordsVisibility", "hasCssFile"], + frontendForcedConfig: { + highlightMode: ["letter", "off"], + }, + frontendFunctions: ["rememberSettings", "handleKeydown"], + name: "read_ahead_hard", + }, + memory: { + description: "Test your memory. Remember the words and type them blind.", + canGetPb: true, + difficultyLevel: 3, + properties: ["changesWordsVisibility", "noInfiniteDuration"], + frontendForcedConfig: { + mode: ["words", "quote", "custom"], + }, + frontendFunctions: ["applyConfig", "rememberSettings", "start", "restart"], + name: "memory", + }, + nospace: { + description: "Whoneedsspacesanyway?", + canGetPb: false, + difficultyLevel: 0, + properties: ["nospace"], + frontendForcedConfig: { + highlightMode: ["letter", "off"], + }, + frontendFunctions: ["rememberSettings"], + name: "nospace", + }, + poetry: { + description: "Practice typing some beautiful prose.", + canGetPb: false, + difficultyLevel: 0, + properties: ["noInfiniteDuration", "ignoresLanguage"], + frontendForcedConfig: { + punctuation: [false], + numbers: [false], + }, + frontendFunctions: ["pullSection"], + name: "poetry", + }, + wikipedia: { + description: "Practice typing wikipedia sections.", + canGetPb: false, + difficultyLevel: 0, + properties: ["noInfiniteDuration", "ignoresLanguage"], + frontendForcedConfig: { + punctuation: [false], + numbers: [false], + }, + frontendFunctions: ["pullSection"], + name: "wikipedia", + }, + weakspot: { + description: "Focus on slow and mistyped letters.", + canGetPb: false, + difficultyLevel: 0, + properties: ["changesWordsFrequency"], + frontendFunctions: ["getWord"], + name: "weakspot", + }, + pseudolang: { + description: "Nonsense words that look like the current language.", + canGetPb: false, + difficultyLevel: 0, + properties: ["unspeakable", "ignoresLanguage"], + frontendFunctions: ["withWords"], + name: "pseudolang", + }, + IPv4: { + alias: "network", + description: "For sysadmins.", + canGetPb: false, + difficultyLevel: 1, + properties: ["ignoresLanguage", "ignoresLayout", "noLetters"], + frontendForcedConfig: { + numbers: [false], + }, + frontendFunctions: ["getWord", "punctuateWord", "rememberSettings"], + name: "IPv4", + }, + IPv6: { + alias: "network", + description: "For sysadmins with a long beard.", + canGetPb: false, + difficultyLevel: 1, + properties: ["ignoresLanguage", "ignoresLayout", "noLetters"], + frontendForcedConfig: { + numbers: [false], + }, + frontendFunctions: ["getWord", "punctuateWord", "rememberSettings"], + name: "IPv6", + }, + binary: { + description: + "01000010 01100101 01100101 01110000 00100000 01100010 01101111 01101111 01110000 00101110", + canGetPb: false, + difficultyLevel: 1, + properties: ["ignoresLanguage", "ignoresLayout", "noLetters"], + frontendForcedConfig: { + numbers: [false], + punctuation: [false], + }, + frontendFunctions: ["getWord"], + name: "binary", + }, + hexadecimal: { + description: + "0x38 0x20 0x74 0x69 0x6D 0x65 0x73 0x20 0x6D 0x6F 0x72 0x65 0x20 0x62 0x6F 0x6F 0x70 0x21", + canGetPb: false, + difficultyLevel: 1, + properties: ["ignoresLanguage", "ignoresLayout", "noLetters"], + frontendForcedConfig: { + numbers: [false], + }, + frontendFunctions: ["getWord", "punctuateWord", "rememberSettings"], + name: "hexadecimal", + }, + zipf: { + description: + "Words are generated according to Zipf's law. (not all languages will produce Zipfy results, use with caution)", + canGetPb: false, + difficultyLevel: 0, + properties: ["changesWordsFrequency"], + frontendFunctions: ["getWordsFrequencyMode"], + name: "zipf", + }, + morse: { + description: "-.../././.--./ -.../---/---/.--./-.-.--/ ", + canGetPb: false, + difficultyLevel: 1, + properties: ["ignoresLanguage", "ignoresLayout", "noLetters", "nospace"], + frontendFunctions: ["alterText"], + name: "morse", + }, + crt: { + description: "Go back to the 1980s", + canGetPb: true, + difficultyLevel: 0, + properties: ["hasCssFile", "noLigatures"], + frontendFunctions: ["applyGlobalCSS", "clearGlobal"], + name: "crt", + }, + backwards: { + description: "...sdrawkcab epyt ot yrt woN", + name: "backwards", + properties: [ + "hasCssFile", + "noLigatures", + "conflictsWithSymmetricChars", + "wordOrder:reverse", + ], + canGetPb: true, + frontendFunctions: ["alterText"], + difficultyLevel: 3, + }, + ddoouubblleedd: { + description: "TTyyppee eevveerryytthhiinngg ttwwiiccee..", + canGetPb: true, + difficultyLevel: 1, + properties: ["noLigatures"], + frontendFunctions: ["alterText"], + name: "ddoouubblleedd", + }, + instant_messaging: { + description: "Who needs shift anyway?", + canGetPb: false, + difficultyLevel: 1, + properties: ["changesCapitalisation"], + frontendFunctions: ["alterText"], + name: "instant_messaging", + }, +}; + +export function getFunbox(name: FunboxName): FunboxMetadata; +export function getFunbox(names: FunboxName[]): FunboxMetadata[]; +export function getFunbox( + nameOrNames: FunboxName | FunboxName[] +): FunboxMetadata | FunboxMetadata[] { + if (Array.isArray(nameOrNames)) { + const out = nameOrNames.map((name) => getObject()[name]); + + //@ts-expect-error + if (out.includes(undefined)) { + throw new Error("One of the funboxes is invalid: " + nameOrNames); + } + + return out; + } else { + const out = getObject()[nameOrNames]; + + if (out === undefined) { + throw new Error("Invalid funbox name: " + nameOrNames); + } + + return out; + } +} + +export function getObject(): Record { + return list; +} + +export function getList(): FunboxMetadata[] { + const out: FunboxMetadata[] = []; + for (const name of getFunboxNames()) { + out.push(list[name]); + } + return out; +} + +function getFunboxNames(): FunboxName[] { + return Object.keys(list) as FunboxName[]; +} diff --git a/packages/funbox/src/types.ts b/packages/funbox/src/types.ts new file mode 100644 index 000000000..8a19d9117 --- /dev/null +++ b/packages/funbox/src/types.ts @@ -0,0 +1,74 @@ +export type FunboxName = + | "58008" + | "mirror" + | "upside_down" + | "nausea" + | "round_round_baby" + | "simon_says" + | "tts" + | "choo_choo" + | "arrows" + | "rAnDoMcAsE" + | "capitals" + | "layoutfluid" + | "earthquake" + | "space_balls" + | "gibberish" + | "ascii" + | "specials" + | "plus_one" + | "plus_zero" + | "plus_two" + | "plus_three" + | "read_ahead_easy" + | "read_ahead" + | "read_ahead_hard" + | "memory" + | "nospace" + | "poetry" + | "wikipedia" + | "weakspot" + | "pseudolang" + | "IPv4" + | "IPv6" + | "binary" + | "hexadecimal" + | "zipf" + | "morse" + | "crt" + | "backwards" + | "ddoouubblleedd" + | "instant_messaging"; + +export type FunboxForcedConfig = Record; + +export type FunboxProperty = + | "hasCssFile" + | "ignoresLanguage" + | "ignoresLayout" + | "noLetters" + | "changesLayout" + | "usesLayout" + | "nospace" + | "changesWordsVisibility" + | "changesWordsFrequency" + | "changesCapitalisation" + | "conflictsWithSymmetricChars" + | "symmetricChars" + | "speaks" + | "unspeakable" + | "noInfiniteDuration" + | "noLigatures" + | `toPush:${number}` + | "wordOrder:reverse"; + +export type FunboxMetadata = { + name: FunboxName; + alias?: string; + description: string; + properties?: FunboxProperty[]; + frontendForcedConfig?: FunboxForcedConfig; + frontendFunctions?: string[]; + difficultyLevel: number; + canGetPb: boolean; +}; diff --git a/packages/funbox/src/util.ts b/packages/funbox/src/util.ts new file mode 100644 index 000000000..3b7a62a53 --- /dev/null +++ b/packages/funbox/src/util.ts @@ -0,0 +1,17 @@ +import { getList } from "./list"; +import { FunboxName } from "./types"; + +export function stringToFunboxNames(names: string): FunboxName[] { + if (names === "none" || names === "") return []; + const unsafeNames = names.split("#").map((name) => name.trim()); + const out: FunboxName[] = []; + const list = getList().map((f) => f.name); + for (const unsafeName of unsafeNames) { + if (list.includes(unsafeName as FunboxName)) { + out.push(unsafeName as FunboxName); + } else { + throw new Error("Invalid funbox name: " + unsafeName); + } + } + return out; +} diff --git a/packages/funbox/src/validation.ts b/packages/funbox/src/validation.ts new file mode 100644 index 000000000..70cdaa5df --- /dev/null +++ b/packages/funbox/src/validation.ts @@ -0,0 +1,154 @@ +import { intersect } from "@monkeytype/util/arrays"; +import { FunboxForcedConfig, FunboxName } from "./types"; +import { getFunbox } from "./list"; + +export function checkCompatibility( + funboxNames: FunboxName[], + withFunbox?: FunboxName +): boolean { + if (withFunbox === undefined || funboxNames.length === 0) return true; + let funboxesToCheck = getFunbox(funboxNames); + if (withFunbox !== undefined) { + funboxesToCheck = funboxesToCheck.concat(getFunbox(withFunbox)); + } + + const allFunboxesAreValid = getFunbox(funboxNames).every( + (f) => f !== undefined + ); + + const oneWordModifierMax = + funboxesToCheck.filter( + (f) => + f.frontendFunctions?.includes("getWord") ?? + f.frontendFunctions?.includes("pullSection") ?? + f.frontendFunctions?.includes("withWords") + ).length <= 1; + const oneWordOrderMax = + funboxesToCheck.filter((f) => + f.properties?.find((fp) => fp.startsWith("wordOrder")) + ).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.frontendFunctions?.includes("pullSection") + ).length <= 1; + const oneCssFileMax = + funboxesToCheck.filter((f) => + f.properties?.find((fp) => fp === "hasCssFile") + ).length <= 1; + const onePunctuateWordMax = + funboxesToCheck.filter((f) => + f.frontendFunctions?.includes("punctuateWord") + ).length <= 1; + const oneCharCheckerMax = + funboxesToCheck.filter((f) => + f.frontendFunctions?.includes("isCharCorrect") + ).length <= 1; + const oneCharReplacerMax = + funboxesToCheck.filter((f) => f.frontendFunctions?.includes("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.frontendForcedConfig) continue; + for (const key in f.frontendForcedConfig) { + if (allowedConfig[key]) { + if ( + intersect( + allowedConfig[key], + f.frontendForcedConfig[key] as string[] | boolean[], + true + ).length === 0 + ) { + noConfigConflicts = false; + break; + } + } else { + allowedConfig[key] = f.frontendForcedConfig[key] as + | string[] + | boolean[]; + } + } + } + + return ( + allFunboxesAreValid && + oneWordModifierMax && + layoutUsability && + oneNospaceOrToPushMax && + oneChangesWordsVisibilityMax && + oneFrequencyChangesMax && + noFrequencyChangesConflicts && + capitalisationChangePosibility && + noConflictsWithSymmetricChars && + oneCanSpeakMax && + hasLanguageToSpeakAndNoUnspeakable && + oneToPushOrPullSectionMax && + oneCssFileMax && + onePunctuateWordMax && + oneCharCheckerMax && + oneCharReplacerMax && + oneChangesCapitalisationMax && + noConfigConflicts && + oneWordOrderMax + ); +} diff --git a/packages/funbox/tsconfig.json b/packages/funbox/tsconfig.json new file mode 100644 index 000000000..de674c340 --- /dev/null +++ b/packages/funbox/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "@monkeytype/typescript-config/base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "declarationMap": true, + "moduleResolution": "Bundler", + "module": "ES6", + "target": "ES2015", + "lib": ["es2016"] + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/funbox/vitest.config.js b/packages/funbox/vitest.config.js new file mode 100644 index 000000000..d071c79ce --- /dev/null +++ b/packages/funbox/vitest.config.js @@ -0,0 +1,11 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + environment: "node", + coverage: { + include: ["**/*.ts"], + }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc0e56319..852d0600c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: '@monkeytype/contracts': specifier: workspace:* version: link:../packages/contracts + '@monkeytype/funbox': + specifier: workspace:* + version: link:../packages/funbox '@monkeytype/util': specifier: workspace:* version: link:../packages/util @@ -267,6 +270,9 @@ importers: '@monkeytype/contracts': specifier: workspace:* version: link:../packages/contracts + '@monkeytype/funbox': + specifier: workspace:* + version: link:../packages/funbox '@monkeytype/util': specifier: workspace:* version: link:../packages/util @@ -522,6 +528,39 @@ importers: specifier: 1.1.9 version: 1.1.9 + packages/funbox: + devDependencies: + '@monkeytype/esbuild': + specifier: workspace:* + version: link:../esbuild-config + '@monkeytype/eslint-config': + specifier: workspace:* + version: link:../eslint-config + '@monkeytype/typescript-config': + specifier: workspace:* + version: link:../typescript-config + '@monkeytype/util': + specifier: workspace:* + version: link:../util + chokidar: + specifier: 3.6.0 + version: 3.6.0 + eslint: + specifier: 8.57.0 + version: 8.57.0 + madge: + specifier: 8.0.0 + version: 8.0.0(typescript@5.5.4) + rimraf: + specifier: 6.0.1 + version: 6.0.1 + typescript: + specifier: 5.5.4 + version: 5.5.4 + vitest: + specifier: 2.0.5 + version: 2.0.5(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.31.3) + packages/release: dependencies: '@octokit/rest': @@ -1302,12 +1341,6 @@ packages: resolution: {integrity: sha512-9Z0sGuXqf6En19qmwB0Syi1Mc8TYl756dNuuaYal9mrypKa0Jq/IX6aJfh6Rk2S3z66KBisWTqloDo7weYj4zg==} engines: {node: '>=4'} - '@esbuild/aix-ppc64@0.19.12': - resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - '@esbuild/aix-ppc64@0.20.2': resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} engines: {node: '>=12'} @@ -1326,12 +1359,6 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.19.12': - resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm64@0.20.2': resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} engines: {node: '>=12'} @@ -1350,12 +1377,6 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm@0.19.12': - resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - '@esbuild/android-arm@0.20.2': resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} engines: {node: '>=12'} @@ -1374,12 +1395,6 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-x64@0.19.12': - resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - '@esbuild/android-x64@0.20.2': resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} engines: {node: '>=12'} @@ -1398,12 +1413,6 @@ packages: cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.19.12': - resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-arm64@0.20.2': resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} engines: {node: '>=12'} @@ -1422,12 +1431,6 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.19.12': - resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - '@esbuild/darwin-x64@0.20.2': resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} engines: {node: '>=12'} @@ -1446,12 +1449,6 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.19.12': - resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-arm64@0.20.2': resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} engines: {node: '>=12'} @@ -1470,12 +1467,6 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.19.12': - resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - '@esbuild/freebsd-x64@0.20.2': resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} engines: {node: '>=12'} @@ -1494,12 +1485,6 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.19.12': - resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm64@0.20.2': resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} engines: {node: '>=12'} @@ -1518,12 +1503,6 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.19.12': - resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - '@esbuild/linux-arm@0.20.2': resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} engines: {node: '>=12'} @@ -1542,12 +1521,6 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.19.12': - resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-ia32@0.20.2': resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} engines: {node: '>=12'} @@ -1566,12 +1539,6 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.19.12': - resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-loong64@0.20.2': resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} engines: {node: '>=12'} @@ -1590,12 +1557,6 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.19.12': - resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-mips64el@0.20.2': resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} engines: {node: '>=12'} @@ -1614,12 +1575,6 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.19.12': - resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-ppc64@0.20.2': resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} engines: {node: '>=12'} @@ -1638,12 +1593,6 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.19.12': - resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-riscv64@0.20.2': resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} engines: {node: '>=12'} @@ -1662,12 +1611,6 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.19.12': - resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-s390x@0.20.2': resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} engines: {node: '>=12'} @@ -1686,12 +1629,6 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.19.12': - resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - '@esbuild/linux-x64@0.20.2': resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} engines: {node: '>=12'} @@ -1710,12 +1647,6 @@ packages: cpu: [x64] os: [linux] - '@esbuild/netbsd-x64@0.19.12': - resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - '@esbuild/netbsd-x64@0.20.2': resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} engines: {node: '>=12'} @@ -1740,12 +1671,6 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.19.12': - resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - '@esbuild/openbsd-x64@0.20.2': resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} engines: {node: '>=12'} @@ -1764,12 +1689,6 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/sunos-x64@0.19.12': - resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - '@esbuild/sunos-x64@0.20.2': resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} engines: {node: '>=12'} @@ -1788,12 +1707,6 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.19.12': - resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-arm64@0.20.2': resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} engines: {node: '>=12'} @@ -1812,12 +1725,6 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.19.12': - resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-ia32@0.20.2': resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} engines: {node: '>=12'} @@ -1836,12 +1743,6 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.19.12': - resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - '@esbuild/win32-x64@0.20.2': resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} engines: {node: '>=12'} @@ -4545,11 +4446,6 @@ packages: es6-weak-map@2.0.3: resolution: {integrity: sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==} - esbuild@0.19.12: - resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} - engines: {node: '>=12'} - hasBin: true - esbuild@0.20.2: resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} engines: {node: '>=12'} @@ -9286,34 +9182,6 @@ packages: '@vite-pwa/assets-generator': optional: true - vite@5.1.7: - resolution: {integrity: sha512-sgnEEFTZYMui/sTlH1/XEnVNHMujOahPLGMxn1+5sIT45Xjng1Ec1K78jRP15dSmVgg5WBin9yO81j3o9OxofA==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - vite@5.2.14: resolution: {integrity: sha512-TFQLuwWLPms+NBNlh0D9LZQ+HXW471COABxw/9TEUBrjuHMo9BrYBPrN/SYAwIuVL+rLerycxiLT41t4f5MZpA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -10665,9 +10533,6 @@ snapshots: to-pascal-case: 1.0.0 unescape-js: 1.1.4 - '@esbuild/aix-ppc64@0.19.12': - optional: true - '@esbuild/aix-ppc64@0.20.2': optional: true @@ -10677,9 +10542,6 @@ snapshots: '@esbuild/aix-ppc64@0.23.0': optional: true - '@esbuild/android-arm64@0.19.12': - optional: true - '@esbuild/android-arm64@0.20.2': optional: true @@ -10689,9 +10551,6 @@ snapshots: '@esbuild/android-arm64@0.23.0': optional: true - '@esbuild/android-arm@0.19.12': - optional: true - '@esbuild/android-arm@0.20.2': optional: true @@ -10701,9 +10560,6 @@ snapshots: '@esbuild/android-arm@0.23.0': optional: true - '@esbuild/android-x64@0.19.12': - optional: true - '@esbuild/android-x64@0.20.2': optional: true @@ -10713,9 +10569,6 @@ snapshots: '@esbuild/android-x64@0.23.0': optional: true - '@esbuild/darwin-arm64@0.19.12': - optional: true - '@esbuild/darwin-arm64@0.20.2': optional: true @@ -10725,9 +10578,6 @@ snapshots: '@esbuild/darwin-arm64@0.23.0': optional: true - '@esbuild/darwin-x64@0.19.12': - optional: true - '@esbuild/darwin-x64@0.20.2': optional: true @@ -10737,9 +10587,6 @@ snapshots: '@esbuild/darwin-x64@0.23.0': optional: true - '@esbuild/freebsd-arm64@0.19.12': - optional: true - '@esbuild/freebsd-arm64@0.20.2': optional: true @@ -10749,9 +10596,6 @@ snapshots: '@esbuild/freebsd-arm64@0.23.0': optional: true - '@esbuild/freebsd-x64@0.19.12': - optional: true - '@esbuild/freebsd-x64@0.20.2': optional: true @@ -10761,9 +10605,6 @@ snapshots: '@esbuild/freebsd-x64@0.23.0': optional: true - '@esbuild/linux-arm64@0.19.12': - optional: true - '@esbuild/linux-arm64@0.20.2': optional: true @@ -10773,9 +10614,6 @@ snapshots: '@esbuild/linux-arm64@0.23.0': optional: true - '@esbuild/linux-arm@0.19.12': - optional: true - '@esbuild/linux-arm@0.20.2': optional: true @@ -10785,9 +10623,6 @@ snapshots: '@esbuild/linux-arm@0.23.0': optional: true - '@esbuild/linux-ia32@0.19.12': - optional: true - '@esbuild/linux-ia32@0.20.2': optional: true @@ -10797,9 +10632,6 @@ snapshots: '@esbuild/linux-ia32@0.23.0': optional: true - '@esbuild/linux-loong64@0.19.12': - optional: true - '@esbuild/linux-loong64@0.20.2': optional: true @@ -10809,9 +10641,6 @@ snapshots: '@esbuild/linux-loong64@0.23.0': optional: true - '@esbuild/linux-mips64el@0.19.12': - optional: true - '@esbuild/linux-mips64el@0.20.2': optional: true @@ -10821,9 +10650,6 @@ snapshots: '@esbuild/linux-mips64el@0.23.0': optional: true - '@esbuild/linux-ppc64@0.19.12': - optional: true - '@esbuild/linux-ppc64@0.20.2': optional: true @@ -10833,9 +10659,6 @@ snapshots: '@esbuild/linux-ppc64@0.23.0': optional: true - '@esbuild/linux-riscv64@0.19.12': - optional: true - '@esbuild/linux-riscv64@0.20.2': optional: true @@ -10845,9 +10668,6 @@ snapshots: '@esbuild/linux-riscv64@0.23.0': optional: true - '@esbuild/linux-s390x@0.19.12': - optional: true - '@esbuild/linux-s390x@0.20.2': optional: true @@ -10857,9 +10677,6 @@ snapshots: '@esbuild/linux-s390x@0.23.0': optional: true - '@esbuild/linux-x64@0.19.12': - optional: true - '@esbuild/linux-x64@0.20.2': optional: true @@ -10869,9 +10686,6 @@ snapshots: '@esbuild/linux-x64@0.23.0': optional: true - '@esbuild/netbsd-x64@0.19.12': - optional: true - '@esbuild/netbsd-x64@0.20.2': optional: true @@ -10884,9 +10698,6 @@ snapshots: '@esbuild/openbsd-arm64@0.23.0': optional: true - '@esbuild/openbsd-x64@0.19.12': - optional: true - '@esbuild/openbsd-x64@0.20.2': optional: true @@ -10896,9 +10707,6 @@ snapshots: '@esbuild/openbsd-x64@0.23.0': optional: true - '@esbuild/sunos-x64@0.19.12': - optional: true - '@esbuild/sunos-x64@0.20.2': optional: true @@ -10908,9 +10716,6 @@ snapshots: '@esbuild/sunos-x64@0.23.0': optional: true - '@esbuild/win32-arm64@0.19.12': - optional: true - '@esbuild/win32-arm64@0.20.2': optional: true @@ -10920,9 +10725,6 @@ snapshots: '@esbuild/win32-arm64@0.23.0': optional: true - '@esbuild/win32-ia32@0.19.12': - optional: true - '@esbuild/win32-ia32@0.20.2': optional: true @@ -10932,9 +10734,6 @@ snapshots: '@esbuild/win32-ia32@0.23.0': optional: true - '@esbuild/win32-x64@0.19.12': - optional: true - '@esbuild/win32-x64@0.20.2': optional: true @@ -14185,32 +13984,6 @@ snapshots: es6-iterator: 2.0.3 es6-symbol: 3.1.4 - esbuild@0.19.12: - optionalDependencies: - '@esbuild/aix-ppc64': 0.19.12 - '@esbuild/android-arm': 0.19.12 - '@esbuild/android-arm64': 0.19.12 - '@esbuild/android-x64': 0.19.12 - '@esbuild/darwin-arm64': 0.19.12 - '@esbuild/darwin-x64': 0.19.12 - '@esbuild/freebsd-arm64': 0.19.12 - '@esbuild/freebsd-x64': 0.19.12 - '@esbuild/linux-arm': 0.19.12 - '@esbuild/linux-arm64': 0.19.12 - '@esbuild/linux-ia32': 0.19.12 - '@esbuild/linux-loong64': 0.19.12 - '@esbuild/linux-mips64el': 0.19.12 - '@esbuild/linux-ppc64': 0.19.12 - '@esbuild/linux-riscv64': 0.19.12 - '@esbuild/linux-s390x': 0.19.12 - '@esbuild/linux-x64': 0.19.12 - '@esbuild/netbsd-x64': 0.19.12 - '@esbuild/openbsd-x64': 0.19.12 - '@esbuild/sunos-x64': 0.19.12 - '@esbuild/win32-arm64': 0.19.12 - '@esbuild/win32-ia32': 0.19.12 - '@esbuild/win32-x64': 0.19.12 - esbuild@0.20.2: optionalDependencies: '@esbuild/aix-ppc64': 0.20.2 @@ -20067,28 +19840,6 @@ snapshots: transitivePeerDependencies: - supports-color - vite@5.1.7(@types/node@20.14.11)(sass@1.70.0)(terser@5.31.3): - dependencies: - esbuild: 0.19.12 - postcss: 8.4.40 - rollup: 4.19.1 - optionalDependencies: - '@types/node': 20.14.11 - fsevents: 2.3.3 - sass: 1.70.0 - terser: 5.31.3 - - vite@5.1.7(@types/node@20.5.1)(sass@1.70.0)(terser@5.31.3): - dependencies: - esbuild: 0.19.12 - postcss: 8.4.40 - rollup: 4.19.1 - optionalDependencies: - '@types/node': 20.5.1 - fsevents: 2.3.3 - sass: 1.70.0 - terser: 5.31.3 - vite@5.2.14(@types/node@20.14.11)(sass@1.70.0)(terser@5.31.3): dependencies: esbuild: 0.20.2 @@ -20141,7 +19892,7 @@ snapshots: tinybench: 2.8.0 tinypool: 1.0.0 tinyrainbow: 1.2.0 - vite: 5.1.7(@types/node@20.14.11)(sass@1.70.0)(terser@5.31.3) + vite: 5.2.14(@types/node@20.14.11)(sass@1.70.0)(terser@5.31.3) vite-node: 2.0.5(@types/node@20.14.11)(sass@1.70.0)(terser@5.31.3) why-is-node-running: 2.3.0 optionalDependencies: @@ -20174,7 +19925,7 @@ snapshots: tinybench: 2.8.0 tinypool: 1.0.0 tinyrainbow: 1.2.0 - vite: 5.1.7(@types/node@20.5.1)(sass@1.70.0)(terser@5.31.3) + vite: 5.2.14(@types/node@20.5.1)(sass@1.70.0)(terser@5.31.3) vite-node: 2.0.5(@types/node@20.5.1)(sass@1.70.0)(terser@5.31.3) why-is-node-running: 2.3.0 optionalDependencies: -- cgit v1.2.3