refactor: move funboxes to a shared package (@miodec) (#6063)

This commit is contained in:
Jack 2024-12-04 16:11:07 +01:00 committed by GitHub
parent a75f0d3b30
commit fdadb4ae83
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 1617 additions and 2293 deletions

View file

@ -24,6 +24,7 @@
"dependencies": {
"@date-fns/utc": "1.2.0",
"@monkeytype/contracts": "workspace:*",
"@monkeytype/funbox": "workspace:*",
"@monkeytype/util": "workspace:*",
"@ts-rest/core": "3.51.0",
"@ts-rest/express": "3.51.0",

View file

@ -6,7 +6,7 @@ import Logger from "../../utils/logger";
import "dotenv/config";
import { MonkeyResponse } from "../../utils/monkey-response";
import MonkeyError from "../../utils/error";
import { areFunboxesCompatible, isTestTooShort } from "../../utils/validation";
import { isTestTooShort } from "../../utils/validation";
import {
implemented as anticheatImplemented,
validateResult,
@ -22,7 +22,6 @@ import { getDailyLeaderboard } from "../../utils/daily-leaderboards";
import AutoRoleList from "../../constants/auto-roles";
import * as UserDAL from "../../dal/user";
import { buildMonkeyMail } from "../../utils/monkey-mail";
import FunboxList from "../../constants/funbox-list";
import _, { omit } from "lodash";
import * as WeeklyXpLeaderboard from "../../services/weekly-xp-leaderboard";
import { UAParser } from "ua-parser-js";
@ -57,6 +56,11 @@ import {
getStartOfDayTimestamp,
} from "@monkeytype/util/date-and-time";
import { MonkeyRequest } from "../types";
import {
getFunbox,
checkCompatibility,
stringToFunboxNames,
} from "@monkeytype/funbox";
try {
if (!anticheatImplemented()) throw new Error("undefined");
@ -232,7 +236,9 @@ export async function addResult(
}
}
if (!areFunboxesCompatible(completedEvent.funbox ?? "")) {
const funboxNames = stringToFunboxNames(completedEvent.funbox ?? "");
if (!checkCompatibility(funboxNames)) {
throw new MonkeyError(400, "Impossible funbox combination");
}
@ -660,7 +666,7 @@ async function calculateXp(
charStats,
punctuation,
numbers,
funbox,
funbox: resultFunboxes,
} = result;
const {
@ -713,12 +719,15 @@ async function calculateXp(
}
}
if (funboxBonusConfiguration > 0) {
const funboxModifier = _.sumBy(funbox.split("#"), (funboxName) => {
const funbox = FunboxList.find((f) => f.name === funboxName);
const difficultyLevel = funbox?.difficultyLevel ?? 0;
return Math.max(difficultyLevel * funboxBonusConfiguration, 0);
});
if (funboxBonusConfiguration > 0 && resultFunboxes !== "none") {
const funboxModifier = _.sumBy(
stringToFunboxNames(resultFunboxes),
(funboxName) => {
const funbox = getFunbox(funboxName);
const difficultyLevel = funbox?.difficultyLevel ?? 0;
return Math.max(difficultyLevel * funboxBonusConfiguration, 0);
}
);
if (funboxModifier > 0) {
modifier += funboxModifier;
breakdown.funbox = Math.round(baseXp * funboxModifier);

View file

@ -1,12 +1,11 @@
import _ from "lodash";
import FunboxList from "../constants/funbox-list";
import {
Mode,
PersonalBest,
PersonalBests,
} from "@monkeytype/contracts/schemas/shared";
import { Result as ResultType } from "@monkeytype/contracts/schemas/results";
import { getFunboxesFromString } from "@monkeytype/funbox";
export type LbPersonalBests = {
time: Record<number, Record<string, PersonalBest>>;
@ -21,20 +20,16 @@ type CheckAndUpdatePbResult = {
type Result = Omit<ResultType<Mode>, "_id" | "name">;
export function canFunboxGetPb(result: Result): boolean {
const funbox = result.funbox;
if (funbox === undefined || funbox === "" || funbox === "none") return true;
let ret = true;
const resultFunboxes = funbox.split("#");
for (const funbox of FunboxList) {
if (resultFunboxes.includes(funbox.name)) {
if (!funbox.canGetPb) {
ret = false;
}
}
const funboxString = result.funbox;
if (
funboxString === undefined ||
funboxString === "" ||
funboxString === "none"
) {
return true;
}
return ret;
return getFunboxesFromString(funboxString).every((f) => f.canGetPb);
}
export function checkAndUpdatePb(

View file

@ -1,7 +1,5 @@
import _ from "lodash";
import { default as FunboxList } from "../constants/funbox-list";
import { CompletedEvent } from "@monkeytype/contracts/schemas/results";
import { intersect } from "@monkeytype/util/arrays";
export function isTestTooShort(result: CompletedEvent): boolean {
const { mode, mode2, customText, testDuration, bailedOut } = result;
@ -48,138 +46,3 @@ export function isTestTooShort(result: CompletedEvent): boolean {
return false;
}
export function areFunboxesCompatible(funboxesString: string): boolean {
const funboxes = funboxesString.split("#").filter((f) => f !== "none");
const funboxesToCheck = FunboxList.filter((f) => funboxes.includes(f.name));
const allFunboxesAreValid = funboxesToCheck.length === funboxes.length;
const oneWordModifierMax =
funboxesToCheck.filter(
(f) =>
f.frontendFunctions?.includes("getWord") ??
f.frontendFunctions?.includes("pullSection") ??
f.frontendFunctions?.includes("withWords")
).length <= 1;
const layoutUsability =
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "changesLayout")
).length === 0 ||
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "ignoresLayout" || fp === "usesLayout")
).length === 0;
const oneNospaceOrToPushMax =
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "nospace" || fp.startsWith("toPush"))
).length <= 1;
const oneWordOrderMax =
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp.startsWith("wordOrder"))
).length <= 1;
const oneChangesWordsVisibilityMax =
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "changesWordsVisibility")
).length <= 1;
const oneFrequencyChangesMax =
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "changesWordsFrequency")
).length <= 1;
const noFrequencyChangesConflicts =
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "changesWordsFrequency")
).length === 0 ||
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "ignoresLanguage")
).length === 0;
const capitalisationChangePosibility =
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "noLetters")
).length === 0 ||
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "changesCapitalisation")
).length === 0;
const noConflictsWithSymmetricChars =
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "conflictsWithSymmetricChars")
).length === 0 ||
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "symmetricChars")
).length === 0;
const canSpeak =
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "speaks" || fp === "unspeakable")
).length <= 1;
const hasLanguageToSpeak =
funboxesToCheck.filter((f) => f.properties?.find((fp) => fp === "speaks"))
.length === 0 ||
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "ignoresLanguage")
).length === 0;
const oneToPushOrPullSectionMax =
funboxesToCheck.filter(
(f) =>
f.properties?.some((fp) => fp.startsWith("toPush:")) ??
f.frontendFunctions?.includes("pullSection")
).length <= 1;
// const oneApplyCSSMax =
// funboxesToCheck.filter((f) => f.frontendFunctions?.includes("applyCSS"))
// .length <= 1; //todo: move all funbox stuff to the shared package, this is ok to remove for now
const onePunctuateWordMax =
funboxesToCheck.filter((f) =>
f.frontendFunctions?.includes("punctuateWord")
).length <= 1;
const oneCharCheckerMax =
funboxesToCheck.filter((f) =>
f.frontendFunctions?.includes("isCharCorrect")
).length <= 1;
const oneCharReplacerMax =
funboxesToCheck.filter((f) => f.frontendFunctions?.includes("getWordHtml"))
.length <= 1;
const oneChangesCapitalisationMax =
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "changesCapitalisation")
).length <= 1;
const allowedConfig = {} as Record<string, string[] | boolean[]>;
let noConfigConflicts = true;
for (const f of funboxesToCheck) {
if (!f.frontendForcedConfig) continue;
for (const key in f.frontendForcedConfig) {
const allowedConfigValue = allowedConfig[key];
const funboxValue = f.frontendForcedConfig[key];
if (allowedConfigValue !== undefined && funboxValue !== undefined) {
if (
intersect<string | boolean>(allowedConfigValue, funboxValue, true)
.length === 0
) {
noConfigConflicts = false;
break;
}
} else if (funboxValue !== undefined) {
allowedConfig[key] = funboxValue;
}
}
}
return (
allFunboxesAreValid &&
oneWordModifierMax &&
layoutUsability &&
oneNospaceOrToPushMax &&
oneChangesWordsVisibilityMax &&
oneFrequencyChangesMax &&
noFrequencyChangesConflicts &&
capitalisationChangePosibility &&
noConflictsWithSymmetricChars &&
canSpeak &&
hasLanguageToSpeak &&
oneToPushOrPullSectionMax &&
// oneApplyCSSMax &&
onePunctuateWordMax &&
oneCharCheckerMax &&
oneCharReplacerMax &&
oneChangesCapitalisationMax &&
noConfigConflicts &&
oneWordOrderMax
);
}

View file

@ -332,10 +332,8 @@ describe("Config", () => {
expect(Config.setFavThemes([stringOfLength(51)])).toBe(false);
});
it("setFunbox", () => {
expect(Config.setFunbox("one")).toBe(true);
expect(Config.setFunbox("one#two")).toBe(true);
expect(Config.setFunbox("one#two#")).toBe(true);
expect(Config.setFunbox(stringOfLength(100))).toBe(true);
expect(Config.setFunbox("mirror")).toBe(true);
expect(Config.setFunbox("mirror#58008")).toBe(true);
expect(Config.setFunbox(stringOfLength(101))).toBe(false);
});

View file

@ -0,0 +1,24 @@
import { getAllFunboxes } from "../../src/ts/test/funbox/list";
describe("funbox", () => {
describe("list", () => {
it("should have every frontendFunctions function defined", () => {
for (const funbox of getAllFunboxes()) {
const packageFunctions = (funbox.frontendFunctions ?? []).sort();
const implementations = Object.keys(funbox.functions ?? {}).sort();
let message = "has mismatched functions";
if (packageFunctions.length > implementations.length) {
message = `missing function implementation in frontend`;
} else if (implementations.length > packageFunctions.length) {
message = `missing properties in frontendFunctions in the package`;
}
expect(packageFunctions, `Funbox ${funbox.name} ${message}`).toEqual(
implementations
);
}
});
});
});

View file

@ -9,6 +9,6 @@
"ts-node": {
"files": true
},
"files": ["../src/ts/types/types.d.ts", "vitest.d.ts"],
"files": ["vitest.d.ts"],
"include": ["./**/*.spec.ts", "./setup-tests.ts"]
}

View file

@ -70,6 +70,7 @@
"dependencies": {
"@date-fns/utc": "1.2.0",
"@monkeytype/contracts": "workspace:*",
"@monkeytype/funbox": "workspace:*",
"@monkeytype/util": "workspace:*",
"@ts-rest/core": "3.51.0",
"canvas-confetti": "1.5.1",

View file

@ -46,34 +46,6 @@ function validateOthers() {
return reject(new Error(fontsValidator.errors[0].message));
}
//funbox
const funboxData = JSON.parse(
fs.readFileSync("./static/funbox/_list.json", {
encoding: "utf8",
flag: "r",
})
);
const funboxSchema = {
type: "array",
items: {
type: "object",
properties: {
name: { type: "string" },
info: { type: "string" },
canGetPb: { type: "boolean" },
alias: { type: "string" },
},
required: ["name", "info", "canGetPb"],
},
};
const funboxValidator = ajv.compile(funboxSchema);
if (funboxValidator(funboxData)) {
console.log("Funbox list JSON schema is \u001b[32mvalid\u001b[0m");
} else {
console.log("Funbox list JSON schema is \u001b[31minvalid\u001b[0m");
return reject(new Error(funboxValidator.errors[0].message));
}
//themes
const themesData = JSON.parse(
fs.readFileSync("./static/themes/_list.json", {

View file

@ -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,

View file

@ -1,27 +1,51 @@
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 list: Command[] = [
{
id: "changeFunboxNone",
display: "none",
configValue: "none",
alias: "off",
sticky: true,
exec: (): void => {
ManualRestart.set();
if (Funbox.setFunbox("none")) {
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();
},
});
}
const subgroup: CommandsSubgroup = {
title: "Funbox...",
configKey: "funbox",
list: [
{
id: "changeFunboxNone",
display: "none",
configValue: "none",
alias: "off",
exec: (): void => {
if (Funbox.setFunbox("none")) {
TestLogic.restart();
}
},
},
],
list,
};
const commands: Command[] = [
@ -34,41 +58,4 @@ const commands: Command[] = [
},
];
function update(funboxes: FunboxMetadata[]): void {
subgroup.list = [];
subgroup.list.push({
id: "changeFunboxNone",
display: "none",
configValue: "none",
alias: "off",
sticky: true,
exec: (): void => {
ManualRestart.set();
if (Funbox.setFunbox("none")) {
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();
},
});
}
}
export default commands;
export { update };

View file

@ -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) => {

View file

@ -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);
}
}
}

View file

@ -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;
}

View file

@ -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")
);
let html = "";
html +=
"<select class='funboxSelect' group='funbox' placeholder='select a funbox' multiple>";
html += "<option value='all'>all</option>";
html += "<option value='none'>no funbox</option>";
for (const funbox of getAllFunboxes()) {
html += `<option value="${funbox.name}" filter="${
funbox.name
}">${funbox.name.replace(/_/g, " ")}</option>`;
}
if (funboxList) {
let html = "";
html +=
"<select class='funboxSelect' group='funbox' placeholder='select a funbox' multiple>";
html += "</select>";
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>`;
}
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();

View file

@ -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();
}
);

View file

@ -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")

View file

@ -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>;
}

View file

@ -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;
}

View file

@ -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
);
}

View file

@ -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";
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;
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");
}
}
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
},
});
import { FunboxName, checkCompatibility } from "@monkeytype/funbox";
import { getActiveFunboxes, getActiveFunboxNames } from "./list";
import { checkForcedConfig } from "./funbox-validation";
export function toggleScript(...params: string[]): void {
FunboxList.get(Config.funbox).forEach((funbox) => {
if (funbox.functions?.toggleScript) funbox.functions.toggleScript(params);
});
if (Config.funbox === "none") return;
for (const fb of getActiveFunboxes()) {
fb.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 =

View file

@ -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]);
}

View file

@ -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(() => {

View file

@ -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") {

View file

@ -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) {

View file

@ -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) {

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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;

View file

@ -1,204 +0,0 @@
[
{
"name": "nausea",
"info": "I think I'm gonna be sick.",
"canGetPb": true
},
{
"name": "round_round_baby",
"info": "...right round, like a record baby. Right, round round round.",
"canGetPb": true
},
{
"name": "simon_says",
"info": "Type what simon says.",
"canGetPb": true
},
{
"name": "mirror",
"info": "Everything is mirrored!",
"canGetPb": true
},
{"name": "upside_down",
"info": "Everything is upside down!",
"canGetPb": true
},
{
"name": "tts",
"info": "Listen closely.",
"canGetPb": true
},
{
"name": "choo_choo",
"info": "All the letters are spinning!",
"canGetPb": true
},
{
"name": "arrows",
"info": "Play it on a pad!",
"canGetPb": false
},
{
"name": "rAnDoMcAsE",
"info": "I kInDa LiKe HoW iNeFfIcIeNt QwErTy Is.",
"canGetPb": false
},
{
"name": "capitals",
"info": "Capitalize Every Word.",
"canGetPb": false
},
{
"name": "layoutfluid",
"info": "Switch between layouts specified below proportionately to the length of the test.",
"canGetPb": true
},
{
"name": "earthquake",
"info": "Everybody get down! The words are shaking!",
"canGetPb": true
},
{
"name": "space_balls",
"info": "In a galaxy far far away.",
"canGetPb": true
},
{
"name": "gibberish",
"info": "Anvbuefl dizzs eoos alsb?",
"canGetPb": false
},
{
"name": "58008",
"alias": "numbers",
"info": "A special mode for accountants.",
"canGetPb": false
},
{
"name": "ascii",
"info": "Where was the ampersand again?. Only ASCII characters.",
"canGetPb": false
},
{
"name": "specials",
"info": "!@#$%^&*. Only special characters.",
"canGetPb": false
},
{
"name": "plus_zero",
"info": "React quickly! Only the current word is visible.",
"canGetPb": true
},
{
"name": "plus_one",
"info": "Only one future word is visible.",
"canGetPb": true
},
{
"name": "plus_two",
"info": "Only two future words are visible.",
"canGetPb": true
},
{
"name": "plus_three",
"info": "Only three future words are visible.",
"canGetPb": true
},
{
"name": "read_ahead_easy",
"info": "Only the current word is invisible.",
"canGetPb": true
},
{
"name": "read_ahead",
"info": "Current and the next word are invisible!",
"canGetPb": true
},
{
"name": "read_ahead_hard",
"info": "Current and the next two words are invisible!",
"canGetPb": true
},
{
"name": "memory",
"info": "Test your memory. Remember the words and type them blind.",
"canGetPb": true
},
{
"name": "nospace",
"info": "Whoneedsspacesanyway?",
"canGetPb": false
},
{
"name": "poetry",
"info": "Practice typing some beautiful prose.",
"canGetPb": false
},
{
"name": "wikipedia",
"info": "Practice typing wikipedia sections.",
"canGetPb": false
},
{
"name": "weakspot",
"info": "Focus on slow and mistyped letters.",
"canGetPb": false
},
{
"name": "pseudolang",
"info": "Nonsense words that look like the current language.",
"canGetPb": false
},
{
"name": "IPv4",
"alias": "network",
"info": "For sysadmins.",
"canGetPb": false
},
{
"name": "IPv6",
"alias": "network",
"info": "For sysadmins with a long beard.",
"canGetPb": false
},
{
"name": "binary",
"info": "01000010 01100101 01100101 01110000 00100000 01100010 01101111 01101111 01110000 00101110",
"canGetPb": false
},
{
"name": "hexadecimal",
"info": "0x38 0x20 0x74 0x69 0x6D 0x65 0x73 0x20 0x6D 0x6F 0x72 0x65 0x20 0x62 0x6F 0x6F 0x70 0x21",
"canGetPb": false
},
{
"name": "zipf",
"info": "Words are generated according to Zipf's law. (not all languages will produce Zipfy results, use with caution)",
"canGetPb": false
},
{
"name": "morse",
"info": "-.../././.--./ -.../---/---/.--./-.-.--/ ",
"canGetPb": false
},
{
"name": "crt",
"info": "Go back to the 1980s",
"canGetPb": true
},
{
"name": "backwards",
"info": "...sdrawkcab epyt ot yrt woN",
"canGetPb": true
},
{
"name": "ddoouubblleedd",
"info": "TTyyppee eevveerryytthhiinngg ttwwiiccee..",
"canGetPb": true
},
{
"name": "instant_messaging",
"info": "Who needs shift anyway?",
"canGetPb": false
}
]

View file

@ -10,5 +10,12 @@ export default defineConfig({
coverage: {
include: ["**/*.ts"],
},
deps: {
optimizer: {
web: {
include: ["@monkeytype/funbox"],
},
},
},
},
});

View file

@ -0,0 +1,5 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: ["@monkeytype/eslint-config"],
};

View file

@ -0,0 +1,12 @@
{
"extends": "@monkeytype/typescript-config/base.json",
"compilerOptions": {
"noEmit": true,
"types": ["vitest/globals"]
},
"ts-node": {
"files": true
},
// "files": ["../src/types/types.d.ts"],
"include": ["./**/*.ts", "./**/*.spec.ts", "./setup-tests.ts"]
}

View file

@ -0,0 +1,30 @@
{
"name": "@monkeytype/funbox",
"private": true,
"scripts": {
"dev": "rimraf ./dist && monkeytype-esbuild --watch",
"build": "rimraf ./dist && npm run madge && monkeytype-esbuild",
"madge": " madge --circular --extensions ts ./src",
"ts-check": "tsc --noEmit",
"lint": "eslint \"./**/*.ts\""
},
"devDependencies": {
"@monkeytype/util": "workspace:*",
"@monkeytype/esbuild": "workspace:*",
"@monkeytype/eslint-config": "workspace:*",
"@monkeytype/typescript-config": "workspace:*",
"chokidar": "3.6.0",
"eslint": "8.57.0",
"madge": "8.0.0",
"rimraf": "6.0.1",
"typescript": "5.5.4",
"vitest": "2.0.5"
},
"exports": {
".": {
"types": "./src/index.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
}
}

View file

@ -0,0 +1,19 @@
import { getList, getFunbox, getObject } from "./list";
import { FunboxMetadata, FunboxName, FunboxProperty } from "./types";
import { stringToFunboxNames } from "./util";
import { checkCompatibility } from "./validation";
export type { FunboxName, FunboxMetadata, FunboxProperty };
export { checkCompatibility, stringToFunboxNames, getFunbox };
export function getFunboxesFromString(names: string): FunboxMetadata[] {
return getFunbox(stringToFunboxNames(names));
}
export function getAllFunboxes(): FunboxMetadata[] {
return getList();
}
export function getFunboxObject(): Record<FunboxName, FunboxMetadata> {
return getObject();
}

View file

@ -1,14 +1,8 @@
export type FunboxMetadata = {
name: string;
canGetPb: boolean;
difficultyLevel: number;
properties?: string[];
frontendForcedConfig?: Record<string, string[] | boolean[]>;
frontendFunctions?: string[];
};
import { FunboxMetadata, FunboxName } from "./types";
const FunboxList: FunboxMetadata[] = [
{
const list: Record<FunboxName, FunboxMetadata> = {
"58008": {
description: "A special mode for accountants.",
canGetPb: false,
difficultyLevel: 1,
properties: ["ignoresLanguage", "ignoresLayout", "noLetters"],
@ -22,64 +16,69 @@ const FunboxList: FunboxMetadata[] = [
"handleChar",
],
name: "58008",
alias: "numbers",
},
{
canGetPb: true,
difficultyLevel: 2,
frontendFunctions: ["applyCSS"],
name: "nausea",
},
{
canGetPb: true,
difficultyLevel: 3,
frontendFunctions: ["applyCSS"],
name: "round_round_baby",
},
{
canGetPb: true,
difficultyLevel: 1,
properties: ["changesWordsVisibility", "usesLayout"],
frontendForcedConfig: {
highlightMode: ["letter", "off"],
},
frontendFunctions: ["applyCSS", "applyConfig", "rememberSettings"],
name: "simon_says",
},
{
canGetPb: true,
difficultyLevel: 3,
frontendFunctions: ["applyCSS"],
mirror: {
name: "mirror",
},
{
description: "Everything is mirrored!",
properties: ["hasCssFile"],
canGetPb: true,
difficultyLevel: 3,
frontendFunctions: ["applyCSS"],
name: "upside_down",
},
{
upside_down: {
name: "upside_down",
description: "Everything is upside down!",
properties: ["hasCssFile"],
canGetPb: true,
difficultyLevel: 3,
},
nausea: {
name: "nausea",
description: "I think I'm gonna be sick.",
canGetPb: true,
difficultyLevel: 2,
properties: ["hasCssFile"],
},
round_round_baby: {
name: "round_round_baby",
description:
"...right round, like a record baby. Right, round round round.",
canGetPb: true,
difficultyLevel: 3,
properties: ["hasCssFile"],
},
simon_says: {
name: "simon_says",
description: "Type what simon says.",
canGetPb: true,
difficultyLevel: 1,
properties: ["changesWordsVisibility", "speaks"],
properties: ["hasCssFile", "changesWordsVisibility", "usesLayout"],
frontendForcedConfig: {
highlightMode: ["letter", "off"],
},
frontendFunctions: [
"applyCSS",
"applyConfig",
"rememberSettings",
"toggleScript",
],
name: "tts",
frontendFunctions: ["applyConfig", "rememberSettings"],
},
{
tts: {
canGetPb: true,
difficultyLevel: 1,
properties: ["hasCssFile", "changesWordsVisibility", "speaks"],
frontendForcedConfig: {
highlightMode: ["letter", "off"],
},
frontendFunctions: ["applyConfig", "rememberSettings", "toggleScript"],
name: "tts",
description: "Listen closely.",
},
choo_choo: {
canGetPb: true,
difficultyLevel: 2,
properties: ["noLigatures", "conflictsWithSymmetricChars"],
frontendFunctions: ["applyCSS"],
properties: ["hasCssFile", "noLigatures", "conflictsWithSymmetricChars"],
name: "choo_choo",
description: "All the letters are spinning!",
},
{
arrows: {
description: "Play it on a pad!",
canGetPb: false,
difficultyLevel: 1,
properties: [
@ -96,7 +95,6 @@ const FunboxList: FunboxMetadata[] = [
},
frontendFunctions: [
"getWord",
"applyConfig",
"rememberSettings",
"handleChar",
"isCharCorrect",
@ -105,21 +103,25 @@ const FunboxList: FunboxMetadata[] = [
],
name: "arrows",
},
{
rAnDoMcAsE: {
description: "I kInDa LiKe HoW iNeFfIcIeNt QwErTy Is.",
canGetPb: false,
difficultyLevel: 2,
properties: ["changesCapitalisation"],
frontendFunctions: ["alterText"],
name: "rAnDoMcAsE",
},
{
capitals: {
description: "Capitalize Every Word.",
canGetPb: false,
difficultyLevel: 1,
properties: ["changesCapitalisation"],
frontendFunctions: ["alterText"],
name: "capitals",
},
{
layoutfluid: {
description:
"Switch between layouts specified below proportionately to the length of the test.",
canGetPb: true,
difficultyLevel: 1,
properties: ["changesLayout", "noInfiniteDuration"],
@ -132,27 +134,30 @@ const FunboxList: FunboxMetadata[] = [
],
name: "layoutfluid",
},
{
earthquake: {
description: "Everybody get down! The words are shaking!",
canGetPb: true,
difficultyLevel: 1,
properties: ["noLigatures"],
frontendFunctions: ["applyCSS"],
properties: ["hasCssFile", "noLigatures"],
name: "earthquake",
},
{
space_balls: {
description: "In a galaxy far far away.",
canGetPb: true,
difficultyLevel: 0,
frontendFunctions: ["applyCSS"],
properties: ["hasCssFile"],
name: "space_balls",
},
{
gibberish: {
description: "Anvbuefl dizzs eoos alsb?",
canGetPb: false,
difficultyLevel: 1,
properties: ["ignoresLanguage", "unspeakable"],
frontendFunctions: ["getWord"],
name: "gibberish",
},
{
ascii: {
description: "Where was the ampersand again?. Only ASCII characters.",
canGetPb: false,
difficultyLevel: 1,
properties: ["ignoresLanguage", "noLetters", "unspeakable"],
@ -163,7 +168,8 @@ const FunboxList: FunboxMetadata[] = [
frontendFunctions: ["getWord"],
name: "ascii",
},
{
specials: {
description: "!@#$%^&*. Only special characters.",
canGetPb: false,
difficultyLevel: 1,
properties: ["ignoresLanguage", "noLetters", "unspeakable"],
@ -174,61 +180,69 @@ const FunboxList: FunboxMetadata[] = [
frontendFunctions: ["getWord"],
name: "specials",
},
{
plus_one: {
description: "Only one future word is visible.",
canGetPb: true,
difficultyLevel: 0,
properties: ["changesWordsVisibility", "toPush:2", "noInfiniteDuration"],
name: "plus_one",
},
{
plus_zero: {
description: "React quickly! Only the current word is visible.",
canGetPb: true,
difficultyLevel: 1,
properties: ["changesWordsVisibility", "toPush:1", "noInfiniteDuration"],
name: "plus_zero",
},
{
plus_two: {
description: "Only two future words are visible.",
canGetPb: true,
difficultyLevel: 0,
properties: ["changesWordsVisibility", "toPush:3", "noInfiniteDuration"],
name: "plus_two",
},
{
plus_three: {
description: "Only three future words are visible.",
canGetPb: true,
difficultyLevel: 0,
properties: ["changesWordsVisibility", "toPush:4", "noInfiniteDuration"],
name: "plus_three",
},
{
read_ahead_easy: {
description: "Only the current word is invisible.",
canGetPb: true,
difficultyLevel: 1,
properties: ["changesWordsVisibility"],
properties: ["changesWordsVisibility", "hasCssFile"],
frontendForcedConfig: {
highlightMode: ["letter", "off"],
},
frontendFunctions: ["applyCSS", "rememberSettings", "handleKeydown"],
frontendFunctions: ["rememberSettings", "handleKeydown"],
name: "read_ahead_easy",
},
{
read_ahead: {
description: "Current and the next word are invisible!",
canGetPb: true,
difficultyLevel: 2,
properties: ["changesWordsVisibility"],
properties: ["changesWordsVisibility", "hasCssFile"],
frontendForcedConfig: {
highlightMode: ["letter", "off"],
},
frontendFunctions: ["applyCSS", "rememberSettings", "handleKeydown"],
frontendFunctions: ["rememberSettings", "handleKeydown"],
name: "read_ahead",
},
{
read_ahead_hard: {
description: "Current and the next two words are invisible!",
canGetPb: true,
difficultyLevel: 3,
properties: ["changesWordsVisibility"],
properties: ["changesWordsVisibility", "hasCssFile"],
frontendForcedConfig: {
highlightMode: ["letter", "off"],
},
frontendFunctions: ["applyCSS", "rememberSettings", "handleKeydown"],
frontendFunctions: ["rememberSettings", "handleKeydown"],
name: "read_ahead_hard",
},
{
memory: {
description: "Test your memory. Remember the words and type them blind.",
canGetPb: true,
difficultyLevel: 3,
properties: ["changesWordsVisibility", "noInfiniteDuration"],
@ -238,17 +252,19 @@ const FunboxList: FunboxMetadata[] = [
frontendFunctions: ["applyConfig", "rememberSettings", "start", "restart"],
name: "memory",
},
{
nospace: {
description: "Whoneedsspacesanyway?",
canGetPb: false,
difficultyLevel: 0,
properties: ["nospace"],
frontendForcedConfig: {
highlightMode: ["letter", "off"],
},
frontendFunctions: ["applyConfig", "rememberSettings"],
frontendFunctions: ["rememberSettings"],
name: "nospace",
},
{
poetry: {
description: "Practice typing some beautiful prose.",
canGetPb: false,
difficultyLevel: 0,
properties: ["noInfiniteDuration", "ignoresLanguage"],
@ -259,7 +275,8 @@ const FunboxList: FunboxMetadata[] = [
frontendFunctions: ["pullSection"],
name: "poetry",
},
{
wikipedia: {
description: "Practice typing wikipedia sections.",
canGetPb: false,
difficultyLevel: 0,
properties: ["noInfiniteDuration", "ignoresLanguage"],
@ -270,21 +287,25 @@ const FunboxList: FunboxMetadata[] = [
frontendFunctions: ["pullSection"],
name: "wikipedia",
},
{
weakspot: {
description: "Focus on slow and mistyped letters.",
canGetPb: false,
difficultyLevel: 0,
properties: ["changesWordsFrequency"],
frontendFunctions: ["getWord"],
name: "weakspot",
},
{
pseudolang: {
description: "Nonsense words that look like the current language.",
canGetPb: false,
difficultyLevel: 0,
properties: ["unspeakable", "ignoresLanguage"],
frontendFunctions: ["withWords"],
name: "pseudolang",
},
{
IPv4: {
alias: "network",
description: "For sysadmins.",
canGetPb: false,
difficultyLevel: 1,
properties: ["ignoresLanguage", "ignoresLayout", "noLetters"],
@ -294,7 +315,9 @@ const FunboxList: FunboxMetadata[] = [
frontendFunctions: ["getWord", "punctuateWord", "rememberSettings"],
name: "IPv4",
},
{
IPv6: {
alias: "network",
description: "For sysadmins with a long beard.",
canGetPb: false,
difficultyLevel: 1,
properties: ["ignoresLanguage", "ignoresLayout", "noLetters"],
@ -304,7 +327,9 @@ const FunboxList: FunboxMetadata[] = [
frontendFunctions: ["getWord", "punctuateWord", "rememberSettings"],
name: "IPv6",
},
{
binary: {
description:
"01000010 01100101 01100101 01110000 00100000 01100010 01101111 01101111 01110000 00101110",
canGetPb: false,
difficultyLevel: 1,
properties: ["ignoresLanguage", "ignoresLayout", "noLetters"],
@ -315,7 +340,9 @@ const FunboxList: FunboxMetadata[] = [
frontendFunctions: ["getWord"],
name: "binary",
},
{
hexadecimal: {
description:
"0x38 0x20 0x74 0x69 0x6D 0x65 0x73 0x20 0x6D 0x6F 0x72 0x65 0x20 0x62 0x6F 0x6F 0x70 0x21",
canGetPb: false,
difficultyLevel: 1,
properties: ["ignoresLanguage", "ignoresLayout", "noLetters"],
@ -325,51 +352,99 @@ const FunboxList: FunboxMetadata[] = [
frontendFunctions: ["getWord", "punctuateWord", "rememberSettings"],
name: "hexadecimal",
},
{
zipf: {
description:
"Words are generated according to Zipf's law. (not all languages will produce Zipfy results, use with caution)",
canGetPb: false,
difficultyLevel: 0,
properties: ["changesWordsFrequency"],
frontendFunctions: ["getWordsFrequencyMode"],
name: "zipf",
},
{
morse: {
description: "-.../././.--./ -.../---/---/.--./-.-.--/ ",
canGetPb: false,
difficultyLevel: 1,
properties: ["ignoresLanguage", "ignoresLayout", "noLetters", "noSpace"],
properties: ["ignoresLanguage", "ignoresLayout", "noLetters", "nospace"],
frontendFunctions: ["alterText"],
name: "morse",
},
{
crt: {
description: "Go back to the 1980s",
canGetPb: true,
difficultyLevel: 0,
properties: ["noLigatures"],
properties: ["hasCssFile", "noLigatures"],
frontendFunctions: ["applyGlobalCSS", "clearGlobal"],
name: "crt",
},
{
backwards: {
description: "...sdrawkcab epyt ot yrt woN",
name: "backwards",
properties: [
"hasCssFile",
"noLigatures",
"conflictsWithSymmetricChars",
"wordOrder:reverse",
],
frontendFunctions: ["applyCSS"],
canGetPb: true,
frontendFunctions: ["alterText"],
difficultyLevel: 3,
},
{
ddoouubblleedd: {
description: "TTyyppee eevveerryytthhiinngg ttwwiiccee..",
canGetPb: true,
difficultyLevel: 1,
properties: ["noLigatures"],
frontendFunctions: ["alterText"],
name: "ddoouubblleedd",
},
{
instant_messaging: {
description: "Who needs shift anyway?",
canGetPb: false,
difficultyLevel: 1,
properties: ["changesCapitalisation"],
frontendFunctions: ["alterText"],
name: "instant_messaging",
},
];
};
export default FunboxList;
export function getFunbox(name: FunboxName): FunboxMetadata;
export function getFunbox(names: FunboxName[]): FunboxMetadata[];
export function getFunbox(
nameOrNames: FunboxName | FunboxName[]
): FunboxMetadata | FunboxMetadata[] {
if (Array.isArray(nameOrNames)) {
const out = nameOrNames.map((name) => getObject()[name]);
//@ts-expect-error
if (out.includes(undefined)) {
throw new Error("One of the funboxes is invalid: " + nameOrNames);
}
return out;
} else {
const out = getObject()[nameOrNames];
if (out === undefined) {
throw new Error("Invalid funbox name: " + nameOrNames);
}
return out;
}
}
export function getObject(): Record<FunboxName, FunboxMetadata> {
return list;
}
export function getList(): FunboxMetadata[] {
const out: FunboxMetadata[] = [];
for (const name of getFunboxNames()) {
out.push(list[name]);
}
return out;
}
function getFunboxNames(): FunboxName[] {
return Object.keys(list) as FunboxName[];
}

View file

@ -0,0 +1,74 @@
export type FunboxName =
| "58008"
| "mirror"
| "upside_down"
| "nausea"
| "round_round_baby"
| "simon_says"
| "tts"
| "choo_choo"
| "arrows"
| "rAnDoMcAsE"
| "capitals"
| "layoutfluid"
| "earthquake"
| "space_balls"
| "gibberish"
| "ascii"
| "specials"
| "plus_one"
| "plus_zero"
| "plus_two"
| "plus_three"
| "read_ahead_easy"
| "read_ahead"
| "read_ahead_hard"
| "memory"
| "nospace"
| "poetry"
| "wikipedia"
| "weakspot"
| "pseudolang"
| "IPv4"
| "IPv6"
| "binary"
| "hexadecimal"
| "zipf"
| "morse"
| "crt"
| "backwards"
| "ddoouubblleedd"
| "instant_messaging";
export type FunboxForcedConfig = Record<string, string[] | boolean[]>;
export type FunboxProperty =
| "hasCssFile"
| "ignoresLanguage"
| "ignoresLayout"
| "noLetters"
| "changesLayout"
| "usesLayout"
| "nospace"
| "changesWordsVisibility"
| "changesWordsFrequency"
| "changesCapitalisation"
| "conflictsWithSymmetricChars"
| "symmetricChars"
| "speaks"
| "unspeakable"
| "noInfiniteDuration"
| "noLigatures"
| `toPush:${number}`
| "wordOrder:reverse";
export type FunboxMetadata = {
name: FunboxName;
alias?: string;
description: string;
properties?: FunboxProperty[];
frontendForcedConfig?: FunboxForcedConfig;
frontendFunctions?: string[];
difficultyLevel: number;
canGetPb: boolean;
};

View file

@ -0,0 +1,17 @@
import { getList } from "./list";
import { FunboxName } from "./types";
export function stringToFunboxNames(names: string): FunboxName[] {
if (names === "none" || names === "") return [];
const unsafeNames = names.split("#").map((name) => name.trim());
const out: FunboxName[] = [];
const list = getList().map((f) => f.name);
for (const unsafeName of unsafeNames) {
if (list.includes(unsafeName as FunboxName)) {
out.push(unsafeName as FunboxName);
} else {
throw new Error("Invalid funbox name: " + unsafeName);
}
}
return out;
}

View file

@ -0,0 +1,154 @@
import { intersect } from "@monkeytype/util/arrays";
import { FunboxForcedConfig, FunboxName } from "./types";
import { getFunbox } from "./list";
export function checkCompatibility(
funboxNames: FunboxName[],
withFunbox?: FunboxName
): boolean {
if (withFunbox === undefined || funboxNames.length === 0) return true;
let funboxesToCheck = getFunbox(funboxNames);
if (withFunbox !== undefined) {
funboxesToCheck = funboxesToCheck.concat(getFunbox(withFunbox));
}
const allFunboxesAreValid = getFunbox(funboxNames).every(
(f) => f !== undefined
);
const oneWordModifierMax =
funboxesToCheck.filter(
(f) =>
f.frontendFunctions?.includes("getWord") ??
f.frontendFunctions?.includes("pullSection") ??
f.frontendFunctions?.includes("withWords")
).length <= 1;
const oneWordOrderMax =
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp.startsWith("wordOrder"))
).length <= 1;
const layoutUsability =
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "changesLayout")
).length === 0 ||
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "ignoresLayout" || fp === "usesLayout")
).length === 0;
const oneNospaceOrToPushMax =
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "nospace" || fp.startsWith("toPush"))
).length <= 1;
const oneChangesWordsVisibilityMax =
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "changesWordsVisibility")
).length <= 1;
const oneFrequencyChangesMax =
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "changesWordsFrequency")
).length <= 1;
const noFrequencyChangesConflicts =
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "changesWordsFrequency")
).length === 0 ||
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "ignoresLanguage")
).length === 0;
const capitalisationChangePosibility =
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "noLetters")
).length === 0 ||
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "changesCapitalisation")
).length === 0;
const noConflictsWithSymmetricChars =
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "conflictsWithSymmetricChars")
).length === 0 ||
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "symmetricChars")
).length === 0;
const oneCanSpeakMax =
funboxesToCheck.filter((f) => f.properties?.find((fp) => fp === "speaks"))
.length <= 1;
const hasLanguageToSpeakAndNoUnspeakable =
funboxesToCheck.filter((f) => f.properties?.find((fp) => fp === "speaks"))
.length === 0 ||
(funboxesToCheck.filter((f) => f.properties?.find((fp) => fp === "speaks"))
.length === 1 &&
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "unspeakable")
).length === 0) ||
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "ignoresLanguage")
).length === 0;
const oneToPushOrPullSectionMax =
funboxesToCheck.filter(
(f) =>
(f.properties?.find((fp) => fp.startsWith("toPush:")) ?? "") ||
f.frontendFunctions?.includes("pullSection")
).length <= 1;
const oneCssFileMax =
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "hasCssFile")
).length <= 1;
const onePunctuateWordMax =
funboxesToCheck.filter((f) =>
f.frontendFunctions?.includes("punctuateWord")
).length <= 1;
const oneCharCheckerMax =
funboxesToCheck.filter((f) =>
f.frontendFunctions?.includes("isCharCorrect")
).length <= 1;
const oneCharReplacerMax =
funboxesToCheck.filter((f) => f.frontendFunctions?.includes("getWordHtml"))
.length <= 1;
const oneChangesCapitalisationMax =
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "changesCapitalisation")
).length <= 1;
const allowedConfig = {} as FunboxForcedConfig;
let noConfigConflicts = true;
for (const f of funboxesToCheck) {
if (!f.frontendForcedConfig) continue;
for (const key in f.frontendForcedConfig) {
if (allowedConfig[key]) {
if (
intersect<string | boolean>(
allowedConfig[key],
f.frontendForcedConfig[key] as string[] | boolean[],
true
).length === 0
) {
noConfigConflicts = false;
break;
}
} else {
allowedConfig[key] = f.frontendForcedConfig[key] as
| string[]
| boolean[];
}
}
}
return (
allFunboxesAreValid &&
oneWordModifierMax &&
layoutUsability &&
oneNospaceOrToPushMax &&
oneChangesWordsVisibilityMax &&
oneFrequencyChangesMax &&
noFrequencyChangesConflicts &&
capitalisationChangePosibility &&
noConflictsWithSymmetricChars &&
oneCanSpeakMax &&
hasLanguageToSpeakAndNoUnspeakable &&
oneToPushOrPullSectionMax &&
oneCssFileMax &&
onePunctuateWordMax &&
oneCharCheckerMax &&
oneCharReplacerMax &&
oneChangesCapitalisationMax &&
noConfigConflicts &&
oneWordOrderMax
);
}

View file

@ -0,0 +1,15 @@
{
"extends": "@monkeytype/typescript-config/base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"moduleResolution": "Bundler",
"module": "ES6",
"target": "ES2015",
"lib": ["es2016"]
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View file

@ -0,0 +1,11 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
environment: "node",
coverage: {
include: ["**/*.ts"],
},
},
});

331
pnpm-lock.yaml generated
View file

@ -53,6 +53,9 @@ importers:
'@monkeytype/contracts':
specifier: workspace:*
version: link:../packages/contracts
'@monkeytype/funbox':
specifier: workspace:*
version: link:../packages/funbox
'@monkeytype/util':
specifier: workspace:*
version: link:../packages/util
@ -267,6 +270,9 @@ importers:
'@monkeytype/contracts':
specifier: workspace:*
version: link:../packages/contracts
'@monkeytype/funbox':
specifier: workspace:*
version: link:../packages/funbox
'@monkeytype/util':
specifier: workspace:*
version: link:../packages/util
@ -522,6 +528,39 @@ importers:
specifier: 1.1.9
version: 1.1.9
packages/funbox:
devDependencies:
'@monkeytype/esbuild':
specifier: workspace:*
version: link:../esbuild-config
'@monkeytype/eslint-config':
specifier: workspace:*
version: link:../eslint-config
'@monkeytype/typescript-config':
specifier: workspace:*
version: link:../typescript-config
'@monkeytype/util':
specifier: workspace:*
version: link:../util
chokidar:
specifier: 3.6.0
version: 3.6.0
eslint:
specifier: 8.57.0
version: 8.57.0
madge:
specifier: 8.0.0
version: 8.0.0(typescript@5.5.4)
rimraf:
specifier: 6.0.1
version: 6.0.1
typescript:
specifier: 5.5.4
version: 5.5.4
vitest:
specifier: 2.0.5
version: 2.0.5(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.31.3)
packages/release:
dependencies:
'@octokit/rest':
@ -1302,12 +1341,6 @@ packages:
resolution: {integrity: sha512-9Z0sGuXqf6En19qmwB0Syi1Mc8TYl756dNuuaYal9mrypKa0Jq/IX6aJfh6Rk2S3z66KBisWTqloDo7weYj4zg==}
engines: {node: '>=4'}
'@esbuild/aix-ppc64@0.19.12':
resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==}
engines: {node: '>=12'}
cpu: [ppc64]
os: [aix]
'@esbuild/aix-ppc64@0.20.2':
resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==}
engines: {node: '>=12'}
@ -1326,12 +1359,6 @@ packages:
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.19.12':
resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==}
engines: {node: '>=12'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm64@0.20.2':
resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==}
engines: {node: '>=12'}
@ -1350,12 +1377,6 @@ packages:
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.19.12':
resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==}
engines: {node: '>=12'}
cpu: [arm]
os: [android]
'@esbuild/android-arm@0.20.2':
resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==}
engines: {node: '>=12'}
@ -1374,12 +1395,6 @@ packages:
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.19.12':
resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==}
engines: {node: '>=12'}
cpu: [x64]
os: [android]
'@esbuild/android-x64@0.20.2':
resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==}
engines: {node: '>=12'}
@ -1398,12 +1413,6 @@ packages:
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.19.12':
resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==}
engines: {node: '>=12'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-arm64@0.20.2':
resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==}
engines: {node: '>=12'}
@ -1422,12 +1431,6 @@ packages:
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.19.12':
resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==}
engines: {node: '>=12'}
cpu: [x64]
os: [darwin]
'@esbuild/darwin-x64@0.20.2':
resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==}
engines: {node: '>=12'}
@ -1446,12 +1449,6 @@ packages:
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.19.12':
resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==}
engines: {node: '>=12'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-arm64@0.20.2':
resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==}
engines: {node: '>=12'}
@ -1470,12 +1467,6 @@ packages:
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.19.12':
resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==}
engines: {node: '>=12'}
cpu: [x64]
os: [freebsd]
'@esbuild/freebsd-x64@0.20.2':
resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==}
engines: {node: '>=12'}
@ -1494,12 +1485,6 @@ packages:
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.19.12':
resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==}
engines: {node: '>=12'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm64@0.20.2':
resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==}
engines: {node: '>=12'}
@ -1518,12 +1503,6 @@ packages:
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.19.12':
resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==}
engines: {node: '>=12'}
cpu: [arm]
os: [linux]
'@esbuild/linux-arm@0.20.2':
resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==}
engines: {node: '>=12'}
@ -1542,12 +1521,6 @@ packages:
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.19.12':
resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==}
engines: {node: '>=12'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-ia32@0.20.2':
resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==}
engines: {node: '>=12'}
@ -1566,12 +1539,6 @@ packages:
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.19.12':
resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==}
engines: {node: '>=12'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-loong64@0.20.2':
resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==}
engines: {node: '>=12'}
@ -1590,12 +1557,6 @@ packages:
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.19.12':
resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==}
engines: {node: '>=12'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-mips64el@0.20.2':
resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==}
engines: {node: '>=12'}
@ -1614,12 +1575,6 @@ packages:
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.19.12':
resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==}
engines: {node: '>=12'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-ppc64@0.20.2':
resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==}
engines: {node: '>=12'}
@ -1638,12 +1593,6 @@ packages:
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.19.12':
resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==}
engines: {node: '>=12'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-riscv64@0.20.2':
resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==}
engines: {node: '>=12'}
@ -1662,12 +1611,6 @@ packages:
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.19.12':
resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==}
engines: {node: '>=12'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-s390x@0.20.2':
resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==}
engines: {node: '>=12'}
@ -1686,12 +1629,6 @@ packages:
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.19.12':
resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==}
engines: {node: '>=12'}
cpu: [x64]
os: [linux]
'@esbuild/linux-x64@0.20.2':
resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==}
engines: {node: '>=12'}
@ -1710,12 +1647,6 @@ packages:
cpu: [x64]
os: [linux]
'@esbuild/netbsd-x64@0.19.12':
resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==}
engines: {node: '>=12'}
cpu: [x64]
os: [netbsd]
'@esbuild/netbsd-x64@0.20.2':
resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==}
engines: {node: '>=12'}
@ -1740,12 +1671,6 @@ packages:
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.19.12':
resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==}
engines: {node: '>=12'}
cpu: [x64]
os: [openbsd]
'@esbuild/openbsd-x64@0.20.2':
resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==}
engines: {node: '>=12'}
@ -1764,12 +1689,6 @@ packages:
cpu: [x64]
os: [openbsd]
'@esbuild/sunos-x64@0.19.12':
resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==}
engines: {node: '>=12'}
cpu: [x64]
os: [sunos]
'@esbuild/sunos-x64@0.20.2':
resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==}
engines: {node: '>=12'}
@ -1788,12 +1707,6 @@ packages:
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.19.12':
resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==}
engines: {node: '>=12'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-arm64@0.20.2':
resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==}
engines: {node: '>=12'}
@ -1812,12 +1725,6 @@ packages:
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.19.12':
resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==}
engines: {node: '>=12'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-ia32@0.20.2':
resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==}
engines: {node: '>=12'}
@ -1836,12 +1743,6 @@ packages:
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.19.12':
resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==}
engines: {node: '>=12'}
cpu: [x64]
os: [win32]
'@esbuild/win32-x64@0.20.2':
resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==}
engines: {node: '>=12'}
@ -4545,11 +4446,6 @@ packages:
es6-weak-map@2.0.3:
resolution: {integrity: sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==}
esbuild@0.19.12:
resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==}
engines: {node: '>=12'}
hasBin: true
esbuild@0.20.2:
resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==}
engines: {node: '>=12'}
@ -9286,34 +9182,6 @@ packages:
'@vite-pwa/assets-generator':
optional: true
vite@5.1.7:
resolution: {integrity: sha512-sgnEEFTZYMui/sTlH1/XEnVNHMujOahPLGMxn1+5sIT45Xjng1Ec1K78jRP15dSmVgg5WBin9yO81j3o9OxofA==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies:
'@types/node': ^18.0.0 || >=20.0.0
less: '*'
lightningcss: ^1.21.0
sass: '*'
stylus: '*'
sugarss: '*'
terser: ^5.4.0
peerDependenciesMeta:
'@types/node':
optional: true
less:
optional: true
lightningcss:
optional: true
sass:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
vite@5.2.14:
resolution: {integrity: sha512-TFQLuwWLPms+NBNlh0D9LZQ+HXW471COABxw/9TEUBrjuHMo9BrYBPrN/SYAwIuVL+rLerycxiLT41t4f5MZpA==}
engines: {node: ^18.0.0 || >=20.0.0}
@ -10665,9 +10533,6 @@ snapshots:
to-pascal-case: 1.0.0
unescape-js: 1.1.4
'@esbuild/aix-ppc64@0.19.12':
optional: true
'@esbuild/aix-ppc64@0.20.2':
optional: true
@ -10677,9 +10542,6 @@ snapshots:
'@esbuild/aix-ppc64@0.23.0':
optional: true
'@esbuild/android-arm64@0.19.12':
optional: true
'@esbuild/android-arm64@0.20.2':
optional: true
@ -10689,9 +10551,6 @@ snapshots:
'@esbuild/android-arm64@0.23.0':
optional: true
'@esbuild/android-arm@0.19.12':
optional: true
'@esbuild/android-arm@0.20.2':
optional: true
@ -10701,9 +10560,6 @@ snapshots:
'@esbuild/android-arm@0.23.0':
optional: true
'@esbuild/android-x64@0.19.12':
optional: true
'@esbuild/android-x64@0.20.2':
optional: true
@ -10713,9 +10569,6 @@ snapshots:
'@esbuild/android-x64@0.23.0':
optional: true
'@esbuild/darwin-arm64@0.19.12':
optional: true
'@esbuild/darwin-arm64@0.20.2':
optional: true
@ -10725,9 +10578,6 @@ snapshots:
'@esbuild/darwin-arm64@0.23.0':
optional: true
'@esbuild/darwin-x64@0.19.12':
optional: true
'@esbuild/darwin-x64@0.20.2':
optional: true
@ -10737,9 +10587,6 @@ snapshots:
'@esbuild/darwin-x64@0.23.0':
optional: true
'@esbuild/freebsd-arm64@0.19.12':
optional: true
'@esbuild/freebsd-arm64@0.20.2':
optional: true
@ -10749,9 +10596,6 @@ snapshots:
'@esbuild/freebsd-arm64@0.23.0':
optional: true
'@esbuild/freebsd-x64@0.19.12':
optional: true
'@esbuild/freebsd-x64@0.20.2':
optional: true
@ -10761,9 +10605,6 @@ snapshots:
'@esbuild/freebsd-x64@0.23.0':
optional: true
'@esbuild/linux-arm64@0.19.12':
optional: true
'@esbuild/linux-arm64@0.20.2':
optional: true
@ -10773,9 +10614,6 @@ snapshots:
'@esbuild/linux-arm64@0.23.0':
optional: true
'@esbuild/linux-arm@0.19.12':
optional: true
'@esbuild/linux-arm@0.20.2':
optional: true
@ -10785,9 +10623,6 @@ snapshots:
'@esbuild/linux-arm@0.23.0':
optional: true
'@esbuild/linux-ia32@0.19.12':
optional: true
'@esbuild/linux-ia32@0.20.2':
optional: true
@ -10797,9 +10632,6 @@ snapshots:
'@esbuild/linux-ia32@0.23.0':
optional: true
'@esbuild/linux-loong64@0.19.12':
optional: true
'@esbuild/linux-loong64@0.20.2':
optional: true
@ -10809,9 +10641,6 @@ snapshots:
'@esbuild/linux-loong64@0.23.0':
optional: true
'@esbuild/linux-mips64el@0.19.12':
optional: true
'@esbuild/linux-mips64el@0.20.2':
optional: true
@ -10821,9 +10650,6 @@ snapshots:
'@esbuild/linux-mips64el@0.23.0':
optional: true
'@esbuild/linux-ppc64@0.19.12':
optional: true
'@esbuild/linux-ppc64@0.20.2':
optional: true
@ -10833,9 +10659,6 @@ snapshots:
'@esbuild/linux-ppc64@0.23.0':
optional: true
'@esbuild/linux-riscv64@0.19.12':
optional: true
'@esbuild/linux-riscv64@0.20.2':
optional: true
@ -10845,9 +10668,6 @@ snapshots:
'@esbuild/linux-riscv64@0.23.0':
optional: true
'@esbuild/linux-s390x@0.19.12':
optional: true
'@esbuild/linux-s390x@0.20.2':
optional: true
@ -10857,9 +10677,6 @@ snapshots:
'@esbuild/linux-s390x@0.23.0':
optional: true
'@esbuild/linux-x64@0.19.12':
optional: true
'@esbuild/linux-x64@0.20.2':
optional: true
@ -10869,9 +10686,6 @@ snapshots:
'@esbuild/linux-x64@0.23.0':
optional: true
'@esbuild/netbsd-x64@0.19.12':
optional: true
'@esbuild/netbsd-x64@0.20.2':
optional: true
@ -10884,9 +10698,6 @@ snapshots:
'@esbuild/openbsd-arm64@0.23.0':
optional: true
'@esbuild/openbsd-x64@0.19.12':
optional: true
'@esbuild/openbsd-x64@0.20.2':
optional: true
@ -10896,9 +10707,6 @@ snapshots:
'@esbuild/openbsd-x64@0.23.0':
optional: true
'@esbuild/sunos-x64@0.19.12':
optional: true
'@esbuild/sunos-x64@0.20.2':
optional: true
@ -10908,9 +10716,6 @@ snapshots:
'@esbuild/sunos-x64@0.23.0':
optional: true
'@esbuild/win32-arm64@0.19.12':
optional: true
'@esbuild/win32-arm64@0.20.2':
optional: true
@ -10920,9 +10725,6 @@ snapshots:
'@esbuild/win32-arm64@0.23.0':
optional: true
'@esbuild/win32-ia32@0.19.12':
optional: true
'@esbuild/win32-ia32@0.20.2':
optional: true
@ -10932,9 +10734,6 @@ snapshots:
'@esbuild/win32-ia32@0.23.0':
optional: true
'@esbuild/win32-x64@0.19.12':
optional: true
'@esbuild/win32-x64@0.20.2':
optional: true
@ -14185,32 +13984,6 @@ snapshots:
es6-iterator: 2.0.3
es6-symbol: 3.1.4
esbuild@0.19.12:
optionalDependencies:
'@esbuild/aix-ppc64': 0.19.12
'@esbuild/android-arm': 0.19.12
'@esbuild/android-arm64': 0.19.12
'@esbuild/android-x64': 0.19.12
'@esbuild/darwin-arm64': 0.19.12
'@esbuild/darwin-x64': 0.19.12
'@esbuild/freebsd-arm64': 0.19.12
'@esbuild/freebsd-x64': 0.19.12
'@esbuild/linux-arm': 0.19.12
'@esbuild/linux-arm64': 0.19.12
'@esbuild/linux-ia32': 0.19.12
'@esbuild/linux-loong64': 0.19.12
'@esbuild/linux-mips64el': 0.19.12
'@esbuild/linux-ppc64': 0.19.12
'@esbuild/linux-riscv64': 0.19.12
'@esbuild/linux-s390x': 0.19.12
'@esbuild/linux-x64': 0.19.12
'@esbuild/netbsd-x64': 0.19.12
'@esbuild/openbsd-x64': 0.19.12
'@esbuild/sunos-x64': 0.19.12
'@esbuild/win32-arm64': 0.19.12
'@esbuild/win32-ia32': 0.19.12
'@esbuild/win32-x64': 0.19.12
esbuild@0.20.2:
optionalDependencies:
'@esbuild/aix-ppc64': 0.20.2
@ -20067,28 +19840,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
vite@5.1.7(@types/node@20.14.11)(sass@1.70.0)(terser@5.31.3):
dependencies:
esbuild: 0.19.12
postcss: 8.4.40
rollup: 4.19.1
optionalDependencies:
'@types/node': 20.14.11
fsevents: 2.3.3
sass: 1.70.0
terser: 5.31.3
vite@5.1.7(@types/node@20.5.1)(sass@1.70.0)(terser@5.31.3):
dependencies:
esbuild: 0.19.12
postcss: 8.4.40
rollup: 4.19.1
optionalDependencies:
'@types/node': 20.5.1
fsevents: 2.3.3
sass: 1.70.0
terser: 5.31.3
vite@5.2.14(@types/node@20.14.11)(sass@1.70.0)(terser@5.31.3):
dependencies:
esbuild: 0.20.2
@ -20141,7 +19892,7 @@ snapshots:
tinybench: 2.8.0
tinypool: 1.0.0
tinyrainbow: 1.2.0
vite: 5.1.7(@types/node@20.14.11)(sass@1.70.0)(terser@5.31.3)
vite: 5.2.14(@types/node@20.14.11)(sass@1.70.0)(terser@5.31.3)
vite-node: 2.0.5(@types/node@20.14.11)(sass@1.70.0)(terser@5.31.3)
why-is-node-running: 2.3.0
optionalDependencies:
@ -20174,7 +19925,7 @@ snapshots:
tinybench: 2.8.0
tinypool: 1.0.0
tinyrainbow: 1.2.0
vite: 5.1.7(@types/node@20.5.1)(sass@1.70.0)(terser@5.31.3)
vite: 5.2.14(@types/node@20.5.1)(sass@1.70.0)(terser@5.31.3)
vite-node: 2.0.5(@types/node@20.5.1)(sass@1.70.0)(terser@5.31.3)
why-is-node-running: 2.3.0
optionalDependencies: