aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJack <[email protected]>2024-12-04 16:11:07 +0100
committerGitHub <[email protected]>2024-12-04 16:11:07 +0100
commitfdadb4ae83cfa16b2d8f8666d265b705b71071e7 (patch)
treebc910de0fe58c9d338d910feb71097aee83c6e06
parenta75f0d3b306134e48e45463b14157b40eb451882 (diff)
downloadmonkeytype-fdadb4ae83cfa16b2d8f8666d265b705b71071e7.tar.gz
monkeytype-fdadb4ae83cfa16b2d8f8666d265b705b71071e7.zip
refactor: move funboxes to a shared package (@miodec) (#6063)
-rw-r--r--backend/package.json1
-rw-r--r--backend/src/api/controllers/result.ts29
-rw-r--r--backend/src/utils/pb.ts23
-rw-r--r--backend/src/utils/validation.ts137
-rw-r--r--frontend/__tests__/root/config.spec.ts6
-rw-r--r--frontend/__tests__/test/funbox.spec.ts24
-rw-r--r--frontend/__tests__/tsconfig.json2
-rw-r--r--frontend/package.json1
-rw-r--r--frontend/scripts/json-validation.cjs28
-rw-r--r--frontend/src/ts/commandline/lists.ts20
-rw-r--r--frontend/src/ts/commandline/lists/funbox.ts93
-rw-r--r--frontend/src/ts/controllers/account-controller.ts3
-rw-r--r--frontend/src/ts/controllers/input-controller.ts69
-rw-r--r--frontend/src/ts/db.ts8
-rw-r--r--frontend/src/ts/elements/account/result-filters.ts82
-rw-r--r--frontend/src/ts/pages/settings.ts110
-rw-r--r--frontend/src/ts/ready.ts9
-rw-r--r--frontend/src/ts/test/funbox/funbox-functions.ts627
-rw-r--r--frontend/src/ts/test/funbox/funbox-list.ts302
-rw-r--r--frontend/src/ts/test/funbox/funbox-validation.ts210
-rw-r--r--frontend/src/ts/test/funbox/funbox.ts657
-rw-r--r--frontend/src/ts/test/funbox/list.ts72
-rw-r--r--frontend/src/ts/test/funbox/memory-funbox-timer.ts6
-rw-r--r--frontend/src/ts/test/pace-caret.ts3
-rw-r--r--frontend/src/ts/test/result.ts25
-rw-r--r--frontend/src/ts/test/test-logic.ts18
-rw-r--r--frontend/src/ts/test/test-stats.ts6
-rw-r--r--frontend/src/ts/test/test-ui.ts23
-rw-r--r--frontend/src/ts/test/words-generator.ts58
-rw-r--r--frontend/src/ts/utils/json-data.ts98
-rw-r--r--frontend/static/funbox/_list.json204
-rw-r--r--frontend/vitest.config.js7
-rw-r--r--packages/funbox/.eslintrc.cjs5
-rw-r--r--packages/funbox/__test__/tsconfig.json12
-rw-r--r--packages/funbox/package.json30
-rw-r--r--packages/funbox/src/index.ts19
-rw-r--r--packages/funbox/src/list.ts (renamed from backend/src/constants/funbox-list.ts)257
-rw-r--r--packages/funbox/src/types.ts74
-rw-r--r--packages/funbox/src/util.ts17
-rw-r--r--packages/funbox/src/validation.ts154
-rw-r--r--packages/funbox/tsconfig.json15
-rw-r--r--packages/funbox/vitest.config.js11
-rw-r--r--pnpm-lock.yaml331
43 files changed, 1605 insertions, 2281 deletions
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/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<number, Record<string, PersonalBest>>;
@@ -21,20 +20,16 @@ type CheckAndUpdatePbResult = {
type Result = Omit<ResultType<Mode>, "_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<string, string[] | boolean[]>;
- 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<string | boolean>(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<CommandsSubgroup> {
await Promise.allSettled([
layoutsPromise,
languagesPromise,
- funboxPromise,
fontsPromise,
themesPromise,
challengesPromise,
diff --git a/frontend/src/ts/commandline/lists/funbox.ts b/frontend/src/ts/commandline/lists/funbox.ts
index 704d0d4d3..f8c984bc3 100644
--- a/frontend/src/ts/commandline/lists/funbox.ts
+++ b/frontend/src/ts/commandline/lists/funbox.ts
@@ -1,42 +1,12 @@
import * as Funbox from "../../test/funbox/funbox";
import * as TestLogic from "../../test/test-logic";
import * as ManualRestart from "../../test/manual-restart-tracker";
-import Config from "../../config";
-import { areFunboxesCompatible } from "../../test/funbox/funbox-validation";
-import { FunboxMetadata } from "../../utils/json-data";
+import { getAllFunboxes, checkCompatibility } from "@monkeytype/funbox";
import { Command, CommandsSubgroup } from "../types";
+import { getActiveFunboxNames } from "../../test/funbox/list";
-const subgroup: CommandsSubgroup = {
- title: "Funbox...",
- configKey: "funbox",
- list: [
- {
- id: "changeFunboxNone",
- display: "none",
- configValue: "none",
- alias: "off",
- exec: (): void => {
- if (Funbox.setFunbox("none")) {
- TestLogic.restart();
- }
- },
- },
- ],
-};
-
-const commands: Command[] = [
+const list: Command[] = [
{
- id: "changeFunbox",
- display: "Funbox...",
- alias: "fun box",
- icon: "fa-gamepad",
- subgroup,
- },
-];
-
-function update(funboxes: FunboxMetadata[]): void {
- subgroup.list = [];
- subgroup.list.push({
id: "changeFunboxNone",
display: "none",
configValue: "none",
@@ -48,27 +18,44 @@ function update(funboxes: FunboxMetadata[]): void {
TestLogic.restart();
}
},
+ },
+];
+
+for (const funbox of getAllFunboxes()) {
+ list.push({
+ id: "changeFunbox" + funbox.name,
+ display: funbox.name.replace(/_/g, " "),
+ available: () => {
+ const activeNames = getActiveFunboxNames();
+ if (activeNames.includes(funbox.name)) return true;
+ return checkCompatibility(activeNames, funbox.name);
+ },
+ sticky: true,
+ alias: funbox.alias,
+ configValue: funbox.name,
+ configValueMode: "include",
+ exec: (): void => {
+ Funbox.toggleFunbox(funbox.name);
+ ManualRestart.set();
+ TestLogic.restart();
+ },
});
- for (const funbox of funboxes) {
- subgroup.list.push({
- id: "changeFunbox" + funbox.name,
- display: funbox.name.replace(/_/g, " "),
- available: () => {
- if (Config.funbox.split("#").includes(funbox.name)) return true;
- return areFunboxesCompatible(Config.funbox, funbox.name);
- },
- sticky: true,
- alias: funbox.alias,
- configValue: funbox.name,
- configValueMode: "include",
- exec: (): void => {
- Funbox.toggleFunbox(funbox.name);
- ManualRestart.set();
- TestLogic.restart();
- },
- });
- }
}
+const subgroup: CommandsSubgroup = {
+ title: "Funbox...",
+ configKey: "funbox",
+ list,
+};
+
+const commands: Command[] = [
+ {
+ id: "changeFunbox",
+ display: "Funbox...",
+ alias: "fun box",
+ icon: "fa-gamepad",
+ subgroup,
+ },
+];
+
export default commands;
-export { update };
diff --git a/frontend/src/ts/controllers/account-controller.ts b/frontend/src/ts/controllers/account-controller.ts
index 78291adf4..70191802c 100644
--- a/frontend/src/ts/controllers/account-controller.ts
+++ b/frontend/src/ts/controllers/account-controller.ts
@@ -20,6 +20,7 @@ import * as URLHandler from "../utils/url-handler";
import * as Account from "../pages/account";
import * as Alerts from "../elements/alerts";
import * as AccountSettings from "../pages/account-settings";
+import { getAllFunboxes } from "@monkeytype/funbox";
import {
GoogleAuthProvider,
GithubAuthProvider,
@@ -129,7 +130,7 @@ async function getDataAndInit(): Promise<boolean> {
ResultFilters.loadTags(snapshot.tags);
- Promise.all([JSONData.getLanguageList(), JSONData.getFunboxList()])
+ Promise.all([JSONData.getLanguageList(), getAllFunboxes()])
.then((values) => {
const [languages, funboxes] = values;
languages.forEach((language) => {
diff --git a/frontend/src/ts/controllers/input-controller.ts b/frontend/src/ts/controllers/input-controller.ts
index d28b4281a..dcb263ca9 100644
--- a/frontend/src/ts/controllers/input-controller.ts
+++ b/frontend/src/ts/controllers/input-controller.ts
@@ -29,13 +29,13 @@ import * as TestInput from "../test/test-input";
import * as TestWords from "../test/test-words";
import * as Hangul from "hangul-js";
import * as CustomTextState from "../states/custom-text-name";
-import * as FunboxList from "../test/funbox/funbox-list";
import * as KeymapEvent from "../observables/keymap-event";
import { IgnoredKeys } from "../constants/ignored-keys";
import { ModifierKeys } from "../constants/modifier-keys";
import { navigate } from "./route-controller";
import * as Loader from "../elements/loader";
import * as KeyConverter from "../utils/key-converter";
+import { getActiveFunboxes } from "../test/funbox/list";
let dontInsertSpace = false;
let correctShiftUsed = true;
@@ -145,9 +145,7 @@ function backspaceToPrevious(): void {
TestInput.input.current = TestInput.input.popHistory();
TestInput.corrected.popHistory();
- if (
- FunboxList.get(Config.funbox).find((f) => f.properties?.includes("nospace"))
- ) {
+ if (getActiveFunboxes().find((f) => f.properties?.includes("nospace"))) {
TestInput.input.current = TestInput.input.current.slice(0, -1);
setWordsInput(" " + TestInput.input.current + " ");
}
@@ -196,10 +194,8 @@ async function handleSpace(): Promise<void> {
const currentWord: string = TestWords.words.getCurrent();
- for (const f of FunboxList.get(Config.funbox)) {
- if (f.functions?.handleSpace) {
- f.functions.handleSpace();
- }
+ for (const fb of getActiveFunboxes()) {
+ fb.functions?.handleSpace?.();
}
dontInsertSpace = true;
@@ -209,9 +205,8 @@ async function handleSpace(): Promise<void> {
TestInput.pushBurstToHistory(burst);
const nospace =
- FunboxList.get(Config.funbox).find((f) =>
- f.properties?.includes("nospace")
- ) !== undefined;
+ getActiveFunboxes().find((f) => f.properties?.includes("nospace")) !==
+ undefined;
//correct word or in zen mode
const isWordCorrect: boolean =
@@ -411,9 +406,7 @@ function isCharCorrect(char: string, charIndex: number): boolean {
return true;
}
- const funbox = FunboxList.get(Config.funbox).find(
- (f) => f.functions?.isCharCorrect
- );
+ const funbox = getActiveFunboxes().find((fb) => fb.functions?.isCharCorrect);
if (funbox?.functions?.isCharCorrect) {
return funbox.functions.isCharCorrect(char, originalChar);
}
@@ -497,14 +490,15 @@ function handleChar(
const isCharKorean: boolean = TestInput.input.getKoreanStatus();
- for (const f of FunboxList.get(Config.funbox)) {
- if (f.functions?.handleChar) char = f.functions.handleChar(char);
+ for (const fb of getActiveFunboxes()) {
+ if (fb.functions?.handleChar) {
+ char = fb.functions.handleChar(char);
+ }
}
const nospace =
- FunboxList.get(Config.funbox).find((f) =>
- f.properties?.includes("nospace")
- ) !== undefined;
+ getActiveFunboxes().find((f) => f.properties?.includes("nospace")) !==
+ undefined;
if (char !== "\n" && char !== "\t" && /\s/.test(char)) {
if (nospace) return;
@@ -908,11 +902,11 @@ $(document).on("keydown", async (event) => {
return;
}
- FunboxList.get(Config.funbox).forEach((value) => {
- if (value.functions?.handleKeydown) {
- void value.functions?.handleKeydown(event);
+ for (const fb of getActiveFunboxes()) {
+ if (fb.functions?.handleKeydown) {
+ void fb.functions.handleKeydown(event);
}
- });
+ }
//autofocus
const wordsFocused: boolean = $("#wordsInput").is(":focus");
@@ -1161,21 +1155,20 @@ $(document).on("keydown", async (event) => {
}
}
- const funbox = FunboxList.get(Config.funbox).find(
- (f) => f.functions?.preventDefaultEvent
- );
- if (funbox?.functions?.preventDefaultEvent) {
- if (
- await funbox.functions.preventDefaultEvent(
- //i cant figure this type out, but it works fine
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
- event as JQuery.KeyDownEvent
- )
- ) {
- event.preventDefault();
- handleChar(event.key, TestInput.input.current.length);
- updateUI();
- setWordsInput(" " + TestInput.input.current);
+ for (const fb of getActiveFunboxes()) {
+ if (fb.functions?.preventDefaultEvent) {
+ if (
+ await fb.functions.preventDefaultEvent(
+ //i cant figure this type out, but it works fine
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
+ event as JQuery.KeyDownEvent
+ )
+ ) {
+ event.preventDefault();
+ handleChar(event.key, TestInput.input.current.length);
+ updateUI();
+ setWordsInput(" " + TestInput.input.current);
+ }
}
}
diff --git a/frontend/src/ts/db.ts b/frontend/src/ts/db.ts
index bc5a619b7..ff437e07f 100644
--- a/frontend/src/ts/db.ts
+++ b/frontend/src/ts/db.ts
@@ -5,7 +5,6 @@ import DefaultConfig from "./constants/default-config";
import { isAuthenticated } from "./firebase";
import * as ConnectionState from "./states/connection";
import { lastElementFromArray } from "./utils/arrays";
-import { getFunboxList } from "./utils/json-data";
import { migrateConfig } from "./utils/config";
import * as Dates from "date-fns";
import {
@@ -32,6 +31,7 @@ import {
import { Preset } from "@monkeytype/contracts/schemas/presets";
import defaultSnapshot from "./constants/default-snapshot";
import { Result } from "@monkeytype/contracts/schemas/results";
+import { FunboxMetadata } from "../../../packages/funbox/src/types";
export type SnapshotUserTag = UserTag & {
active?: boolean;
@@ -704,12 +704,8 @@ export async function getLocalPB<M extends Mode>(
language: string,
difficulty: Difficulty,
lazyMode: boolean,
- funbox: string
+ funboxes: FunboxMetadata[]
): Promise<PersonalBest | undefined> {
- const funboxes = (await getFunboxList()).filter((fb) => {
- return funbox?.split("#").includes(fb.name);
- });
-
if (!funboxes.every((f) => f.canGetPb)) {
return undefined;
}
diff --git a/frontend/src/ts/elements/account/result-filters.ts b/frontend/src/ts/elements/account/result-filters.ts
index d29c61a93..7c685a3a5 100644
--- a/frontend/src/ts/elements/account/result-filters.ts
+++ b/frontend/src/ts/elements/account/result-filters.ts
@@ -16,6 +16,7 @@ import {
} from "@monkeytype/contracts/schemas/users";
import { LocalStorageWithSchema } from "../../utils/local-storage-with-schema";
import defaultResultFilters from "../../constants/default-result-filters";
+import { getAllFunboxes } from "@monkeytype/funbox";
export function mergeWithDefaultFilters(
filters: Partial<ResultFilters>
@@ -801,61 +802,48 @@ export async function appendButtons(
}
}
- let funboxList;
- try {
- funboxList = await JSONData.getFunboxList();
- } catch (e) {
- console.error(
- Misc.createErrorMessage(e, "Failed to append funbox buttons")
- );
- }
- if (funboxList) {
- let html = "";
+ let html = "";
- html +=
- "<select class='funboxSelect' group='funbox' placeholder='select a funbox' multiple>";
+ html +=
+ "<select class='funboxSelect' group='funbox' placeholder='select a funbox' multiple>";
- html += "<option value='all'>all</option>";
- html += "<option value='none'>no funbox</option>";
+ html += "<option value='all'>all</option>";
+ html += "<option value='none'>no funbox</option>";
- for (const funbox of funboxList) {
- html += `<option value="${funbox.name}" filter="${
- funbox.name
- }">${funbox.name.replace(/_/g, " ")}</option>`;
- }
+ for (const funbox of getAllFunboxes()) {
+ html += `<option value="${funbox.name}" filter="${
+ funbox.name
+ }">${funbox.name.replace(/_/g, " ")}</option>`;
+ }
- html += "</select>";
+ html += "</select>";
- const el = document.querySelector(
- ".pageAccount .content .filterButtons .buttonsAndTitle.funbox .select"
- );
- if (el) {
- el.innerHTML = html;
- groupSelects["funbox"] = new SlimSelect({
- select: el.querySelector(".funboxSelect") as HTMLSelectElement,
- settings: {
- showSearch: true,
- placeholderText: "select a funbox",
- allowDeselect: true,
- closeOnSelect: false,
- },
- events: {
- beforeChange: (
+ const el = document.querySelector(
+ ".pageAccount .content .filterButtons .buttonsAndTitle.funbox .select"
+ );
+ if (el) {
+ el.innerHTML = html;
+ groupSelects["funbox"] = new SlimSelect({
+ select: el.querySelector(".funboxSelect") as HTMLSelectElement,
+ settings: {
+ showSearch: true,
+ placeholderText: "select a funbox",
+ allowDeselect: true,
+ closeOnSelect: false,
+ },
+ events: {
+ beforeChange: (selectedOptions, oldSelectedOptions): void | boolean => {
+ return selectBeforeChangeFn(
+ "funbox",
selectedOptions,
oldSelectedOptions
- ): void | boolean => {
- return selectBeforeChangeFn(
- "funbox",
- selectedOptions,
- oldSelectedOptions
- );
- },
- beforeOpen: (): void => {
- adjustScrollposition("funbox");
- },
+ );
},
- });
- }
+ beforeOpen: (): void => {
+ adjustScrollposition("funbox");
+ },
+ },
+ });
}
const snapshot = DB.getSnapshot();
diff --git a/frontend/src/ts/pages/settings.ts b/frontend/src/ts/pages/settings.ts
index 2581cfb73..5b7e507d6 100644
--- a/frontend/src/ts/pages/settings.ts
+++ b/frontend/src/ts/pages/settings.ts
@@ -5,7 +5,7 @@ import * as Misc from "../utils/misc";
import * as Strings from "../utils/strings";
import * as JSONData from "../utils/json-data";
import * as DB from "../db";
-import { toggleFunbox } from "../test/funbox/funbox";
+import * as Funbox from "../test/funbox/funbox";
import * as TagController from "../controllers/tag-controller";
import * as PresetController from "../controllers/preset-controller";
import * as ThemePicker from "../elements/settings/theme-picker";
@@ -15,7 +15,6 @@ import * as ConfigEvent from "../observables/config-event";
import * as ActivePage from "../states/active-page";
import Page from "./page";
import { isAuthenticated } from "../firebase";
-import { areFunboxesCompatible } from "../test/funbox/funbox-validation";
import { get as getTypingSpeedUnit } from "../utils/typing-speed-units";
import SlimSelect from "slim-select";
@@ -25,6 +24,12 @@ import {
ConfigValue,
CustomLayoutFluid,
} from "@monkeytype/contracts/schemas/configs";
+import {
+ getAllFunboxes,
+ FunboxName,
+ checkCompatibility,
+} from "@monkeytype/funbox";
+import { getActiveFunboxNames } from "../test/funbox/list";
type SettingsGroups<T extends ConfigValue> = Record<string, SettingsGroup<T>>;
@@ -588,46 +593,37 @@ async function fillSettingsPage(): Promise<void> {
funboxEl.innerHTML = `<div class="funbox button" data-config-value='none'>none</div>`;
let funboxElHTML = "";
- let funboxList;
- try {
- funboxList = await JSONData.getFunboxList();
- } catch (e) {
- console.error(Misc.createErrorMessage(e, "Failed to get funbox list"));
- }
-
- if (funboxList) {
- for (const funbox of funboxList) {
- if (funbox.name === "mirror") {
- funboxElHTML += `<div class="funbox button" data-config-value='${
- funbox.name
- }' aria-label="${
- funbox.info
- }" data-balloon-pos="up" data-balloon-length="fit" style="transform:scaleX(-1);">${funbox.name.replace(
- /_/g,
- " "
- )}</div>`;
- } else if (funbox.name === "upside_down") {
- funboxElHTML += `<div class="funbox button" data-config-value='${
- funbox.name
- }' aria-label="${
- funbox.info
- }" data-balloon-pos="up" data-balloon-length="fit" style="transform:scaleX(-1) scaleY(-1); z-index:1;">${funbox.name.replace(
- /_/g,
- " "
- )}</div>`;
- } else {
- funboxElHTML += `<div class="funbox button" data-config-value='${
- funbox.name
- }' aria-label="${
- funbox.info
- }" data-balloon-pos="up" data-balloon-length="fit">${funbox.name.replace(
- /_/g,
- " "
- )}</div>`;
- }
+ for (const funbox of getAllFunboxes()) {
+ if (funbox.name === "mirror") {
+ funboxElHTML += `<div class="funbox button" data-config-value='${
+ funbox.name
+ }' aria-label="${
+ funbox.description
+ }" data-balloon-pos="up" data-balloon-length="fit" style="transform:scaleX(-1);">${funbox.name.replace(
+ /_/g,
+ " "
+ )}</div>`;
+ } else if (funbox.name === "upside_down") {
+ funboxElHTML += `<div class="funbox button" data-config-value='${
+ funbox.name
+ }' aria-label="${
+ funbox.description
+ }" data-balloon-pos="up" data-balloon-length="fit" style="transform:scaleX(-1) scaleY(-1); z-index:1;">${funbox.name.replace(
+ /_/g,
+ " "
+ )}</div>`;
+ } else {
+ funboxElHTML += `<div class="funbox button" data-config-value='${
+ funbox.name
+ }' aria-label="${
+ funbox.description
+ }" data-balloon-pos="up" data-balloon-length="fit">${funbox.name.replace(
+ /_/g,
+ " "
+ )}</div>`;
}
- funboxEl.innerHTML = funboxElHTML;
}
+ funboxEl.innerHTML = funboxElHTML;
let isCustomFont = true;
const fontsEl = document.querySelector(
@@ -728,26 +724,16 @@ function setActiveFunboxButton(): void {
$(`.pageSettings .section[data-config-name='funbox'] .button`).removeClass(
"disabled"
);
- JSONData.getFunboxList()
- .then((funboxModes) => {
- funboxModes.forEach((funbox) => {
- if (
- !areFunboxesCompatible(Config.funbox, funbox.name) &&
- !Config.funbox.split("#").includes(funbox.name)
- ) {
- $(
- `.pageSettings .section[data-config-name='funbox'] .button[data-config-value='${funbox.name}']`
- ).addClass("disabled");
- }
- });
- })
- .catch((e: unknown) => {
- const message = Misc.createErrorMessage(
- e,
- "Failed to update funbox buttons"
- );
- Notifications.add(message, -1);
- });
+ getAllFunboxes().forEach((funbox) => {
+ if (
+ !checkCompatibility(getActiveFunboxNames(), funbox.name) &&
+ !Config.funbox.split("#").includes(funbox.name)
+ ) {
+ $(
+ `.pageSettings .section[data-config-name='funbox'] .button[data-config-value='${funbox.name}']`
+ ).addClass("disabled");
+ }
+ });
Config.funbox.split("#").forEach((funbox) => {
$(
`.pageSettings .section[data-config-name='funbox'] .button[data-config-value='${funbox}']`
@@ -1057,8 +1043,8 @@ $(".pageSettings .section[data-config-name='funbox']").on(
"click",
".button",
(e) => {
- const funbox = $(e.currentTarget).attr("data-config-value") as string;
- toggleFunbox(funbox);
+ const funbox = $(e.currentTarget).attr("data-config-value") as FunboxName;
+ Funbox.toggleFunbox(funbox);
setActiveFunboxButton();
}
);
diff --git a/frontend/src/ts/ready.ts b/frontend/src/ts/ready.ts
index 5ae62c550..e1b923688 100644
--- a/frontend/src/ts/ready.ts
+++ b/frontend/src/ts/ready.ts
@@ -1,14 +1,13 @@
-import Config from "./config";
import * as Misc from "./utils/misc";
import * as MonkeyPower from "./elements/monkey-power";
import * as MerchBanner from "./elements/merch-banner";
import * as CookiesModal from "./modals/cookies";
import * as ConnectionState from "./states/connection";
import * as AccountButton from "./elements/account-button";
-import * as FunboxList from "./test/funbox/funbox-list";
//@ts-expect-error
import Konami from "konami";
import * as ServerConfiguration from "./ape/server-configuration";
+import { getActiveFunboxes } from "./test/funbox/list";
$((): void => {
Misc.loadCSS("/css/slimselect.min.css", true);
@@ -21,9 +20,9 @@ $((): void => {
$("body").css("transition", "background .25s, transform .05s");
MerchBanner.showIfNotClosedBefore();
setTimeout(() => {
- FunboxList.get(Config.funbox).forEach((it) =>
- it.functions?.applyGlobalCSS?.()
- );
+ for (const fb of getActiveFunboxes()) {
+ fb.functions?.applyGlobalCSS?.();
+ }
}, 500); //this approach will probably bite me in the ass at some point
$("#app")
diff --git a/frontend/src/ts/test/funbox/funbox-functions.ts b/frontend/src/ts/test/funbox/funbox-functions.ts
new file mode 100644
index 000000000..73d606979
--- /dev/null
+++ b/frontend/src/ts/test/funbox/funbox-functions.ts
@@ -0,0 +1,627 @@
+import { Section } from "../../utils/json-data";
+import { FunboxWordsFrequency, Wordset } from "../wordset";
+import * as GetText from "../../utils/generate";
+import Config, * as UpdateConfig from "../../config";
+import * as Misc from "../../utils/misc";
+import * as Strings from "../../utils/strings";
+import { randomIntFromRange } from "@monkeytype/util/numbers";
+import * as Arrays from "../../utils/arrays";
+import { save } from "./funbox-memory";
+import { type FunboxName } from "@monkeytype/funbox";
+import * as TTSEvent from "../../observables/tts-event";
+import * as Notifications from "../../elements/notifications";
+import * as DDR from "../../utils/ddr";
+import * as TestWords from "../test-words";
+import * as TestInput from "../test-input";
+import * as LayoutfluidFunboxTimer from "./layoutfluid-funbox-timer";
+import * as KeymapEvent from "../../observables/keymap-event";
+import * as MemoryTimer from "./memory-funbox-timer";
+import { getPoem } from "../poetry";
+import * as JSONData from "../../utils/json-data";
+import { getSection } from "../wikipedia";
+import * as WeakSpot from "../weak-spot";
+import * as IPAddresses from "../../utils/ip-addresses";
+
+export type FunboxFunctions = {
+ getWord?: (wordset?: Wordset, wordIndex?: number) => string;
+ punctuateWord?: (word: string) => string;
+ withWords?: (words?: string[]) => Promise<Wordset>;
+ alterText?: (word: string) => string;
+ applyConfig?: () => void;
+ applyGlobalCSS?: () => void;
+ clearGlobal?: () => void;
+ rememberSettings?: () => void;
+ toggleScript?: (params: string[]) => void;
+ pullSection?: (language?: string) => Promise<Section | false>;
+ handleSpace?: () => void;
+ handleChar?: (char: string) => string;
+ isCharCorrect?: (char: string, originalChar: string) => boolean;
+ preventDefaultEvent?: (
+ event: JQuery.KeyDownEvent<Document, null, Document, Document>
+ ) => Promise<boolean>;
+ handleKeydown?: (
+ event: JQuery.KeyDownEvent<Document, undefined, Document, Document>
+ ) => Promise<void>;
+ getResultContent?: () => string;
+ start?: () => void;
+ restart?: () => void;
+ getWordHtml?: (char: string, letterTag?: boolean) => string;
+ getWordsFrequencyMode?: () => FunboxWordsFrequency;
+};
+
+async function readAheadHandleKeydown(
+ event: JQuery.KeyDownEvent<Document, undefined, Document, Document>
+): Promise<void> {
+ const inputCurrentChar = (TestInput.input.current ?? "").slice(-1);
+ const wordCurrentChar = TestWords.words
+ .getCurrent()
+ .slice(TestInput.input.current.length - 1, TestInput.input.current.length);
+ const isCorrect = inputCurrentChar === wordCurrentChar;
+
+ if (
+ event.key == "Backspace" &&
+ !isCorrect &&
+ (TestInput.input.current != "" ||
+ TestInput.input.history[TestWords.words.currentIndex - 1] !=
+ TestWords.words.get(TestWords.words.currentIndex - 1) ||
+ Config.freedomMode)
+ ) {
+ $("#words").addClass("read_ahead_disabled");
+ } else if (event.key == " ") {
+ $("#words").removeClass("read_ahead_disabled");
+ }
+}
+
+//todo move to its own file
+class CharDistribution {
+ public chars: Record<string, number>;
+ public count: number;
+ constructor() {
+ this.chars = {};
+ this.count = 0;
+ }
+
+ public addChar(char: string): void {
+ this.count++;
+ if (char in this.chars) {
+ (this.chars[char] as number)++;
+ } else {
+ this.chars[char] = 1;
+ }
+ }
+
+ public randomChar(): string {
+ const randomIndex = randomIntFromRange(0, this.count - 1);
+ let runningCount = 0;
+ for (const [char, charCount] of Object.entries(this.chars)) {
+ runningCount += charCount;
+ if (runningCount > randomIndex) {
+ return char;
+ }
+ }
+
+ return Object.keys(this.chars)[0] as string;
+ }
+}
+const prefixSize = 2;
+class PseudolangWordGenerator extends Wordset {
+ public ngrams: Record<string, CharDistribution> = {};
+ constructor(words: string[]) {
+ super(words);
+ // Can generate an unbounded number of words in theory.
+ this.length = Infinity;
+
+ for (let word of words) {
+ // Mark the end of each word with a space.
+ word += " ";
+ let prefix = "";
+ for (const c of word) {
+ // Add `c` to the distribution of chars that can come after `prefix`.
+ if (!(prefix in this.ngrams)) {
+ this.ngrams[prefix] = new CharDistribution();
+ }
+ (this.ngrams[prefix] as CharDistribution).addChar(c);
+ prefix = (prefix + c).slice(-prefixSize);
+ }
+ }
+ }
+
+ public override randomWord(): string {
+ let word = "";
+ for (;;) {
+ const prefix = word.slice(-prefixSize);
+ const charDistribution = this.ngrams[prefix];
+ if (!charDistribution) {
+ // This shouldn't happen if this.ngrams is complete. If it does
+ // somehow, start generating a new word.
+ word = "";
+ continue;
+ }
+ // Pick a random char from the distribution that comes after `prefix`.
+ const nextChar = charDistribution.randomChar();
+ if (nextChar === " ") {
+ // A space marks the end of the word, so stop generating and return.
+ break;
+ }
+ word += nextChar;
+ }
+ return word;
+ }
+}
+
+const list: Partial<Record<FunboxName, FunboxFunctions>> = {
+ "58008": {
+ getWord(): string {
+ let num = GetText.getNumbers(7);
+ if (Config.language.startsWith("kurdish")) {
+ num = Misc.convertNumberToArabic(num);
+ } else if (Config.language.startsWith("nepali")) {
+ num = Misc.convertNumberToNepali(num);
+ }
+ return num;
+ },
+ punctuateWord(word: string): string {
+ if (word.length > 3) {
+ if (Math.random() < 0.5) {
+ word = Strings.replaceCharAt(
+ word,
+ randomIntFromRange(1, word.length - 2),
+ "."
+ );
+ }
+ if (Math.random() < 0.75) {
+ const index = randomIntFromRange(1, word.length - 2);
+ if (
+ word[index - 1] !== "." &&
+ word[index + 1] !== "." &&
+ word[index + 1] !== "0"
+ ) {
+ const special = Arrays.randomElementFromArray(["/", "*", "-", "+"]);
+ word = Strings.replaceCharAt(word, index, special);
+ }
+ }
+ }
+ return word;
+ },
+ rememberSettings(): void {
+ save("numbers", Config.numbers, UpdateConfig.setNumbers);
+ },
+ handleChar(char: string): string {
+ if (char === "\n") {
+ return " ";
+ }
+ return char;
+ },
+ },
+ simon_says: {
+ applyConfig(): void {
+ UpdateConfig.setKeymapMode("next", true);
+ },
+ rememberSettings(): void {
+ save("keymapMode", Config.keymapMode, UpdateConfig.setKeymapMode);
+ },
+ },
+ tts: {
+ applyConfig(): void {
+ UpdateConfig.setKeymapMode("off", true);
+ },
+ rememberSettings(): void {
+ save("keymapMode", Config.keymapMode, UpdateConfig.setKeymapMode);
+ },
+ toggleScript(params: string[]): void {
+ if (window.speechSynthesis === undefined) {
+ Notifications.add("Failed to load text-to-speech script", -1);
+ return;
+ }
+ if (params[0] !== undefined) void TTSEvent.dispatch(params[0]);
+ },
+ },
+ arrows: {
+ getWord(_wordset, wordIndex): string {
+ return DDR.chart2Word(wordIndex === 0);
+ },
+ rememberSettings(): void {
+ save(
+ "highlightMode",
+ Config.highlightMode,
+ UpdateConfig.setHighlightMode
+ );
+ },
+ handleChar(char: string): string {
+ if (char === "a" || char === "ArrowLeft" || char === "j") {
+ return "←";
+ }
+ if (char === "s" || char === "ArrowDown" || char === "k") {
+ return "↓";
+ }
+ if (char === "w" || char === "ArrowUp" || char === "i") {
+ return "↑";
+ }
+ if (char === "d" || char === "ArrowRight" || char === "l") {
+ return "→";
+ }
+ return char;
+ },
+ isCharCorrect(char: string, originalChar: string): boolean {
+ if (
+ (char === "a" || char === "ArrowLeft" || char === "j") &&
+ originalChar === "←"
+ ) {
+ return true;
+ }
+ if (
+ (char === "s" || char === "ArrowDown" || char === "k") &&
+ originalChar === "↓"
+ ) {
+ return true;
+ }
+ if (
+ (char === "w" || char === "ArrowUp" || char === "i") &&
+ originalChar === "↑"
+ ) {
+ return true;
+ }
+ if (
+ (char === "d" || char === "ArrowRight" || char === "l") &&
+ originalChar === "→"
+ ) {
+ return true;
+ }
+ return false;
+ },
+ async preventDefaultEvent(event: JQuery.KeyDownEvent): Promise<boolean> {
+ return ["ArrowLeft", "ArrowUp", "ArrowRight", "ArrowDown"].includes(
+ event.key
+ );
+ },
+ getWordHtml(char: string, letterTag?: boolean): string {
+ let retval = "";
+ if (char === "↑") {
+ if (letterTag) retval += `<letter>`;
+ retval += `<i class="fas fa-arrow-up"></i>`;
+ if (letterTag) retval += `</letter>`;
+ }
+ if (char === "↓") {
+ if (letterTag) retval += `<letter>`;
+ retval += `<i class="fas fa-arrow-down"></i>`;
+ if (letterTag) retval += `</letter>`;
+ }
+ if (char === "←") {
+ if (letterTag) retval += `<letter>`;
+ retval += `<i class="fas fa-arrow-left"></i>`;
+ if (letterTag) retval += `</letter>`;
+ }
+ if (char === "→") {
+ if (letterTag) retval += `<letter>`;
+ retval += `<i class="fas fa-arrow-right"></i>`;
+ if (letterTag) retval += `</letter>`;
+ }
+ return retval;
+ },
+ },
+ rAnDoMcAsE: {
+ alterText(word: string): string {
+ let randomcaseword = word[0] as string;
+ for (let i = 1; i < word.length; i++) {
+ if (
+ randomcaseword[i - 1] ===
+ (randomcaseword[i - 1] as string).toUpperCase()
+ ) {
+ randomcaseword += (word[i] as string).toLowerCase();
+ } else {
+ randomcaseword += (word[i] as string).toUpperCase();
+ }
+ }
+ return randomcaseword;
+ },
+ },
+ backwards: {
+ alterText(word: string): string {
+ return word.split("").reverse().join("");
+ },
+ },
+ capitals: {
+ alterText(word: string): string {
+ return Strings.capitalizeFirstLetterOfEachWord(word);
+ },
+ },
+ layoutfluid: {
+ applyConfig(): void {
+ const layout = Config.customLayoutfluid.split("#")[0] ?? "qwerty";
+
+ UpdateConfig.setLayout(layout, true);
+ UpdateConfig.setKeymapLayout(layout, true);
+ },
+ rememberSettings(): void {
+ save("keymapMode", Config.keymapMode, UpdateConfig.setKeymapMode);
+ save("layout", Config.layout, UpdateConfig.setLayout);
+ save("keymapLayout", Config.keymapLayout, UpdateConfig.setKeymapLayout);
+ },
+ handleSpace(): void {
+ if (Config.mode !== "time") {
+ // here I need to check if Config.customLayoutFluid exists because of my
+ // scuffed solution of returning whenever value is undefined in the setCustomLayoutfluid function
+ const layouts: string[] = Config.customLayoutfluid
+ ? Config.customLayoutfluid.split("#")
+ : ["qwerty", "dvorak", "colemak"];
+ const outOf: number = TestWords.words.length;
+ const wordsPerLayout = Math.floor(outOf / layouts.length);
+ const index = Math.floor(
+ (TestInput.input.history.length + 1) / wordsPerLayout
+ );
+ const mod =
+ wordsPerLayout -
+ ((TestWords.words.currentIndex + 1) % wordsPerLayout);
+
+ if (layouts[index] as string) {
+ if (mod <= 3 && (layouts[index + 1] as string)) {
+ LayoutfluidFunboxTimer.show();
+ LayoutfluidFunboxTimer.updateWords(
+ mod,
+ layouts[index + 1] as string
+ );
+ } else {
+ LayoutfluidFunboxTimer.hide();
+ }
+ if (mod === wordsPerLayout) {
+ UpdateConfig.setLayout(layouts[index] as string);
+ UpdateConfig.setKeymapLayout(layouts[index] as string);
+ if (mod > 3) {
+ LayoutfluidFunboxTimer.hide();
+ }
+ }
+ } else {
+ LayoutfluidFunboxTimer.hide();
+ }
+ setTimeout(() => {
+ void KeymapEvent.highlight(
+ TestWords.words
+ .getCurrent()
+ .charAt(TestInput.input.current.length)
+ .toString()
+ );
+ }, 1);
+ }
+ },
+ getResultContent(): string {
+ return Config.customLayoutfluid.replace(/#/g, " ");
+ },
+ restart(): void {
+ if (this.applyConfig) this.applyConfig();
+ setTimeout(() => {
+ void KeymapEvent.highlight(
+ TestWords.words
+ .getCurrent()
+ .substring(
+ TestInput.input.current.length,
+ TestInput.input.current.length + 1
+ )
+ .toString()
+ );
+ }, 1);
+ },
+ },
+ gibberish: {
+ getWord(): string {
+ return GetText.getGibberish();
+ },
+ },
+ ascii: {
+ getWord(): string {
+ return GetText.getASCII();
+ },
+ },
+ specials: {
+ getWord(): string {
+ return GetText.getSpecials();
+ },
+ },
+ read_ahead_easy: {
+ rememberSettings(): void {
+ save(
+ "highlightMode",
+ Config.highlightMode,
+ UpdateConfig.setHighlightMode
+ );
+ },
+ async handleKeydown(event): Promise<void> {
+ await readAheadHandleKeydown(event);
+ },
+ },
+ read_ahead: {
+ rememberSettings(): void {
+ save(
+ "highlightMode",
+ Config.highlightMode,
+ UpdateConfig.setHighlightMode
+ );
+ },
+ async handleKeydown(event): Promise<void> {
+ await readAheadHandleKeydown(event);
+ },
+ },
+ read_ahead_hard: {
+ rememberSettings(): void {
+ save(
+ "highlightMode",
+ Config.highlightMode,
+ UpdateConfig.setHighlightMode
+ );
+ },
+ async handleKeydown(event): Promise<void> {
+ await readAheadHandleKeydown(event);
+ },
+ },
+ memory: {
+ applyConfig(): void {
+ $("#wordsWrapper").addClass("hidden");
+ UpdateConfig.setShowAllLines(true, true);
+ if (Config.keymapMode === "next") {
+ UpdateConfig.setKeymapMode("react", true);
+ }
+ },
+ rememberSettings(): void {
+ save("mode", Config.mode, UpdateConfig.setMode);
+ save("showAllLines", Config.showAllLines, UpdateConfig.setShowAllLines);
+ if (Config.keymapMode === "next") {
+ save("keymapMode", Config.keymapMode, UpdateConfig.setKeymapMode);
+ }
+ },
+ start(): void {
+ MemoryTimer.reset();
+ $("#words").addClass("hidden");
+ },
+ restart(): void {
+ MemoryTimer.start(Math.round(Math.pow(TestWords.words.length, 1.2)));
+ $("#words").removeClass("hidden");
+ if (Config.keymapMode === "next") {
+ UpdateConfig.setKeymapMode("react");
+ }
+ },
+ },
+ nospace: {
+ rememberSettings(): void {
+ save(
+ "highlightMode",
+ Config.highlightMode,
+ UpdateConfig.setHighlightMode
+ );
+ },
+ },
+ poetry: {
+ async pullSection(): Promise<JSONData.Section | false> {
+ return getPoem();
+ },
+ },
+ wikipedia: {
+ async pullSection(lang?: string): Promise<JSONData.Section | false> {
+ return getSection((lang ?? "") || "english");
+ },
+ },
+ weakspot: {
+ getWord(wordset?: Wordset): string {
+ if (wordset !== undefined) return WeakSpot.getWord(wordset);
+ else return "";
+ },
+ },
+ pseudolang: {
+ async withWords(words?: string[]): Promise<Wordset> {
+ if (words !== undefined) return new PseudolangWordGenerator(words);
+ return new Wordset([]);
+ },
+ },
+ IPv4: {
+ getWord(): string {
+ return IPAddresses.getRandomIPv4address();
+ },
+ punctuateWord(word: string): string {
+ let w = word;
+ if (Math.random() < 0.25) {
+ w = IPAddresses.addressToCIDR(word);
+ }
+ return w;
+ },
+ rememberSettings(): void {
+ save("numbers", Config.numbers, UpdateConfig.setNumbers);
+ },
+ },
+ IPv6: {
+ getWord(): string {
+ return IPAddresses.getRandomIPv6address();
+ },
+ punctuateWord(word: string): string {
+ let w = word;
+ if (Math.random() < 0.25) {
+ w = IPAddresses.addressToCIDR(word);
+ }
+ // Compress
+ if (w.includes(":")) {
+ w = IPAddresses.compressIpv6(w);
+ }
+ return w;
+ },
+ rememberSettings(): void {
+ save("numbers", Config.numbers, UpdateConfig.setNumbers);
+ },
+ },
+ binary: {
+ getWord(): string {
+ return GetText.getBinary();
+ },
+ },
+ hexadecimal: {
+ getWord(): string {
+ return GetText.getHexadecimal();
+ },
+ punctuateWord(word: string): string {
+ return `0x${word}`;
+ },
+ rememberSettings(): void {
+ save("punctuation", Config.punctuation, UpdateConfig.setPunctuation);
+ },
+ },
+ zipf: {
+ getWordsFrequencyMode(): FunboxWordsFrequency {
+ return "zipf";
+ },
+ },
+ ddoouubblleedd: {
+ alterText(word: string): string {
+ return word.replace(/./gu, "$&$&");
+ },
+ },
+ instant_messaging: {
+ alterText(word: string): string {
+ return word
+ .toLowerCase()
+ .replace(/[.!?]$/g, "\n") //replace .?! with enter
+ .replace(/[().'"]/g, "") //remove special characters
+ .replace(/\n+/g, "\n"); //make sure there is only one enter
+ },
+ },
+ morse: {
+ alterText(word: string): string {
+ return GetText.getMorse(word);
+ },
+ },
+ crt: {
+ applyGlobalCSS(): void {
+ const isSafari = /^((?!chrome|android).)*safari/i.test(
+ navigator.userAgent
+ );
+ if (isSafari) {
+ //Workaround for bug https://bugs.webkit.org/show_bug.cgi?id=256171 in Safari 16.5 or earlier
+ const versionMatch = navigator.userAgent.match(
+ /.*Version\/([0-9]*)\.([0-9]*).*/
+ );
+ const mainVersion =
+ versionMatch !== null ? parseInt(versionMatch[1] ?? "0") : 0;
+ const minorVersion =
+ versionMatch !== null ? parseInt(versionMatch[2] ?? "0") : 0;
+ if (mainVersion <= 16 && minorVersion <= 5) {
+ Notifications.add(
+ "CRT is not available on Safari 16.5 or earlier.",
+ 0,
+ {
+ duration: 5,
+ }
+ );
+ UpdateConfig.toggleFunbox("crt");
+ return;
+ }
+ }
+ $("body").append('<div id="scanline" />');
+ $("body").addClass("crtmode");
+ $("#globalFunBoxTheme").attr("href", `funbox/crt.css`);
+ },
+ clearGlobal(): void {
+ $("#scanline").remove();
+ $("body").removeClass("crtmode");
+ $("#globalFunBoxTheme").attr("href", ``);
+ },
+ },
+};
+
+export function getFunboxFunctions(): Record<FunboxName, FunboxFunctions> {
+ return list as Record<FunboxName, FunboxFunctions>;
+}
diff --git a/frontend/src/ts/test/funbox/funbox-list.ts b/frontend/src/ts/test/funbox/funbox-list.ts
deleted file mode 100644
index 9090c7a74..000000000
--- a/frontend/src/ts/test/funbox/funbox-list.ts
+++ /dev/null
@@ -1,302 +0,0 @@
-import { FunboxFunctions, FunboxMetadata } from "../../utils/json-data";
-
-const list: FunboxMetadata[] = [
- {
- name: "nausea",
- info: "I think I'm gonna be sick.",
- hasCSS: true,
- },
- {
- name: "round_round_baby",
- info: "...right round, like a record baby. Right, round round round.",
- hasCSS: true,
- },
- {
- name: "simon_says",
- info: "Type what simon says.",
- properties: ["changesWordsVisibility", "usesLayout"],
- forcedConfig: {
- highlightMode: ["letter", "off"],
- },
- hasCSS: true,
- },
- {
- name: "mirror",
- info: "Everything is mirrored!",
- hasCSS: true,
- },
- {
- name: "upside_down",
- info: "Everything is upside down!",
- hasCSS: true,
- },
- {
- name: "tts",
- info: "Listen closely.",
- properties: ["changesWordsVisibility", "speaks"],
- forcedConfig: {
- highlightMode: ["letter", "off"],
- },
- hasCSS: true,
- },
- {
- name: "choo_choo",
- info: "All the letters are spinning!",
- properties: ["noLigatures", "conflictsWithSymmetricChars"],
- hasCSS: true,
- },
- {
- name: "arrows",
- info: "Play it on a pad!",
- properties: [
- "ignoresLanguage",
- "ignoresLayout",
- "nospace",
- "noLetters",
- "symmetricChars",
- ],
- forcedConfig: {
- punctuation: [false],
- numbers: [false],
- highlightMode: ["letter", "off"],
- },
- },
- {
- name: "rAnDoMcAsE",
- info: "I kInDa LiKe HoW iNeFfIcIeNt QwErTy Is.",
- properties: ["changesCapitalisation"],
- },
- {
- name: "capitals",
- info: "Capitalize Every Word.",
- properties: ["changesCapitalisation"],
- },
- {
- name: "layoutfluid",
- info: "Switch between layouts specified below proportionately to the length of the test.",
- properties: ["changesLayout", "noInfiniteDuration"],
- },
- {
- name: "earthquake",
- info: "Everybody get down! The words are shaking!",
- properties: ["noLigatures"],
- hasCSS: true,
- },
- {
- name: "space_balls",
- info: "In a galaxy far far away.",
- hasCSS: true,
- },
- {
- name: "gibberish",
- info: "Anvbuefl dizzs eoos alsb?",
- properties: ["ignoresLanguage", "unspeakable"],
- },
- {
- name: "58008",
- alias: "numbers",
- info: "A special mode for accountants.",
- properties: ["ignoresLanguage", "ignoresLayout", "noLetters"],
- forcedConfig: {
- numbers: [false],
- },
- },
- {
- name: "ascii",
- info: "Where was the ampersand again?. Only ASCII characters.",
- properties: ["ignoresLanguage", "noLetters", "unspeakable"],
- forcedConfig: {
- numbers: [false],
- },
- },
- {
- name: "specials",
- info: "!@#$%^&*. Only special characters.",
- properties: ["ignoresLanguage", "noLetters", "unspeakable"],
- forcedConfig: {
- punctuation: [false],
- numbers: [false],
- },
- },
- {
- name: "plus_zero",
- info: "React quickly! Only the current word is visible.",
- properties: ["changesWordsVisibility", "toPush:1", "noInfiniteDuration"],
- },
- {
- name: "plus_one",
- info: "Only one future word is visible.",
- properties: ["changesWordsVisibility", "toPush:2", "noInfiniteDuration"],
- },
- {
- name: "plus_two",
- info: "Only two future words are visible.",
- properties: ["changesWordsVisibility", "toPush:3", "noInfiniteDuration"],
- },
- {
- name: "plus_three",
- info: "Only three future words are visible.",
- properties: ["changesWordsVisibility", "toPush:4", "noInfiniteDuration"],
- },
- {
- name: "read_ahead_easy",
- info: "Only the current word is invisible.",
- properties: ["changesWordsVisibility"],
- forcedConfig: {
- highlightMode: ["letter", "off"],
- },
- hasCSS: true,
- },
- {
- name: "read_ahead",
- info: "Current and the next word are invisible!",
- properties: ["changesWordsVisibility"],
- forcedConfig: {
- highlightMode: ["letter", "off"],
- },
- hasCSS: true,
- },
- {
- name: "read_ahead_hard",
- info: "Current and the next two words are invisible!",
- properties: ["changesWordsVisibility"],
- forcedConfig: {
- highlightMode: ["letter", "off"],
- },
- hasCSS: true,
- },
- {
- name: "memory",
- info: "Test your memory. Remember the words and type them blind.",
- properties: ["changesWordsVisibility", "noInfiniteDuration"],
- forcedConfig: {
- mode: ["words", "quote", "custom"],
- },
- },
- {
- name: "nospace",
- info: "Whoneedsspacesanyway?",
- properties: ["nospace"],
- forcedConfig: {
- highlightMode: ["letter", "off"],
- },
- },
- {
- name: "poetry",
- info: "Practice typing some beautiful prose.",
- properties: ["noInfiniteDuration", "ignoresLanguage"],
- forcedConfig: {
- punctuation: [false],
- numbers: [false],
- },
- },
- {
- name: "wikipedia",
- info: "Practice typing wikipedia sections.",
- properties: ["noInfiniteDuration", "ignoresLanguage"],
- forcedConfig: {
- punctuation: [false],
- numbers: [false],
- },
- },
- {
- name: "weakspot",
- info: "Focus on slow and mistyped letters.",
- properties: ["changesWordsFrequency"],
- },
- {
- name: "pseudolang",
- info: "Nonsense words that look like the current language.",
- properties: ["unspeakable", "ignoresLanguage"],
- },
- {
- name: "IPv4",
- alias: "network",
- info: "For sysadmins.",
- properties: ["ignoresLanguage", "ignoresLayout", "noLetters"],
- forcedConfig: {
- numbers: [false],
- },
- },
- {
- name: "IPv6",
- alias: "network",
- info: "For sysadmins with a long beard.",
- properties: ["ignoresLanguage", "ignoresLayout", "noLetters"],
- forcedConfig: {
- numbers: [false],
- },
- },
- {
- name: "binary",
- alias: "numbers",
- info: "01000010 01100101 01100101 01110000 00100000 01100010 01101111 01101111 01110000 00101110",
- properties: ["ignoresLanguage", "ignoresLayout", "noLetters"],
- forcedConfig: {
- numbers: [false],
- punctuation: [false],
- },
- },
- {
- name: "hexadecimal",
- info: "0x38 0x20 0x74 0x69 0x6D 0x65 0x73 0x20 0x6D 0x6F 0x72 0x65 0x20 0x62 0x6F 0x6F 0x70 0x21",
- properties: ["ignoresLanguage", "ignoresLayout", "noLetters"],
- forcedConfig: {
- numbers: [false],
- },
- },
- {
- name: "zipf",
- alias: "frequency",
- info: "Words are generated according to Zipf's law. (not all languages will produce Zipfy results, use with caution)",
- properties: ["changesWordsFrequency"],
- },
- {
- name: "morse",
- info: "-.../././.--./ -.../---/---/.--./-.-.--/ ",
- properties: ["ignoresLanguage", "ignoresLayout", "noLetters", "nospace"],
- },
- {
- name: "crt",
- info: "Go back to the 1980s",
- properties: ["noLigatures"],
- },
- {
- name: "backwards",
- info: "...sdrawkcab epyt ot yrt woN",
- properties: [
- "noLigatures",
- "conflictsWithSymmetricChars",
- "wordOrder:reverse",
- ],
- },
- {
- name: "ddoouubblleedd",
- info: "TTyyppee eevveerryytthhiinngg ttwwiiccee..",
- properties: ["noLigatures"],
- },
- {
- name: "instant_messaging",
- info: "Who needs shift anyway?",
- properties: ["changesCapitalisation"],
- },
-];
-
-export function getAll(): FunboxMetadata[] {
- return list;
-}
-
-export function get(config: string): FunboxMetadata[] {
- const funboxes: FunboxMetadata[] = [];
- for (const i of config.split("#")) {
- const f = list.find((f) => f.name === i);
- if (f) funboxes.push(f);
- }
- return funboxes;
-}
-
-export function setFunboxFunctions(name: string, obj: FunboxFunctions): void {
- const fb = list.find((f) => f.name === name);
- if (!fb) throw new Error(`Funbox ${name} not found.`);
- fb.functions = obj;
-}
diff --git a/frontend/src/ts/test/funbox/funbox-validation.ts b/frontend/src/ts/test/funbox/funbox-validation.ts
index 0f0bf1cfd..eb0510736 100644
--- a/frontend/src/ts/test/funbox/funbox-validation.ts
+++ b/frontend/src/ts/test/funbox/funbox-validation.ts
@@ -1,26 +1,24 @@
-import * as FunboxList from "./funbox-list";
import * as Notifications from "../../elements/notifications";
import * as Strings from "../../utils/strings";
import { Config, ConfigValue } from "@monkeytype/contracts/schemas/configs";
+import { FunboxMetadata, getFunboxesFromString } from "@monkeytype/funbox";
import { intersect } from "@monkeytype/util/arrays";
-import { FunboxForcedConfig, FunboxMetadata } from "../../utils/json-data";
-export function checkFunboxForcedConfigs(
+export function checkForcedConfig(
key: string,
value: ConfigValue,
- funbox: string
+ funboxes: FunboxMetadata[]
): {
result: boolean;
forcedConfigs?: ConfigValue[];
} {
- if (FunboxList.get(funbox).length === 0) return { result: true };
+ if (funboxes.length === 0) {
+ return { result: true };
+ }
if (key === "words" || key === "time") {
if (value === 0) {
- if (funbox === "nospace") {
- console.log("break");
- }
- const fb = FunboxList.get(funbox).filter((f) =>
+ const fb = funboxes.filter((f) =>
f.properties?.includes("noInfiniteDuration")
);
if (fb.length > 0) {
@@ -37,16 +35,16 @@ export function checkFunboxForcedConfigs(
} else {
const forcedConfigs: Record<string, ConfigValue[]> = {};
// collect all forced configs
- for (const fb of FunboxList.get(funbox)) {
- if (fb.forcedConfig) {
+ for (const fb of funboxes) {
+ if (fb.frontendForcedConfig) {
//push keys to forcedConfigs, if they don't exist. if they do, intersect the values
- for (const key in fb.forcedConfig) {
+ for (const key in fb.frontendForcedConfig) {
if (forcedConfigs[key] === undefined) {
- forcedConfigs[key] = fb.forcedConfig[key] as ConfigValue[];
+ forcedConfigs[key] = fb.frontendForcedConfig[key] as ConfigValue[];
} else {
forcedConfigs[key] = intersect(
forcedConfigs[key],
- fb.forcedConfig[key] as ConfigValue[],
+ fb.frontendForcedConfig[key] as ConfigValue[],
true
);
}
@@ -80,22 +78,19 @@ export function canSetConfigWithCurrentFunboxes(
): boolean {
let errorCount = 0;
if (key === "mode") {
- let fb: FunboxMetadata[] = [];
- fb = fb.concat(
- FunboxList.get(funbox).filter(
- (f) =>
- f.forcedConfig?.["mode"] !== undefined &&
- !f.forcedConfig?.["mode"].includes(value)
- )
+ let fb = getFunboxesFromString(funbox).filter(
+ (f) =>
+ f.frontendForcedConfig?.["mode"] !== undefined &&
+ !(f.frontendForcedConfig["mode"] as ConfigValue[]).includes(value)
);
if (value === "zen") {
fb = fb.concat(
- FunboxList.get(funbox).filter(
- (f) =>
- f.functions?.getWord ??
- f.functions?.pullSection ??
- f.functions?.alterText ??
- f.functions?.withWords ??
+ getFunboxesFromString(funbox).filter((f) => {
+ return (
+ f.frontendFunctions?.includes("getWord") ??
+ f.frontendFunctions?.includes("pullSection") ??
+ f.frontendFunctions?.includes("alterText") ??
+ f.frontendFunctions?.includes("withWords") ??
f.properties?.includes("changesCapitalisation") ??
f.properties?.includes("nospace") ??
f.properties?.find((fp) => fp.startsWith("toPush:")) ??
@@ -103,18 +98,20 @@ export function canSetConfigWithCurrentFunboxes(
f.properties?.includes("speaks") ??
f.properties?.includes("changesLayout") ??
f.properties?.includes("changesWordsFrequency")
- )
+ );
+ })
);
}
if (value === "quote" || value === "custom") {
fb = fb.concat(
- FunboxList.get(funbox).filter(
- (f) =>
- f.functions?.getWord ??
- f.functions?.pullSection ??
- f.functions?.withWords ??
+ getFunboxesFromString(funbox).filter((f) => {
+ return (
+ f.frontendFunctions?.includes("getWord") ??
+ f.frontendFunctions?.includes("pullSection") ??
+ f.frontendFunctions?.includes("withWords") ??
f.properties?.includes("changesWordsFrequency")
- )
+ );
+ })
);
}
@@ -123,7 +120,7 @@ export function canSetConfigWithCurrentFunboxes(
}
}
if (key === "words" || key === "time") {
- if (!checkFunboxForcedConfigs(key, value, funbox).result) {
+ if (!checkForcedConfig(key, value, getFunboxesFromString(funbox)).result) {
if (!noNotification) {
Notifications.add("Active funboxes do not support infinite tests", 0);
return false;
@@ -131,7 +128,9 @@ export function canSetConfigWithCurrentFunboxes(
errorCount += 1;
}
}
- } else if (!checkFunboxForcedConfigs(key, value, funbox).result) {
+ } else if (
+ !checkForcedConfig(key, value, getFunboxesFromString(funbox)).result
+ ) {
errorCount += 1;
}
@@ -204,142 +203,3 @@ export function canSetFunboxWithConfig(
return true;
}
}
-
-export function areFunboxesCompatible(
- funboxes: string,
- withFunbox?: string
-): boolean {
- if (withFunbox === "none" || funboxes === "none") return true;
- let funboxesToCheck = FunboxList.get(funboxes);
- if (withFunbox !== undefined) {
- funboxesToCheck = funboxesToCheck.concat(
- FunboxList.getAll().filter((f) => f.name === withFunbox)
- );
- }
-
- const allFunboxesAreValid =
- FunboxList.get(funboxes).filter(
- (f) => funboxes.split("#").find((cf) => cf === f.name) !== undefined
- ).length === funboxes.split("#").length;
- const oneWordModifierMax =
- funboxesToCheck.filter(
- (f) =>
- f.functions?.getWord ??
- f.functions?.pullSection ??
- f.functions?.withWords
- ).length <= 1;
- const layoutUsability =
- funboxesToCheck.filter((f) =>
- f.properties?.find((fp) => fp === "changesLayout")
- ).length === 0 ||
- funboxesToCheck.filter((f) =>
- f.properties?.find((fp) => fp === "ignoresLayout" || fp === "usesLayout")
- ).length === 0;
- const oneNospaceOrToPushMax =
- funboxesToCheck.filter((f) =>
- f.properties?.find((fp) => fp === "nospace" || fp.startsWith("toPush"))
- ).length <= 1;
- const oneChangesWordsVisibilityMax =
- funboxesToCheck.filter((f) =>
- f.properties?.find((fp) => fp === "changesWordsVisibility")
- ).length <= 1;
- const oneFrequencyChangesMax =
- funboxesToCheck.filter((f) =>
- f.properties?.find((fp) => fp === "changesWordsFrequency")
- ).length <= 1;
- const noFrequencyChangesConflicts =
- funboxesToCheck.filter((f) =>
- f.properties?.find((fp) => fp === "changesWordsFrequency")
- ).length === 0 ||
- funboxesToCheck.filter((f) =>
- f.properties?.find((fp) => fp === "ignoresLanguage")
- ).length === 0;
- const capitalisationChangePosibility =
- funboxesToCheck.filter((f) =>
- f.properties?.find((fp) => fp === "noLetters")
- ).length === 0 ||
- funboxesToCheck.filter((f) =>
- f.properties?.find((fp) => fp === "changesCapitalisation")
- ).length === 0;
- const noConflictsWithSymmetricChars =
- funboxesToCheck.filter((f) =>
- f.properties?.find((fp) => fp === "conflictsWithSymmetricChars")
- ).length === 0 ||
- funboxesToCheck.filter((f) =>
- f.properties?.find((fp) => fp === "symmetricChars")
- ).length === 0;
- const oneCanSpeakMax =
- funboxesToCheck.filter((f) => f.properties?.find((fp) => fp === "speaks"))
- .length <= 1;
- const hasLanguageToSpeakAndNoUnspeakable =
- funboxesToCheck.filter((f) => f.properties?.find((fp) => fp === "speaks"))
- .length === 0 ||
- (funboxesToCheck.filter((f) => f.properties?.find((fp) => fp === "speaks"))
- .length === 1 &&
- funboxesToCheck.filter((f) =>
- f.properties?.find((fp) => fp === "unspeakable")
- ).length === 0) ||
- funboxesToCheck.filter((f) =>
- f.properties?.find((fp) => fp === "ignoresLanguage")
- ).length === 0;
- const oneToPushOrPullSectionMax =
- funboxesToCheck.filter(
- (f) =>
- (f.properties?.find((fp) => fp.startsWith("toPush:")) ?? "") ||
- f.functions?.pullSection
- ).length <= 1;
- const oneApplyCSSMax =
- funboxesToCheck.filter((f) => f.hasCSS == true).length <= 1;
- const onePunctuateWordMax =
- funboxesToCheck.filter((f) => f.functions?.punctuateWord).length <= 1;
- const oneCharCheckerMax =
- funboxesToCheck.filter((f) => f.functions?.isCharCorrect).length <= 1;
- const oneCharReplacerMax =
- funboxesToCheck.filter((f) => f.functions?.getWordHtml).length <= 1;
- const oneChangesCapitalisationMax =
- funboxesToCheck.filter((f) =>
- f.properties?.find((fp) => fp === "changesCapitalisation")
- ).length <= 1;
- const allowedConfig = {} as FunboxForcedConfig;
- let noConfigConflicts = true;
- for (const f of funboxesToCheck) {
- if (!f.forcedConfig) continue;
- for (const key in f.forcedConfig) {
- if (allowedConfig[key]) {
- if (
- intersect(
- allowedConfig[key],
- f.forcedConfig[key] as ConfigValue[],
- true
- ).length === 0
- ) {
- noConfigConflicts = false;
- break;
- }
- } else {
- allowedConfig[key] = f.forcedConfig[key] as ConfigValue[];
- }
- }
- }
-
- return (
- allFunboxesAreValid &&
- oneWordModifierMax &&
- layoutUsability &&
- oneNospaceOrToPushMax &&
- oneChangesWordsVisibilityMax &&
- oneFrequencyChangesMax &&
- noFrequencyChangesConflicts &&
- capitalisationChangePosibility &&
- noConflictsWithSymmetricChars &&
- oneCanSpeakMax &&
- hasLanguageToSpeakAndNoUnspeakable &&
- oneToPushOrPullSectionMax &&
- oneApplyCSSMax &&
- onePunctuateWordMax &&
- oneCharCheckerMax &&
- oneCharReplacerMax &&
- oneChangesCapitalisationMax &&
- noConfigConflicts
- );
-}
diff --git a/frontend/src/ts/test/funbox/funbox.ts b/frontend/src/ts/test/funbox/funbox.ts
index 3974bc5d8..b84423257 100644
--- a/frontend/src/ts/test/funbox/funbox.ts
+++ b/frontend/src/ts/test/funbox/funbox.ts
@@ -1,589 +1,43 @@
import * as Notifications from "../../elements/notifications";
import * as Misc from "../../utils/misc";
import * as JSONData from "../../utils/json-data";
-import * as GetText from "../../utils/generate";
-import * as Arrays from "../../utils/arrays";
import * as Strings from "../../utils/strings";
import * as ManualRestart from "../manual-restart-tracker";
import Config, * as UpdateConfig from "../../config";
import * as MemoryTimer from "./memory-funbox-timer";
import * as FunboxMemory from "./funbox-memory";
-import * as FunboxList from "./funbox-list";
-import { save } from "./funbox-memory";
-import * as TTSEvent from "../../observables/tts-event";
-import * as KeymapEvent from "../../observables/keymap-event";
-import * as TestWords from "../test-words";
-import * as TestInput from "../test-input";
-import * as WeakSpot from "../weak-spot";
-import { getPoem } from "../poetry";
-import { getSection } from "../wikipedia";
-import * as IPAddresses from "../../utils/ip-addresses";
-import {
- areFunboxesCompatible,
- checkFunboxForcedConfigs,
-} from "./funbox-validation";
-import { FunboxWordsFrequency, Wordset } from "../wordset";
-import * as LayoutfluidFunboxTimer from "./layoutfluid-funbox-timer";
-import * as DDR from "../../utils/ddr";
import { HighlightMode } from "@monkeytype/contracts/schemas/configs";
import { Mode } from "@monkeytype/contracts/schemas/shared";
-import { randomIntFromRange } from "@monkeytype/util/numbers";
+import { FunboxName, checkCompatibility } from "@monkeytype/funbox";
+import { getActiveFunboxes, getActiveFunboxNames } from "./list";
+import { checkForcedConfig } from "./funbox-validation";
-const prefixSize = 2;
-
-class CharDistribution {
- public chars: Record<string, number>;
- public count: number;
- constructor() {
- this.chars = {};
- this.count = 0;
- }
-
- public addChar(char: string): void {
- this.count++;
- if (char in this.chars) {
- (this.chars[char] as number)++;
- } else {
- this.chars[char] = 1;
- }
- }
-
- public randomChar(): string {
- const randomIndex = randomIntFromRange(0, this.count - 1);
- let runningCount = 0;
- for (const [char, charCount] of Object.entries(this.chars)) {
- runningCount += charCount;
- if (runningCount > randomIndex) {
- return char;
- }
- }
-
- return Object.keys(this.chars)[0] as string;
- }
-}
-
-class PseudolangWordGenerator extends Wordset {
- public ngrams: Record<string, CharDistribution> = {};
- constructor(words: string[]) {
- super(words);
- // Can generate an unbounded number of words in theory.
- this.length = Infinity;
-
- for (let word of words) {
- // Mark the end of each word with a space.
- word += " ";
- let prefix = "";
- for (const c of word) {
- // Add `c` to the distribution of chars that can come after `prefix`.
- if (!(prefix in this.ngrams)) {
- this.ngrams[prefix] = new CharDistribution();
- }
- (this.ngrams[prefix] as CharDistribution).addChar(c);
- prefix = (prefix + c).slice(-prefixSize);
- }
- }
- }
-
- public override randomWord(): string {
- let word = "";
- for (;;) {
- const prefix = word.slice(-prefixSize);
- const charDistribution = this.ngrams[prefix];
- if (!charDistribution) {
- // This shouldn't happen if this.ngrams is complete. If it does
- // somehow, start generating a new word.
- word = "";
- continue;
- }
- // Pick a random char from the distribution that comes after `prefix`.
- const nextChar = charDistribution.randomChar();
- if (nextChar === " ") {
- // A space marks the end of the word, so stop generating and return.
- break;
- }
- word += nextChar;
- }
- return word;
- }
-}
-
-FunboxList.setFunboxFunctions("simon_says", {
- applyConfig(): void {
- UpdateConfig.setKeymapMode("next", true);
- },
- rememberSettings(): void {
- save("keymapMode", Config.keymapMode, UpdateConfig.setKeymapMode);
- },
-});
-
-FunboxList.setFunboxFunctions("tts", {
- applyConfig(): void {
- UpdateConfig.setKeymapMode("off", true);
- },
- rememberSettings(): void {
- save("keymapMode", Config.keymapMode, UpdateConfig.setKeymapMode);
- },
- toggleScript(params: string[]): void {
- if (window.speechSynthesis === undefined) {
- Notifications.add("Failed to load text-to-speech script", -1);
- return;
- }
- if (params[0] !== undefined) void TTSEvent.dispatch(params[0]);
- },
-});
-
-FunboxList.setFunboxFunctions("arrows", {
- getWord(_wordset, wordIndex): string {
- return DDR.chart2Word(wordIndex === 0);
- },
- rememberSettings(): void {
- save("highlightMode", Config.highlightMode, UpdateConfig.setHighlightMode);
- },
- handleChar(char: string): string {
- if (char === "a" || char === "ArrowLeft" || char === "j") {
- return "←";
- }
- if (char === "s" || char === "ArrowDown" || char === "k") {
- return "↓";
- }
- if (char === "w" || char === "ArrowUp" || char === "i") {
- return "↑";
- }
- if (char === "d" || char === "ArrowRight" || char === "l") {
- return "→";
- }
- return char;
- },
- isCharCorrect(char: string, originalChar: string): boolean {
- if (
- (char === "a" || char === "ArrowLeft" || char === "j") &&
- originalChar === "←"
- ) {
- return true;
- }
- if (
- (char === "s" || char === "ArrowDown" || char === "k") &&
- originalChar === "↓"
- ) {
- return true;
- }
- if (
- (char === "w" || char === "ArrowUp" || char === "i") &&
- originalChar === "↑"
- ) {
- return true;
- }
- if (
- (char === "d" || char === "ArrowRight" || char === "l") &&
- originalChar === "→"
- ) {
- return true;
- }
- return false;
- },
- async preventDefaultEvent(event: JQuery.KeyDownEvent): Promise<boolean> {
- return ["ArrowLeft", "ArrowUp", "ArrowRight", "ArrowDown"].includes(
- event.key
- );
- },
- getWordHtml(char: string, letterTag?: boolean): string {
- let retval = "";
- if (char === "↑") {
- if (letterTag) retval += `<letter>`;
- retval += `<i class="fas fa-arrow-up"></i>`;
- if (letterTag) retval += `</letter>`;
- }
- if (char === "↓") {
- if (letterTag) retval += `<letter>`;
- retval += `<i class="fas fa-arrow-down"></i>`;
- if (letterTag) retval += `</letter>`;
- }
- if (char === "←") {
- if (letterTag) retval += `<letter>`;
- retval += `<i class="fas fa-arrow-left"></i>`;
- if (letterTag) retval += `</letter>`;
- }
- if (char === "→") {
- if (letterTag) retval += `<letter>`;
- retval += `<i class="fas fa-arrow-right"></i>`;
- if (letterTag) retval += `</letter>`;
- }
- return retval;
- },
-});
-
-FunboxList.setFunboxFunctions("rAnDoMcAsE", {
- alterText(word: string): string {
- let randomcaseword = word[0] as string;
- for (let i = 1; i < word.length; i++) {
- if (
- randomcaseword[i - 1] ===
- (randomcaseword[i - 1] as string).toUpperCase()
- ) {
- randomcaseword += (word[i] as string).toLowerCase();
- } else {
- randomcaseword += (word[i] as string).toUpperCase();
- }
- }
- return randomcaseword;
- },
-});
-
-FunboxList.setFunboxFunctions("backwards", {
- alterText(word: string): string {
- return word.split("").reverse().join("");
- },
-});
-
-FunboxList.setFunboxFunctions("capitals", {
- alterText(word: string): string {
- return Strings.capitalizeFirstLetterOfEachWord(word);
- },
-});
-
-FunboxList.setFunboxFunctions("layoutfluid", {
- applyConfig(): void {
- const layout = Config.customLayoutfluid.split("#")[0] ?? "qwerty";
-
- UpdateConfig.setLayout(layout, true);
- UpdateConfig.setKeymapLayout(layout, true);
- },
- rememberSettings(): void {
- save("keymapMode", Config.keymapMode, UpdateConfig.setKeymapMode);
- save("layout", Config.layout, UpdateConfig.setLayout);
- save("keymapLayout", Config.keymapLayout, UpdateConfig.setKeymapLayout);
- },
- handleSpace(): void {
- if (Config.mode !== "time") {
- // here I need to check if Config.customLayoutFluid exists because of my
- // scuffed solution of returning whenever value is undefined in the setCustomLayoutfluid function
- const layouts: string[] = Config.customLayoutfluid
- ? Config.customLayoutfluid.split("#")
- : ["qwerty", "dvorak", "colemak"];
- const outOf: number = TestWords.words.length;
- const wordsPerLayout = Math.floor(outOf / layouts.length);
- const index = Math.floor(
- (TestInput.input.history.length + 1) / wordsPerLayout
- );
- const mod =
- wordsPerLayout - ((TestWords.words.currentIndex + 1) % wordsPerLayout);
-
- if (layouts[index] as string) {
- if (mod <= 3 && (layouts[index + 1] as string)) {
- LayoutfluidFunboxTimer.show();
- LayoutfluidFunboxTimer.updateWords(mod, layouts[index + 1] as string);
- } else {
- LayoutfluidFunboxTimer.hide();
- }
- if (mod === wordsPerLayout) {
- UpdateConfig.setLayout(layouts[index] as string);
- UpdateConfig.setKeymapLayout(layouts[index] as string);
- if (mod > 3) {
- LayoutfluidFunboxTimer.hide();
- }
- }
- } else {
- LayoutfluidFunboxTimer.hide();
- }
- setTimeout(() => {
- void KeymapEvent.highlight(
- TestWords.words
- .getCurrent()
- .charAt(TestInput.input.current.length)
- .toString()
- );
- }, 1);
- }
- },
- getResultContent(): string {
- return Config.customLayoutfluid.replace(/#/g, " ");
- },
- restart(): void {
- if (this.applyConfig) this.applyConfig();
- setTimeout(() => {
- void KeymapEvent.highlight(
- TestWords.words
- .getCurrent()
- .substring(
- TestInput.input.current.length,
- TestInput.input.current.length + 1
- )
- .toString()
- );
- }, 1);
- },
-});
-
-FunboxList.setFunboxFunctions("gibberish", {
- getWord(): string {
- return GetText.getGibberish();
- },
-});
-
-FunboxList.setFunboxFunctions("58008", {
- getWord(): string {
- let num = GetText.getNumbers(7);
- if (Config.language.startsWith("kurdish")) {
- num = Misc.convertNumberToArabic(num);
- } else if (Config.language.startsWith("nepali")) {
- num = Misc.convertNumberToNepali(num);
- }
- return num;
- },
- punctuateWord(word: string): string {
- if (word.length > 3) {
- if (Math.random() < 0.5) {
- word = Strings.replaceCharAt(
- word,
- randomIntFromRange(1, word.length - 2),
- "."
- );
- }
- if (Math.random() < 0.75) {
- const index = randomIntFromRange(1, word.length - 2);
- if (
- word[index - 1] !== "." &&
- word[index + 1] !== "." &&
- word[index + 1] !== "0"
- ) {
- const special = Arrays.randomElementFromArray(["/", "*", "-", "+"]);
- word = Strings.replaceCharAt(word, index, special);
- }
- }
- }
- return word;
- },
- rememberSettings(): void {
- save("numbers", Config.numbers, UpdateConfig.setNumbers);
- },
- handleChar(char: string): string {
- if (char === "\n") {
- return " ";
- }
- return char;
- },
-});
-
-FunboxList.setFunboxFunctions("ascii", {
- getWord(): string {
- return GetText.getASCII();
- },
- punctuateWord(word: string): string {
- return word;
- },
-});
-
-FunboxList.setFunboxFunctions("specials", {
- getWord(): string {
- return GetText.getSpecials();
- },
-});
-
-async function readAheadHandleKeydown(
- event: JQuery.KeyDownEvent<Document, undefined, Document, Document>
-): Promise<void> {
- const inputCurrentChar = (TestInput.input.current ?? "").slice(-1);
- const wordCurrentChar = TestWords.words
- .getCurrent()
- .slice(TestInput.input.current.length - 1, TestInput.input.current.length);
- const isCorrect = inputCurrentChar === wordCurrentChar;
+export function toggleScript(...params: string[]): void {
+ if (Config.funbox === "none") return;
- if (
- event.key == "Backspace" &&
- !isCorrect &&
- (TestInput.input.current != "" ||
- TestInput.input.history[TestWords.words.currentIndex - 1] !=
- TestWords.words.get(TestWords.words.currentIndex - 1) ||
- Config.freedomMode)
- ) {
- $("#words").addClass("read_ahead_disabled");
- } else if (event.key == " ") {
- $("#words").removeClass("read_ahead_disabled");
+ for (const fb of getActiveFunboxes()) {
+ fb.functions?.toggleScript?.(params);
}
}
-FunboxList.setFunboxFunctions("read_ahead_easy", {
- rememberSettings(): void {
- save("highlightMode", Config.highlightMode, UpdateConfig.setHighlightMode);
- },
- async handleKeydown(event): Promise<void> {
- await readAheadHandleKeydown(event);
- },
-});
-
-FunboxList.setFunboxFunctions("read_ahead", {
- rememberSettings(): void {
- save("highlightMode", Config.highlightMode, UpdateConfig.setHighlightMode);
- },
- async handleKeydown(event): Promise<void> {
- await readAheadHandleKeydown(event);
- },
-});
-
-FunboxList.setFunboxFunctions("read_ahead_hard", {
- rememberSettings(): void {
- save("highlightMode", Config.highlightMode, UpdateConfig.setHighlightMode);
- },
- async handleKeydown(event): Promise<void> {
- await readAheadHandleKeydown(event);
- },
-});
-
-FunboxList.setFunboxFunctions("memory", {
- applyConfig(): void {
- $("#wordsWrapper").addClass("hidden");
- UpdateConfig.setShowAllLines(true, true);
- if (Config.keymapMode === "next") {
- UpdateConfig.setKeymapMode("react", true);
- }
- },
- rememberSettings(): void {
- save("mode", Config.mode, UpdateConfig.setMode);
- save("showAllLines", Config.showAllLines, UpdateConfig.setShowAllLines);
- if (Config.keymapMode === "next") {
- save("keymapMode", Config.keymapMode, UpdateConfig.setKeymapMode);
- }
- },
- start(): void {
- MemoryTimer.reset();
- $("#words").addClass("hidden");
- },
- restart(): void {
- MemoryTimer.start();
- $("#words").removeClass("hidden");
- if (Config.keymapMode === "next") {
- UpdateConfig.setKeymapMode("react");
- }
- },
-});
-
-FunboxList.setFunboxFunctions("nospace", {
- rememberSettings(): void {
- save("highlightMode", Config.highlightMode, UpdateConfig.setHighlightMode);
- },
-});
-
-FunboxList.setFunboxFunctions("poetry", {
- async pullSection(): Promise<JSONData.Section | false> {
- return getPoem();
- },
-});
-
-FunboxList.setFunboxFunctions("wikipedia", {
- async pullSection(lang?: string): Promise<JSONData.Section | false> {
- return getSection((lang ?? "") || "english");
- },
-});
-
-FunboxList.setFunboxFunctions("weakspot", {
- getWord(wordset?: Wordset): string {
- if (wordset !== undefined) return WeakSpot.getWord(wordset);
- else return "";
- },
-});
-
-FunboxList.setFunboxFunctions("pseudolang", {
- async withWords(words?: string[]): Promise<Wordset> {
- if (words !== undefined) return new PseudolangWordGenerator(words);
- return new Wordset([]);
- },
-});
-
-FunboxList.setFunboxFunctions("IPv4", {
- getWord(): string {
- return IPAddresses.getRandomIPv4address();
- },
- punctuateWord(word: string): string {
- let w = word;
- if (Math.random() < 0.25) {
- w = IPAddresses.addressToCIDR(word);
- }
- return w;
- },
- rememberSettings(): void {
- save("numbers", Config.numbers, UpdateConfig.setNumbers);
- },
-});
-
-FunboxList.setFunboxFunctions("IPv6", {
- getWord(): string {
- return IPAddresses.getRandomIPv6address();
- },
- punctuateWord(word: string): string {
- let w = word;
- if (Math.random() < 0.25) {
- w = IPAddresses.addressToCIDR(word);
- }
- // Compress
- if (w.includes(":")) {
- w = IPAddresses.compressIpv6(w);
- }
- return w;
- },
- rememberSettings(): void {
- save("numbers", Config.numbers, UpdateConfig.setNumbers);
- },
-});
-
-FunboxList.setFunboxFunctions("binary", {
- getWord(): string {
- return GetText.getBinary();
- },
-});
-
-FunboxList.setFunboxFunctions("hexadecimal", {
- getWord(): string {
- return GetText.getHexadecimal();
- },
- punctuateWord(word: string): string {
- return `0x${word}`;
- },
- rememberSettings(): void {
- save("punctuation", Config.punctuation, UpdateConfig.setPunctuation);
- },
-});
-
-FunboxList.setFunboxFunctions("zipf", {
- getWordsFrequencyMode(): FunboxWordsFrequency {
- return "zipf";
- },
-});
-
-FunboxList.setFunboxFunctions("ddoouubblleedd", {
- alterText(word: string): string {
- return word.replace(/./gu, "$&$&");
- },
-});
-
-FunboxList.setFunboxFunctions("instant_messaging", {
- alterText(word: string): string {
- return word
- .toLowerCase()
- .replace(/[.!?]$/g, "\n") //replace .?! with enter
- .replace(/[().'"]/g, "") //remove special characters
- .replace(/\n+/g, "\n"); //make sure there is only one enter
- },
-});
-
-export function toggleScript(...params: string[]): void {
- FunboxList.get(Config.funbox).forEach((funbox) => {
- if (funbox.functions?.toggleScript) funbox.functions.toggleScript(params);
- });
-}
-
export function setFunbox(funbox: string): boolean {
if (funbox === "none") {
- FunboxList.get(Config.funbox).forEach((f) => f.functions?.clearGlobal?.());
+ for (const fb of getActiveFunboxes()) {
+ fb.functions?.clearGlobal?.();
+ }
}
FunboxMemory.load();
UpdateConfig.setFunbox(funbox, false);
return true;
}
-export function toggleFunbox(funbox: string): boolean {
+export function toggleFunbox(funbox: "none" | FunboxName): boolean {
if (funbox === "none") setFunbox("none");
if (
- !areFunboxesCompatible(Config.funbox, funbox) &&
+ !checkCompatibility(
+ getActiveFunboxNames(),
+ funbox === "none" ? undefined : funbox
+ ) &&
!Config.funbox.split("#").includes(funbox)
) {
Notifications.add(
@@ -597,10 +51,12 @@ export function toggleFunbox(funbox: string): boolean {
FunboxMemory.load();
const e = UpdateConfig.toggleFunbox(funbox, false);
- if (!Config.funbox.includes(funbox)) {
- FunboxList.get(funbox).forEach((f) => f.functions?.clearGlobal?.());
- } else {
- FunboxList.get(funbox).forEach((f) => f.functions?.applyGlobalCSS?.());
+ for (const fb of getActiveFunboxes()) {
+ if (!Config.funbox.includes(funbox)) {
+ fb.functions?.clearGlobal?.();
+ } else {
+ fb.functions?.applyGlobalCSS?.();
+ }
}
//todo find out what the hell this means
@@ -635,7 +91,7 @@ export async function activate(funbox?: string): Promise<boolean | undefined> {
// The configuration might be edited with dev tools,
// so we need to double check its validity
- if (!areFunboxesCompatible(Config.funbox)) {
+ if (!checkCompatibility(getActiveFunboxNames())) {
Notifications.add(
Misc.createErrorMessage(
undefined,
@@ -672,9 +128,7 @@ export async function activate(funbox?: string): Promise<boolean | undefined> {
if (language.ligatures) {
if (
- FunboxList.get(Config.funbox).find((f) =>
- f.properties?.includes("noLigatures")
- )
+ getActiveFunboxes().find((f) => f.properties?.includes("noLigatures"))
) {
Notifications.add(
"Current language does not support this funbox mode",
@@ -689,10 +143,10 @@ export async function activate(funbox?: string): Promise<boolean | undefined> {
let canSetSoFar = true;
for (const [configKey, configValue] of Object.entries(Config)) {
- const check = checkFunboxForcedConfigs(
+ const check = checkForcedConfig(
configKey,
configValue,
- Config.funbox
+ getActiveFunboxes()
);
if (check.result) continue;
if (!check.result) {
@@ -742,65 +196,24 @@ export async function activate(funbox?: string): Promise<boolean | undefined> {
}
ManualRestart.set();
- FunboxList.get(Config.funbox).forEach(async (funbox) => {
- funbox.functions?.applyConfig?.();
- });
+ for (const fb of getActiveFunboxes()) {
+ fb.functions?.applyConfig?.();
+ }
// ModesNotice.update();
return true;
}
export async function rememberSettings(): Promise<void> {
- FunboxList.get(Config.funbox).forEach(async (funbox) => {
- if (funbox.functions?.rememberSettings) funbox.functions.rememberSettings();
- });
+ for (const fb of getActiveFunboxes()) {
+ fb.functions?.rememberSettings?.();
+ }
}
-FunboxList.setFunboxFunctions("morse", {
- alterText(word: string): string {
- return GetText.getMorse(word);
- },
-});
-
-FunboxList.setFunboxFunctions("crt", {
- applyGlobalCSS(): void {
- const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
- if (isSafari) {
- //Workaround for bug https://bugs.webkit.org/show_bug.cgi?id=256171 in Safari 16.5 or earlier
- const versionMatch = navigator.userAgent.match(
- /.*Version\/([0-9]*)\.([0-9]*).*/
- );
- const mainVersion =
- versionMatch !== null ? parseInt(versionMatch[1] ?? "0") : 0;
- const minorVersion =
- versionMatch !== null ? parseInt(versionMatch[2] ?? "0") : 0;
- if (mainVersion <= 16 && minorVersion <= 5) {
- Notifications.add(
- "CRT is not available on Safari 16.5 or earlier.",
- 0,
- {
- duration: 5,
- }
- );
- toggleFunbox("crt");
- return;
- }
- }
- $("body").append('<div id="scanline" />');
- $("body").addClass("crtmode");
- $("#globalFunBoxTheme").attr("href", `funbox/crt.css`);
- },
- clearGlobal(): void {
- $("#scanline").remove();
- $("body").removeClass("crtmode");
- $("#globalFunBoxTheme").attr("href", ``);
- },
-});
-
async function setFunboxBodyClasses(): Promise<boolean> {
const $body = $("body");
- const activeFbClasses = FunboxList.get(Config.funbox).map(
- (it) => "fb-" + it.name.replaceAll("_", "-")
+ const activeFbClasses = getActiveFunboxNames().map(
+ (name) => "fb-" + name.replaceAll("_", "-")
);
const currentClasses =
@@ -818,8 +231,8 @@ async function applyFunboxCSS(): Promise<boolean> {
const $theme = $("#funBoxTheme");
//currently we only support one active funbox with hasCSS
- const activeFunboxWithTheme = FunboxList.get(Config.funbox).find(
- (it) => it.hasCSS == true
+ const activeFunboxWithTheme = getActiveFunboxes().find((fb) =>
+ fb?.properties?.includes("hasCssFile")
);
const activeTheme =
diff --git a/frontend/src/ts/test/funbox/list.ts b/frontend/src/ts/test/funbox/list.ts
new file mode 100644
index 000000000..48f45f4a8
--- /dev/null
+++ b/frontend/src/ts/test/funbox/list.ts
@@ -0,0 +1,72 @@
+import Config from "../../config";
+import {
+ FunboxName,
+ stringToFunboxNames,
+ FunboxMetadata,
+ getFunboxObject,
+ FunboxProperty,
+} from "@monkeytype/funbox";
+
+import { FunboxFunctions, getFunboxFunctions } from "./funbox-functions";
+
+type FunboxMetadataWithFunctions = FunboxMetadata & {
+ functions?: FunboxFunctions;
+};
+
+const metadata = getFunboxObject();
+const functions = getFunboxFunctions();
+
+const metadataWithFunctions = {} as Record<
+ FunboxName,
+ FunboxMetadataWithFunctions
+>;
+
+for (const [name, data] of Object.entries(metadata)) {
+ metadataWithFunctions[name as FunboxName] = {
+ ...data,
+ functions: functions[name as FunboxName],
+ };
+}
+
+export function get(funboxName: FunboxName): FunboxMetadataWithFunctions;
+export function get(funboxNames: FunboxName[]): FunboxMetadataWithFunctions[];
+export function get(
+ funboxNameOrNames: FunboxName | FunboxName[]
+): FunboxMetadataWithFunctions | FunboxMetadataWithFunctions[] {
+ if (Array.isArray(funboxNameOrNames)) {
+ const fns = funboxNameOrNames.map((name) => metadataWithFunctions[name]);
+ return fns;
+ } else {
+ return metadataWithFunctions[funboxNameOrNames];
+ }
+}
+
+export function getAllFunboxes(): FunboxMetadataWithFunctions[] {
+ return Object.values(metadataWithFunctions);
+}
+
+export function getFromString(
+ hashSeparatedFunboxes: string
+): FunboxMetadataWithFunctions[] {
+ return get(stringToFunboxNames(hashSeparatedFunboxes));
+}
+
+export function getActiveFunboxes(): FunboxMetadataWithFunctions[] {
+ return get(stringToFunboxNames(Config.funbox));
+}
+
+export function getActiveFunboxNames(): FunboxName[] {
+ return stringToFunboxNames(Config.funbox);
+}
+
+export function getActiveFunboxesWithProperty(
+ property: FunboxProperty
+): FunboxMetadataWithFunctions[] {
+ return getActiveFunboxes().filter((fb) => fb.properties?.includes(property));
+}
+
+export function getActiveFunboxesWithFunction(
+ functionName: keyof FunboxFunctions
+): FunboxMetadataWithFunctions[] {
+ return getActiveFunboxes().filter((fb) => fb.functions?.[functionName]);
+}
diff --git a/frontend/src/ts/test/funbox/memory-funbox-timer.ts b/frontend/src/ts/test/funbox/memory-funbox-timer.ts
index df851e747..e09cdb1d2 100644
--- a/frontend/src/ts/test/funbox/memory-funbox-timer.ts
+++ b/frontend/src/ts/test/funbox/memory-funbox-timer.ts
@@ -1,5 +1,3 @@
-import * as TestWords from "../test-words";
-
let memoryTimer: number | null = null;
let memoryInterval: NodeJS.Timeout | null = null;
@@ -30,9 +28,9 @@ export function reset(): void {
hide();
}
-export function start(): void {
+export function start(time: number): void {
reset();
- memoryTimer = Math.round(Math.pow(TestWords.words.length, 1.2));
+ memoryTimer = time;
update(memoryTimer);
show();
memoryInterval = setInterval(() => {
diff --git a/frontend/src/ts/test/pace-caret.ts b/frontend/src/ts/test/pace-caret.ts
index 13cf7a15f..e5a1ea09f 100644
--- a/frontend/src/ts/test/pace-caret.ts
+++ b/frontend/src/ts/test/pace-caret.ts
@@ -8,6 +8,7 @@ import * as JSONData from "../utils/json-data";
import * as TestState from "./test-state";
import * as ConfigEvent from "../observables/config-event";
import { convertRemToPixels } from "../utils/numbers";
+import { getActiveFunboxes } from "./funbox/list";
type Settings = {
wpm: number;
@@ -79,7 +80,7 @@ export async function init(): Promise<void> {
Config.language,
Config.difficulty,
Config.lazyMode,
- Config.funbox
+ getActiveFunboxes()
)
)?.wpm ?? 0;
} else if (Config.paceCaret === "tagPb") {
diff --git a/frontend/src/ts/test/result.ts b/frontend/src/ts/test/result.ts
index 52a9b1cd7..6b87db26b 100644
--- a/frontend/src/ts/test/result.ts
+++ b/frontend/src/ts/test/result.ts
@@ -15,11 +15,9 @@ import * as SlowTimer from "../states/slow-timer";
import * as DateTime from "../utils/date-and-time";
import * as Misc from "../utils/misc";
import * as Strings from "../utils/strings";
-import * as JSONData from "../utils/json-data";
import * as Numbers from "@monkeytype/util/numbers";
import * as Arrays from "../utils/arrays";
import { get as getTypingSpeedUnit } from "../utils/typing-speed-units";
-import * as FunboxList from "./funbox/funbox-list";
import * as PbCrown from "./pb-crown";
import * as TestConfig from "./test-config";
import * as TestInput from "./test-input";
@@ -32,7 +30,6 @@ import * as CustomText from "./custom-text";
import * as CustomTextState from "./../states/custom-text-name";
import * as Funbox from "./funbox/funbox";
import Format from "../utils/format";
-
import confetti from "canvas-confetti";
import type {
AnnotationOptions,
@@ -40,6 +37,8 @@ import type {
} from "chartjs-plugin-annotation";
import Ape from "../ape";
import { CompletedEvent } from "@monkeytype/contracts/schemas/results";
+import { getActiveFunboxes, getFromString } from "./funbox/list";
+import { getFunboxesFromString } from "@monkeytype/funbox";
let result: CompletedEvent;
let maxChartVal: number;
@@ -127,10 +126,10 @@ async function updateGraph(): Promise<void> {
const fc = await ThemeColors.get("sub");
if (Config.funbox !== "none") {
let content = "";
- for (const f of FunboxList.get(Config.funbox)) {
- content += f.name;
- if (f.functions?.getResultContent) {
- content += "(" + f.functions.getResultContent() + ")";
+ for (const fb of getActiveFunboxes()) {
+ content += fb.name;
+ if (fb.functions?.getResultContent) {
+ content += "(" + fb.functions.getResultContent() + ")";
}
content += " ";
}
@@ -180,7 +179,7 @@ export async function updateGraphPBLine(): Promise<void> {
result.language,
result.difficulty,
result.lazyMode ?? false,
- result.funbox ?? "none"
+ getFunboxesFromString(result.funbox ?? "none")
);
const localPbWpm = localPb?.wpm ?? 0;
if (localPbWpm === 0) return;
@@ -403,7 +402,7 @@ export async function updateCrown(dontSave: boolean): Promise<void> {
Config.language,
Config.difficulty,
Config.lazyMode,
- Config.funbox
+ getActiveFunboxes()
);
const localPbWpm = localPb?.wpm ?? 0;
pbDiff = result.wpm - localPbWpm;
@@ -425,7 +424,7 @@ export async function updateCrown(dontSave: boolean): Promise<void> {
Config.language,
Config.difficulty,
Config.lazyMode,
- "none"
+ []
);
const localPbWpm = localPb?.wpm ?? 0;
pbDiff = result.wpm - localPbWpm;
@@ -474,9 +473,7 @@ type CanGetPbObject =
async function resultCanGetPb(): Promise<CanGetPbObject> {
const funboxes = result.funbox?.split("#") ?? [];
- const funboxObjects = await Promise.all(
- funboxes.map(async (f) => JSONData.getFunbox(f))
- );
+ const funboxObjects = getFromString(result.funbox);
const allFunboxesCanGetPb = funboxObjects.every((f) => f?.canGetPb);
const funboxesOk =
@@ -678,7 +675,7 @@ function updateTestType(randomQuote: Quote | null): void {
}
}
const ignoresLanguage =
- FunboxList.get(Config.funbox).find((f) =>
+ getActiveFunboxes().find((f) =>
f.properties?.includes("ignoresLanguage")
) !== undefined;
if (Config.mode !== "custom" && !ignoresLanguage) {
diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts
index a2aa2882c..3874fba2a 100644
--- a/frontend/src/ts/test/test-logic.ts
+++ b/frontend/src/ts/test/test-logic.ts
@@ -52,7 +52,6 @@ import { Auth, isAuthenticated } from "../firebase";
import * as AdController from "../controllers/ad-controller";
import * as TestConfig from "./test-config";
import * as ConnectionState from "../states/connection";
-import * as FunboxList from "./funbox/funbox-list";
import * as MemoryFunboxTimer from "./funbox/memory-funbox-timer";
import * as KeymapEvent from "../observables/keymap-event";
import * as LayoutfluidFunboxTimer from "../test/funbox/layoutfluid-funbox-timer";
@@ -65,6 +64,8 @@ import {
CustomTextDataWithTextLen,
} from "@monkeytype/contracts/schemas/results";
import * as XPBar from "../elements/xp-bar";
+import { getActiveFunboxes } from "./funbox/list";
+import { getFunboxesFromString } from "@monkeytype/funbox";
let failReason = "";
const koInputVisual = document.getElementById("koInputVisual") as HTMLElement;
@@ -106,8 +107,8 @@ export function startTest(now: number): boolean {
TestTimer.clear();
Monkey.show();
- for (const f of FunboxList.get(Config.funbox)) {
- if (f.functions?.start) f.functions.start();
+ for (const fb of getActiveFunboxes()) {
+ fb.functions?.start?.();
}
try {
@@ -328,8 +329,8 @@ export function restart(options = {} as RestartOptions): void {
await init();
await PaceCaret.init();
- for (const f of FunboxList.get(Config.funbox)) {
- if (f.functions?.restart) f.functions.restart();
+ for (const fb of getActiveFunboxes()) {
+ fb.functions?.restart?.();
}
if (Config.showAverage !== "off") {
@@ -540,7 +541,7 @@ export function areAllTestWordsGenerated(): boolean {
//add word during the test
export async function addWord(): Promise<void> {
let bound = 100; // how many extra words to aim for AFTER the current word
- const funboxToPush = FunboxList.get(Config.funbox)
+ const funboxToPush = getActiveFunboxes()
.find((f) => f.properties?.find((fp) => fp.startsWith("toPush")))
?.properties?.find((fp) => fp.startsWith("toPush:"));
const toPushCount = funboxToPush?.split(":")[1];
@@ -555,9 +556,10 @@ export async function addWord(): Promise<void> {
return;
}
- const sectionFunbox = FunboxList.get(Config.funbox).find(
+ const sectionFunbox = getActiveFunboxes().find(
(f) => f.functions?.pullSection
);
+
if (sectionFunbox?.functions?.pullSection) {
if (TestWords.words.length - TestWords.words.currentIndex < 20) {
const section = await sectionFunbox.functions.pullSection(
@@ -1221,7 +1223,7 @@ async function saveResult(
completedEvent.language,
completedEvent.difficulty,
completedEvent.lazyMode,
- completedEvent.funbox
+ getFunboxesFromString(completedEvent.funbox)
);
if (localPb !== undefined) {
diff --git a/frontend/src/ts/test/test-stats.ts b/frontend/src/ts/test/test-stats.ts
index 44fb8dcbd..b69cc6b97 100644
--- a/frontend/src/ts/test/test-stats.ts
+++ b/frontend/src/ts/test/test-stats.ts
@@ -3,13 +3,13 @@ import Config from "../config";
import * as Strings from "../utils/strings";
import * as TestInput from "./test-input";
import * as TestWords from "./test-words";
-import * as FunboxList from "./funbox/funbox-list";
import * as TestState from "./test-state";
import * as Numbers from "@monkeytype/util/numbers";
import {
CompletedEvent,
IncompleteTest,
} from "@monkeytype/contracts/schemas/results";
+import { getActiveFunboxes } from "./funbox/list";
type CharCount = {
spaces: number;
@@ -350,9 +350,7 @@ function countChars(): CharCount {
spaces++;
}
}
- if (
- FunboxList.get(Config.funbox).find((f) => f.properties?.includes("nospace"))
- ) {
+ if (getActiveFunboxes().find((f) => f.properties?.includes("nospace"))) {
spaces = 0;
correctspaces = 0;
}
diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts
index 25a0f9ee0..542dd3afb 100644
--- a/frontend/src/ts/test/test-ui.ts
+++ b/frontend/src/ts/test/test-ui.ts
@@ -19,7 +19,6 @@ import * as ConfigEvent from "../observables/config-event";
import * as Hangul from "hangul-js";
import { format } from "date-fns/format";
import { isAuthenticated } from "../firebase";
-import * as FunboxList from "./funbox/funbox-list";
import { debounce } from "throttle-debounce";
import * as ResultWordHighlight from "../elements/result-word-highlight";
import * as ActivePage from "../states/active-page";
@@ -31,6 +30,7 @@ import {
TimerOpacity,
} from "@monkeytype/contracts/schemas/configs";
import { convertRemToPixels } from "../utils/numbers";
+import { getActiveFunboxes } from "./funbox/list";
async function gethtml2canvas(): Promise<typeof import("html2canvas").default> {
return (await import("html2canvas")).default;
@@ -330,9 +330,8 @@ export async function updateHintsPosition(): Promise<void> {
function getWordHTML(word: string): string {
let newlineafter = false;
let retval = `<div class='word'>`;
- const funbox = FunboxList.get(Config.funbox).find(
- (f) => f.functions?.getWordHtml
- );
+
+ const funbox = getActiveFunboxes().find((f) => f.functions?.getWordHtml);
const chars = Strings.splitIntoCharacters(word);
for (const char of chars) {
if (funbox?.functions?.getWordHtml) {
@@ -648,9 +647,9 @@ export async function screenshot(): Promise<void> {
}
(document.querySelector("html") as HTMLElement).style.scrollBehavior =
"smooth";
- FunboxList.get(Config.funbox).forEach((f) =>
- f.functions?.applyGlobalCSS?.()
- );
+ for (const fb of getActiveFunboxes()) {
+ fb.functions?.applyGlobalCSS?.();
+ }
}
if (!$("#resultReplay").hasClass("hidden")) {
@@ -690,7 +689,9 @@ export async function screenshot(): Promise<void> {
$(".highlightContainer").addClass("hidden");
if (revertCookie) $("#cookiesModal").addClass("hidden");
- FunboxList.get(Config.funbox).forEach((f) => f.functions?.clearGlobal?.());
+ for (const fb of getActiveFunboxes()) {
+ fb.functions?.clearGlobal?.();
+ }
(document.querySelector("html") as HTMLElement).style.scrollBehavior = "auto";
window.scrollTo({
@@ -837,9 +838,7 @@ export async function updateActiveWordLetters(
}
}
- const funbox = FunboxList.get(Config.funbox).find(
- (f) => f.functions?.getWordHtml
- );
+ const funbox = getActiveFunboxes().find((fb) => fb.functions?.getWordHtml);
const inputChars = Strings.splitIntoCharacters(input);
const currentWordChars = Strings.splitIntoCharacters(currentWord);
@@ -850,7 +849,7 @@ export async function updateActiveWordLetters(
let tabChar = "";
let nlChar = "";
if (funbox?.functions?.getWordHtml) {
- const cl = funbox.functions.getWordHtml(currentLetter);
+ const cl = funbox.functions?.getWordHtml(currentLetter);
if (cl !== "") {
currentLetter = cl;
}
diff --git a/frontend/src/ts/test/words-generator.ts b/frontend/src/ts/test/words-generator.ts
index 0b7237800..7dabe7ef3 100644
--- a/frontend/src/ts/test/words-generator.ts
+++ b/frontend/src/ts/test/words-generator.ts
@@ -1,5 +1,4 @@
import Config, * as UpdateConfig from "../config";
-import * as FunboxList from "./funbox/funbox-list";
import * as CustomText from "./custom-text";
import * as Wordset from "./wordset";
import QuotesController, {
@@ -17,6 +16,7 @@ import * as Arrays from "../utils/arrays";
import * as TestState from "../test/test-state";
import * as GetText from "../utils/generate";
import { FunboxWordOrder, LanguageObject } from "../utils/json-data";
+import { getActiveFunboxes } from "./funbox/list";
function shouldCapitalize(lastChar: string): boolean {
return /[?!.؟]/.test(lastChar);
@@ -35,9 +35,8 @@ export async function punctuateWord(
const lastChar = Strings.getLastChar(previousWord);
- const funbox = FunboxList.get(Config.funbox).find(
- (f) => f.functions?.punctuateWord
- );
+ const funbox = getActiveFunboxes()?.find((fb) => fb.functions?.punctuateWord);
+
if (funbox?.functions?.punctuateWord) {
return funbox.functions.punctuateWord(word);
}
@@ -302,25 +301,25 @@ async function applyEnglishPunctuationToWord(word: string): Promise<string> {
}
function getFunboxWordsFrequency(): Wordset.FunboxWordsFrequency | undefined {
- const wordFunbox = FunboxList.get(Config.funbox).find(
- (f) => f.functions?.getWordsFrequencyMode
+ const funbox = getActiveFunboxes().find(
+ (fb) => fb.functions?.getWordsFrequencyMode
);
- if (wordFunbox?.functions?.getWordsFrequencyMode) {
- return wordFunbox.functions.getWordsFrequencyMode();
+ if (funbox?.functions?.getWordsFrequencyMode) {
+ return funbox.functions.getWordsFrequencyMode();
}
return undefined;
}
async function getFunboxSection(): Promise<string[]> {
const ret = [];
- const sectionFunbox = FunboxList.get(Config.funbox).find(
- (f) => f.functions?.pullSection
- );
- if (sectionFunbox?.functions?.pullSection) {
- const section = await sectionFunbox.functions.pullSection(Config.language);
+
+ const funbox = getActiveFunboxes().find((fb) => fb.functions?.pullSection);
+
+ if (funbox?.functions?.pullSection) {
+ const section = await funbox.functions.pullSection(Config.language);
if (section === false || section === undefined) {
- UpdateConfig.toggleFunbox(sectionFunbox.name);
+ UpdateConfig.toggleFunbox(funbox.name);
throw new Error("Failed to pull section");
}
@@ -339,19 +338,18 @@ function getFunboxWord(
wordIndex: number,
wordset?: Wordset.Wordset
): string {
- const wordFunbox = FunboxList.get(Config.funbox).find(
- (f) => f.functions?.getWord
- );
- if (wordFunbox?.functions?.getWord) {
- word = wordFunbox.functions.getWord(wordset, wordIndex);
+ const funbox = getActiveFunboxes()?.find((fb) => fb.functions?.getWord);
+
+ if (funbox?.functions?.getWord) {
+ word = funbox.functions.getWord(wordset, wordIndex);
}
return word;
}
function applyFunboxesToWord(word: string): string {
- for (const f of FunboxList.get(Config.funbox)) {
- if (f.functions?.alterText) {
- word = f.functions.alterText(word);
+ for (const fb of getActiveFunboxes()) {
+ if (fb.functions?.alterText) {
+ word = fb.functions.alterText(word);
}
}
return word;
@@ -384,7 +382,7 @@ function applyLazyModeToWord(word: string, language: LanguageObject): string {
export function getWordOrder(): FunboxWordOrder {
const wordOrder =
- FunboxList.get(Config.funbox)
+ getActiveFunboxes()
.find((f) => f.properties?.find((fp) => fp.startsWith("wordOrder")))
?.properties?.find((fp) => fp.startsWith("wordOrder")) ?? "";
@@ -409,7 +407,7 @@ export function getWordsLimit(): number {
}
const funboxToPush =
- FunboxList.get(Config.funbox)
+ getActiveFunboxes()
.find((f) => f.properties?.find((fp) => fp.startsWith("toPush")))
?.properties?.find((fp) => fp.startsWith("toPush:")) ?? "";
@@ -607,8 +605,8 @@ export async function generateWords(
hasNewline: false,
};
- const sectionFunbox = FunboxList.get(Config.funbox).find(
- (f) => f.functions?.pullSection
+ const sectionFunbox = getActiveFunboxes().find(
+ (fb) => fb.functions?.pullSection
);
isCurrentlyUsingFunboxSection =
sectionFunbox?.functions?.pullSection !== undefined;
@@ -632,11 +630,9 @@ export async function generateWords(
wordList = wordList.reverse();
}
- const wordFunbox = FunboxList.get(Config.funbox).find(
- (f) => f.functions?.withWords
- );
- if (wordFunbox?.functions?.withWords) {
- currentWordset = await wordFunbox.functions.withWords(wordList);
+ const funbox = getActiveFunboxes().find((fb) => fb.functions?.withWords);
+ if (funbox?.functions?.withWords) {
+ currentWordset = await funbox.functions.withWords(wordList);
} else {
currentWordset = await Wordset.withWords(wordList);
}
diff --git a/frontend/src/ts/utils/json-data.ts b/frontend/src/ts/utils/json-data.ts
index 93580bf61..ef51dce6b 100644
--- a/frontend/src/ts/utils/json-data.ts
+++ b/frontend/src/ts/utils/json-data.ts
@@ -1,7 +1,5 @@
-import { ConfigValue } from "@monkeytype/contracts/schemas/configs";
import { Accents } from "../test/lazy-mode";
import { hexToHSL } from "./colors";
-import { FunboxWordsFrequency, Wordset } from "../test/wordset";
/**
* Fetches JSON data from the specified URL using the fetch API.
@@ -276,29 +274,6 @@ export async function getCurrentGroup(
return retgroup;
}
-let funboxList: FunboxMetadata[] | undefined;
-
-/**
- * Fetches the list of funbox metadata from the server.
- * @returns A promise that resolves to the list of funbox metadata.
- */
-export async function getFunboxList(): Promise<FunboxMetadata[]> {
- if (!funboxList) {
- let list = await cachedFetchJson<FunboxMetadata[]>("/funbox/_list.json");
- list = list.sort((a, b) => {
- const nameA = a.name.toLowerCase();
- const nameB = b.name.toLowerCase();
- if (nameA < nameB) return -1;
- if (nameA > nameB) return 1;
- return 0;
- });
- funboxList = list;
- return funboxList;
- } else {
- return funboxList;
- }
-}
-
export class Section {
public title: string;
public author: string;
@@ -310,81 +285,8 @@ export class Section {
}
}
-export type FunboxMetadata = {
- name: string;
- info: string;
- canGetPb?: boolean;
- alias?: string;
- forcedConfig?: FunboxForcedConfig;
- properties?: FunboxProperty[];
- functions?: FunboxFunctions;
- hasCSS?: boolean;
-};
-
export type FunboxWordOrder = "normal" | "reverse";
-type FunboxProperty =
- | "symmetricChars"
- | "conflictsWithSymmetricChars"
- | "changesWordsVisibility"
- | "speaks"
- | "unspeakable"
- | "changesLayout"
- | "ignoresLayout"
- | "usesLayout"
- | "ignoresLanguage"
- | "noLigatures"
- | "noLetters"
- | "changesCapitalisation"
- | "nospace"
- | `toPush:${number}`
- | "noInfiniteDuration"
- | "changesWordsFrequency"
- | `wordOrder:${FunboxWordOrder}`;
-
-export type FunboxForcedConfig = Record<string, ConfigValue[]>;
-
-export type FunboxFunctions = {
- getWord?: (wordset?: Wordset, wordIndex?: number) => string;
- punctuateWord?: (word: string) => string;
- withWords?: (words?: string[]) => Promise<Wordset>;
- alterText?: (word: string) => string;
- applyConfig?: () => void;
- applyGlobalCSS?: () => void;
- clearGlobal?: () => void;
- rememberSettings?: () => void;
- toggleScript?: (params: string[]) => void;
- pullSection?: (language?: string) => Promise<Section | false>;
- handleSpace?: () => void;
- handleChar?: (char: string) => string;
- isCharCorrect?: (char: string, originalChar: string) => boolean;
- preventDefaultEvent?: (
- event: JQuery.KeyDownEvent<Document, null, Document, Document>
- ) => Promise<boolean>;
- handleKeydown?: (
- event: JQuery.KeyDownEvent<Document, undefined, Document, Document>
- ) => Promise<void>;
- getResultContent?: () => string;
- start?: () => void;
- restart?: () => void;
- getWordHtml?: (char: string, letterTag?: boolean) => string;
- getWordsFrequencyMode?: () => FunboxWordsFrequency;
-};
-
-/**
- * Fetches the funbox metadata for a given funbox from the server.
- * @param funbox The name of the funbox.
- * @returns A promise that resolves to the funbox metadata.
- */
-export async function getFunbox(
- funbox: string
-): Promise<FunboxMetadata | undefined> {
- const list: FunboxMetadata[] = await getFunboxList();
- return list.find(function (element) {
- return element.name === funbox;
- });
-}
-
export type FontObject = {
name: string;
display?: string;
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<FunboxName, FunboxMetadata> {
+ return getObject();
+}
diff --git a/backend/src/constants/funbox-list.ts b/packages/funbox/src/list.ts
index 6ee0bb06a..06a5055f5 100644
--- a/backend/src/constants/funbox-list.ts
+++ b/packages/funbox/src/list.ts
@@ -1,14 +1,8 @@
-export type FunboxMetadata = {
- name: string;
- canGetPb: boolean;
- difficultyLevel: number;
- properties?: string[];
- frontendForcedConfig?: Record<string, string[] | boolean[]>;
- frontendFunctions?: string[];
-};
+import { FunboxMetadata, FunboxName } from "./types";
-const FunboxList: FunboxMetadata[] = [
- {
+const list: Record<FunboxName, FunboxMetadata> = {
+ "58008": {
+ description: "A special mode for accountants.",
canGetPb: false,
difficultyLevel: 1,
properties: ["ignoresLanguage", "ignoresLayout", "noLetters"],
@@ -22,64 +16,69 @@ const FunboxList: FunboxMetadata[] = [
"handleChar",
],
name: "58008",
+ alias: "numbers",
},
- {
+ mirror: {
+ name: "mirror",
+ description: "Everything is mirrored!",
+ properties: ["hasCssFile"],
canGetPb: true,
- difficultyLevel: 2,
- frontendFunctions: ["applyCSS"],
- name: "nausea",
+ difficultyLevel: 3,
},
- {
+ upside_down: {
+ name: "upside_down",
+ description: "Everything is upside down!",
+ properties: ["hasCssFile"],
canGetPb: true,
difficultyLevel: 3,
- frontendFunctions: ["applyCSS"],
- name: "round_round_baby",
},
- {
+ nausea: {
+ name: "nausea",
+ description: "I think I'm gonna be sick.",
canGetPb: true,
- difficultyLevel: 1,
- properties: ["changesWordsVisibility", "usesLayout"],
- frontendForcedConfig: {
- highlightMode: ["letter", "off"],
- },
- frontendFunctions: ["applyCSS", "applyConfig", "rememberSettings"],
- name: "simon_says",
+ 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,
- frontendFunctions: ["applyCSS"],
- name: "mirror",
+ properties: ["hasCssFile"],
},
- {
+ simon_says: {
+ name: "simon_says",
+ description: "Type what simon says.",
canGetPb: true,
- difficultyLevel: 3,
- frontendFunctions: ["applyCSS"],
- name: "upside_down",
+ difficultyLevel: 1,
+ properties: ["hasCssFile", "changesWordsVisibility", "usesLayout"],
+ frontendForcedConfig: {
+ highlightMode: ["letter", "off"],
+ },
+ frontendFunctions: ["applyConfig", "rememberSettings"],
},
- {
+
+ tts: {
canGetPb: true,
difficultyLevel: 1,
- properties: ["changesWordsVisibility", "speaks"],
+ properties: ["hasCssFile", "changesWordsVisibility", "speaks"],
frontendForcedConfig: {
highlightMode: ["letter", "off"],
},
- frontendFunctions: [
- "applyCSS",
- "applyConfig",
- "rememberSettings",
- "toggleScript",
- ],
+ frontendFunctions: ["applyConfig", "rememberSettings", "toggleScript"],
name: "tts",
+ description: "Listen closely.",
},
- {
+ choo_choo: {
canGetPb: true,
difficultyLevel: 2,
- properties: ["noLigatures", "conflictsWithSymmetricChars"],
- frontendFunctions: ["applyCSS"],
+ 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: [
@@ -96,7 +95,6 @@ const FunboxList: FunboxMetadata[] = [
},
frontendFunctions: [
"getWord",
- "applyConfig",
"rememberSettings",
"handleChar",
"isCharCorrect",
@@ -105,21 +103,25 @@ const FunboxList: FunboxMetadata[] = [
],
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"],
@@ -132,27 +134,30 @@ const FunboxList: FunboxMetadata[] = [
],
name: "layoutfluid",
},
- {
+ earthquake: {
+ description: "Everybody get down! The words are shaking!",
canGetPb: true,
difficultyLevel: 1,
- properties: ["noLigatures"],
- frontendFunctions: ["applyCSS"],
+ properties: ["hasCssFile", "noLigatures"],
name: "earthquake",
},
- {
+ space_balls: {
+ description: "In a galaxy far far away.",
canGetPb: true,
difficultyLevel: 0,
- frontendFunctions: ["applyCSS"],
+ 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"],
@@ -163,7 +168,8 @@ const FunboxList: FunboxMetadata[] = [
frontendFunctions: ["getWord"],
name: "ascii",
},
- {
+ specials: {
+ description: "!@#$%^&*. Only special characters.",
canGetPb: false,
difficultyLevel: 1,
properties: ["ignoresLanguage", "noLetters", "unspeakable"],
@@ -174,61 +180,69 @@ const FunboxList: FunboxMetadata[] = [
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"],
+ properties: ["changesWordsVisibility", "hasCssFile"],
frontendForcedConfig: {
highlightMode: ["letter", "off"],
},
- frontendFunctions: ["applyCSS", "rememberSettings", "handleKeydown"],
+ frontendFunctions: ["rememberSettings", "handleKeydown"],
name: "read_ahead_easy",
},
- {
+ read_ahead: {
+ description: "Current and the next word are invisible!",
canGetPb: true,
difficultyLevel: 2,
- properties: ["changesWordsVisibility"],
+ properties: ["changesWordsVisibility", "hasCssFile"],
frontendForcedConfig: {
highlightMode: ["letter", "off"],
},
- frontendFunctions: ["applyCSS", "rememberSettings", "handleKeydown"],
+ frontendFunctions: ["rememberSettings", "handleKeydown"],
name: "read_ahead",
},
- {
+ read_ahead_hard: {
+ description: "Current and the next two words are invisible!",
canGetPb: true,
difficultyLevel: 3,
- properties: ["changesWordsVisibility"],
+ properties: ["changesWordsVisibility", "hasCssFile"],
frontendForcedConfig: {
highlightMode: ["letter", "off"],
},
- frontendFunctions: ["applyCSS", "rememberSettings", "handleKeydown"],
+ 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"],
@@ -238,17 +252,19 @@ const FunboxList: FunboxMetadata[] = [
frontendFunctions: ["applyConfig", "rememberSettings", "start", "restart"],
name: "memory",
},
- {
+ nospace: {
+ description: "Whoneedsspacesanyway?",
canGetPb: false,
difficultyLevel: 0,
properties: ["nospace"],
frontendForcedConfig: {
highlightMode: ["letter", "off"],
},
- frontendFunctions: ["applyConfig", "rememberSettings"],
+ frontendFunctions: ["rememberSettings"],
name: "nospace",
},
- {
+ poetry: {
+ description: "Practice typing some beautiful prose.",
canGetPb: false,
difficultyLevel: 0,
properties: ["noInfiniteDuration", "ignoresLanguage"],
@@ -259,7 +275,8 @@ const FunboxList: FunboxMetadata[] = [
frontendFunctions: ["pullSection"],
name: "poetry",
},
- {
+ wikipedia: {
+ description: "Practice typing wikipedia sections.",
canGetPb: false,
difficultyLevel: 0,
properties: ["noInfiniteDuration", "ignoresLanguage"],
@@ -270,21 +287,25 @@ const FunboxList: FunboxMetadata[] = [
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"],
@@ -294,7 +315,9 @@ const FunboxList: FunboxMetadata[] = [
frontendFunctions: ["getWord", "punctuateWord", "rememberSettings"],
name: "IPv4",
},
- {
+ IPv6: {
+ alias: "network",
+ description: "For sysadmins with a long beard.",
canGetPb: false,
difficultyLevel: 1,
properties: ["ignoresLanguage", "ignoresLayout", "noLetters"],
@@ -304,7 +327,9 @@ const FunboxList: FunboxMetadata[] = [
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"],
@@ -315,7 +340,9 @@ const FunboxList: FunboxMetadata[] = [
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"],
@@ -325,51 +352,99 @@ const FunboxList: FunboxMetadata[] = [
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"],
+ properties: ["ignoresLanguage", "ignoresLayout", "noLetters", "nospace"],
frontendFunctions: ["alterText"],
name: "morse",
},
- {
+ crt: {
+ description: "Go back to the 1980s",
canGetPb: true,
difficultyLevel: 0,
- properties: ["noLigatures"],
+ properties: ["hasCssFile", "noLigatures"],
+ frontendFunctions: ["applyGlobalCSS", "clearGlobal"],
name: "crt",
},
- {
+ backwards: {
+ description: "...sdrawkcab epyt ot yrt woN",
name: "backwards",
properties: [
+ "hasCssFile",
"noLigatures",
"conflictsWithSymmetricChars",
"wordOrder:reverse",
],
- frontendFunctions: ["applyCSS"],
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<FunboxName, FunboxMetadata> {
+ return list;
+}
+
+export function getList(): FunboxMetadata[] {
+ const out: FunboxMetadata[] = [];
+ for (const name of getFunboxNames()) {
+ out.push(list[name]);
+ }
+ return out;
+}
-export default FunboxList;
+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<string, string[] | boolean[]>;
+
+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<string | boolean>(
+ 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([email protected])
+ rimraf:
+ specifier: 6.0.1
+ version: 6.0.1
+ typescript:
+ specifier: 5.5.4
+ version: 5.5.4
+ vitest:
+ specifier: 2.0.5
+
packages/release:
dependencies:
'@octokit/rest':
@@ -1302,12 +1341,6 @@ packages:
resolution: {integrity: sha512-9Z0sGuXqf6En19qmwB0Syi1Mc8TYl756dNuuaYal9mrypKa0Jq/IX6aJfh6Rk2S3z66KBisWTqloDo7weYj4zg==}
engines: {node: '>=4'}
- '@esbuild/[email protected]':
- resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==}
- engines: {node: '>=12'}
- cpu: [ppc64]
- os: [aix]
-
'@esbuild/[email protected]':
resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==}
engines: {node: '>=12'}
@@ -1326,12 +1359,6 @@ packages:
cpu: [ppc64]
os: [aix]
- '@esbuild/[email protected]':
- resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==}
- engines: {node: '>=12'}
- cpu: [arm64]
- os: [android]
-
'@esbuild/[email protected]':
resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==}
engines: {node: '>=12'}
@@ -1350,12 +1377,6 @@ packages:
cpu: [arm64]
os: [android]
- '@esbuild/[email protected]':
- resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==}
- engines: {node: '>=12'}
- cpu: [arm]
- os: [android]
-
'@esbuild/[email protected]':
resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==}
engines: {node: '>=12'}
@@ -1374,12 +1395,6 @@ packages:
cpu: [arm]
os: [android]
- '@esbuild/[email protected]':
- resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [android]
-
'@esbuild/[email protected]':
resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==}
engines: {node: '>=12'}
@@ -1398,12 +1413,6 @@ packages:
cpu: [x64]
os: [android]
- '@esbuild/[email protected]':
- resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==}
- engines: {node: '>=12'}
- cpu: [arm64]
- os: [darwin]
-
'@esbuild/[email protected]':
resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==}
engines: {node: '>=12'}
@@ -1422,12 +1431,6 @@ packages:
cpu: [arm64]
os: [darwin]
- '@esbuild/[email protected]':
- resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [darwin]
-
'@esbuild/[email protected]':
resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==}
engines: {node: '>=12'}
@@ -1446,12 +1449,6 @@ packages:
cpu: [x64]
os: [darwin]
- '@esbuild/[email protected]':
- resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==}
- engines: {node: '>=12'}
- cpu: [arm64]
- os: [freebsd]
-
'@esbuild/[email protected]':
resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==}
engines: {node: '>=12'}
@@ -1470,12 +1467,6 @@ packages:
cpu: [arm64]
os: [freebsd]
- '@esbuild/[email protected]':
- resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [freebsd]
-
'@esbuild/[email protected]':
resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==}
engines: {node: '>=12'}
@@ -1494,12 +1485,6 @@ packages:
cpu: [x64]
os: [freebsd]
- '@esbuild/[email protected]':
- resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==}
- engines: {node: '>=12'}
- cpu: [arm64]
- os: [linux]
-
'@esbuild/[email protected]':
resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==}
engines: {node: '>=12'}
@@ -1518,12 +1503,6 @@ packages:
cpu: [arm64]
os: [linux]
- '@esbuild/[email protected]':
- resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==}
- engines: {node: '>=12'}
- cpu: [arm]
- os: [linux]
-
'@esbuild/[email protected]':
resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==}
engines: {node: '>=12'}
@@ -1542,12 +1521,6 @@ packages:
cpu: [arm]
os: [linux]
- '@esbuild/[email protected]':
- resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==}
- engines: {node: '>=12'}
- cpu: [ia32]
- os: [linux]
-
'@esbuild/[email protected]':
resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==}
engines: {node: '>=12'}
@@ -1566,12 +1539,6 @@ packages:
cpu: [ia32]
os: [linux]
- '@esbuild/[email protected]':
- resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==}
- engines: {node: '>=12'}
- cpu: [loong64]
- os: [linux]
-
'@esbuild/[email protected]':
resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==}
engines: {node: '>=12'}
@@ -1590,12 +1557,6 @@ packages:
cpu: [loong64]
os: [linux]
- '@esbuild/[email protected]':
- resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==}
- engines: {node: '>=12'}
- cpu: [mips64el]
- os: [linux]
-
'@esbuild/[email protected]':
resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==}
engines: {node: '>=12'}
@@ -1614,12 +1575,6 @@ packages:
cpu: [mips64el]
os: [linux]
- '@esbuild/[email protected]':
- resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==}
- engines: {node: '>=12'}
- cpu: [ppc64]
- os: [linux]
-
'@esbuild/[email protected]':
resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==}
engines: {node: '>=12'}
@@ -1638,12 +1593,6 @@ packages:
cpu: [ppc64]
os: [linux]
- '@esbuild/[email protected]':
- resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==}
- engines: {node: '>=12'}
- cpu: [riscv64]
- os: [linux]
-
'@esbuild/[email protected]':
resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==}
engines: {node: '>=12'}
@@ -1662,12 +1611,6 @@ packages:
cpu: [riscv64]
os: [linux]
- '@esbuild/[email protected]':
- resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==}
- engines: {node: '>=12'}
- cpu: [s390x]
- os: [linux]
-
'@esbuild/[email protected]':
resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==}
engines: {node: '>=12'}
@@ -1686,12 +1629,6 @@ packages:
cpu: [s390x]
os: [linux]
- '@esbuild/[email protected]':
- resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [linux]
-
'@esbuild/[email protected]':
resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==}
engines: {node: '>=12'}
@@ -1710,12 +1647,6 @@ packages:
cpu: [x64]
os: [linux]
- '@esbuild/[email protected]':
- resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [netbsd]
-
'@esbuild/[email protected]':
resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==}
engines: {node: '>=12'}
@@ -1740,12 +1671,6 @@ packages:
cpu: [arm64]
os: [openbsd]
- '@esbuild/[email protected]':
- resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [openbsd]
-
'@esbuild/[email protected]':
resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==}
engines: {node: '>=12'}
@@ -1764,12 +1689,6 @@ packages:
cpu: [x64]
os: [openbsd]
- '@esbuild/[email protected]':
- resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [sunos]
-
'@esbuild/[email protected]':
resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==}
engines: {node: '>=12'}
@@ -1788,12 +1707,6 @@ packages:
cpu: [x64]
os: [sunos]
- '@esbuild/[email protected]':
- resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==}
- engines: {node: '>=12'}
- cpu: [arm64]
- os: [win32]
-
'@esbuild/[email protected]':
resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==}
engines: {node: '>=12'}
@@ -1812,12 +1725,6 @@ packages:
cpu: [arm64]
os: [win32]
- '@esbuild/[email protected]':
- resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==}
- engines: {node: '>=12'}
- cpu: [ia32]
- os: [win32]
-
'@esbuild/[email protected]':
resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==}
engines: {node: '>=12'}
@@ -1836,12 +1743,6 @@ packages:
cpu: [ia32]
os: [win32]
- '@esbuild/[email protected]':
- resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [win32]
-
'@esbuild/[email protected]':
resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==}
engines: {node: '>=12'}
@@ -4545,11 +4446,6 @@ packages:
resolution: {integrity: sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==}
- resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==}
- engines: {node: '>=12'}
- hasBin: true
-
resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==}
engines: {node: '>=12'}
@@ -9286,34 +9182,6 @@ packages:
'@vite-pwa/assets-generator':
optional: true
- 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
-
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/[email protected]':
- optional: true
-
'@esbuild/[email protected]':
optional: true
@@ -10677,9 +10542,6 @@ snapshots:
'@esbuild/[email protected]':
optional: true
- '@esbuild/[email protected]':
- optional: true
-
'@esbuild/[email protected]':
optional: true
@@ -10689,9 +10551,6 @@ snapshots:
'@esbuild/[email protected]':
optional: true
- '@esbuild/[email protected]':
- optional: true
-
'@esbuild/[email protected]':
optional: true
@@ -10701,9 +10560,6 @@ snapshots:
'@esbuild/[email protected]':
optional: true
- '@esbuild/[email protected]':
- optional: true
-
'@esbuild/[email protected]':
optional: true
@@ -10713,9 +10569,6 @@ snapshots:
'@esbuild/[email protected]':
optional: true
- '@esbuild/[email protected]':
- optional: true
-
'@esbuild/[email protected]':
optional: true
@@ -10725,9 +10578,6 @@ snapshots:
'@esbuild/[email protected]':
optional: true
- '@esbuild/[email protected]':
- optional: true
-
'@esbuild/[email protected]':
optional: true
@@ -10737,9 +10587,6 @@ snapshots:
'@esbuild/[email protected]':
optional: true
- '@esbuild/[email protected]':
- optional: true
-
'@esbuild/[email protected]':
optional: true
@@ -10749,9 +10596,6 @@ snapshots:
'@esbuild/[email protected]':
optional: true
- '@esbuild/[email protected]':
- optional: true
-
'@esbuild/[email protected]':
optional: true
@@ -10761,9 +10605,6 @@ snapshots:
'@esbuild/[email protected]':
optional: true
- '@esbuild/[email protected]':
- optional: true
-
'@esbuild/[email protected]':
optional: true
@@ -10773,9 +10614,6 @@ snapshots:
'@esbuild/[email protected]':
optional: true
- '@esbuild/[email protected]':
- optional: true
-
'@esbuild/[email protected]':
optional: true
@@ -10785,9 +10623,6 @@ snapshots:
'@esbuild/[email protected]':
optional: true
- '@esbuild/[email protected]':
- optional: true
-
'@esbuild/[email protected]':
optional: true
@@ -10797,9 +10632,6 @@ snapshots:
'@esbuild/[email protected]':
optional: true
- '@esbuild/[email protected]':
- optional: true
-
'@esbuild/[email protected]':
optional: true
@@ -10809,9 +10641,6 @@ snapshots:
'@esbuild/[email protected]':
optional: true
- '@esbuild/[email protected]':
- optional: true
-
'@esbuild/[email protected]':
optional: true
@@ -10821,9 +10650,6 @@ snapshots:
'@esbuild/[email protected]':
optional: true
- '@esbuild/[email protected]':
- optional: true
-
'@esbuild/[email protected]':
optional: true
@@ -10833,9 +10659,6 @@ snapshots:
'@esbuild/[email protected]':
optional: true
- '@esbuild/[email protected]':
- optional: true
-
'@esbuild/[email protected]':
optional: true
@@ -10845,9 +10668,6 @@ snapshots:
'@esbuild/[email protected]':
optional: true
- '@esbuild/[email protected]':
- optional: true
-
'@esbuild/[email protected]':
optional: true
@@ -10857,9 +10677,6 @@ snapshots:
'@esbuild/[email protected]':
optional: true
- '@esbuild/[email protected]':
- optional: true
-
'@esbuild/[email protected]':
optional: true
@@ -10869,9 +10686,6 @@ snapshots:
'@esbuild/[email protected]':
optional: true
- '@esbuild/[email protected]':
- optional: true
-
'@esbuild/[email protected]':
optional: true
@@ -10884,9 +10698,6 @@ snapshots:
'@esbuild/[email protected]':
optional: true
- '@esbuild/[email protected]':
- optional: true
-
'@esbuild/[email protected]':
optional: true
@@ -10896,9 +10707,6 @@ snapshots:
'@esbuild/[email protected]':
optional: true
- '@esbuild/[email protected]':
- optional: true
-
'@esbuild/[email protected]':
optional: true
@@ -10908,9 +10716,6 @@ snapshots:
'@esbuild/[email protected]':
optional: true
- '@esbuild/[email protected]':
- optional: true
-
'@esbuild/[email protected]':
optional: true
@@ -10920,9 +10725,6 @@ snapshots:
'@esbuild/[email protected]':
optional: true
- '@esbuild/[email protected]':
- optional: true
-
'@esbuild/[email protected]':
optional: true
@@ -10932,9 +10734,6 @@ snapshots:
'@esbuild/[email protected]':
optional: true
- '@esbuild/[email protected]':
- optional: true
-
'@esbuild/[email protected]':
optional: true
@@ -14185,32 +13984,6 @@ snapshots:
es6-iterator: 2.0.3
es6-symbol: 3.1.4
- 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
-
optionalDependencies:
'@esbuild/aix-ppc64': 0.20.2
@@ -20067,28 +19840,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
- 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
-
- 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
-
dependencies:
esbuild: 0.20.2
@@ -20141,7 +19892,7 @@ snapshots:
tinybench: 2.8.0
tinypool: 1.0.0
tinyrainbow: 1.2.0
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
why-is-node-running: 2.3.0
optionalDependencies: