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