aboutsummaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/funbox/.eslintrc.cjs5
-rw-r--r--packages/funbox/__test__/tsconfig.json12
-rw-r--r--packages/funbox/package.json30
-rw-r--r--packages/funbox/src/index.ts19
-rw-r--r--packages/funbox/src/list.ts450
-rw-r--r--packages/funbox/src/types.ts74
-rw-r--r--packages/funbox/src/util.ts17
-rw-r--r--packages/funbox/src/validation.ts154
-rw-r--r--packages/funbox/tsconfig.json15
-rw-r--r--packages/funbox/vitest.config.js11
10 files changed, 787 insertions, 0 deletions
diff --git a/packages/funbox/.eslintrc.cjs b/packages/funbox/.eslintrc.cjs
new file mode 100644
index 000000000..922de4abe
--- /dev/null
+++ b/packages/funbox/.eslintrc.cjs
@@ -0,0 +1,5 @@
+/** @type {import("eslint").Linter.Config} */
+module.exports = {
+ root: true,
+ extends: ["@monkeytype/eslint-config"],
+};
diff --git a/packages/funbox/__test__/tsconfig.json b/packages/funbox/__test__/tsconfig.json
new file mode 100644
index 000000000..8d8a39621
--- /dev/null
+++ b/packages/funbox/__test__/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "extends": "@monkeytype/typescript-config/base.json",
+ "compilerOptions": {
+ "noEmit": true,
+ "types": ["vitest/globals"]
+ },
+ "ts-node": {
+ "files": true
+ },
+ // "files": ["../src/types/types.d.ts"],
+ "include": ["./**/*.ts", "./**/*.spec.ts", "./setup-tests.ts"]
+}
diff --git a/packages/funbox/package.json b/packages/funbox/package.json
new file mode 100644
index 000000000..525e3b83d
--- /dev/null
+++ b/packages/funbox/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@monkeytype/funbox",
+ "private": true,
+ "scripts": {
+ "dev": "rimraf ./dist && monkeytype-esbuild --watch",
+ "build": "rimraf ./dist && npm run madge && monkeytype-esbuild",
+ "madge": " madge --circular --extensions ts ./src",
+ "ts-check": "tsc --noEmit",
+ "lint": "eslint \"./**/*.ts\""
+ },
+ "devDependencies": {
+ "@monkeytype/util": "workspace:*",
+ "@monkeytype/esbuild": "workspace:*",
+ "@monkeytype/eslint-config": "workspace:*",
+ "@monkeytype/typescript-config": "workspace:*",
+ "chokidar": "3.6.0",
+ "eslint": "8.57.0",
+ "madge": "8.0.0",
+ "rimraf": "6.0.1",
+ "typescript": "5.5.4",
+ "vitest": "2.0.5"
+ },
+ "exports": {
+ ".": {
+ "types": "./src/index.ts",
+ "import": "./dist/index.mjs",
+ "require": "./dist/index.cjs"
+ }
+ }
+}
diff --git a/packages/funbox/src/index.ts b/packages/funbox/src/index.ts
new file mode 100644
index 000000000..dc36d1f68
--- /dev/null
+++ b/packages/funbox/src/index.ts
@@ -0,0 +1,19 @@
+import { getList, getFunbox, getObject } from "./list";
+import { FunboxMetadata, FunboxName, FunboxProperty } from "./types";
+import { stringToFunboxNames } from "./util";
+import { checkCompatibility } from "./validation";
+
+export type { FunboxName, FunboxMetadata, FunboxProperty };
+export { checkCompatibility, stringToFunboxNames, getFunbox };
+
+export function getFunboxesFromString(names: string): FunboxMetadata[] {
+ return getFunbox(stringToFunboxNames(names));
+}
+
+export function getAllFunboxes(): FunboxMetadata[] {
+ return getList();
+}
+
+export function getFunboxObject(): Record<FunboxName, FunboxMetadata> {
+ return getObject();
+}
diff --git a/packages/funbox/src/list.ts b/packages/funbox/src/list.ts
new file mode 100644
index 000000000..06a5055f5
--- /dev/null
+++ b/packages/funbox/src/list.ts
@@ -0,0 +1,450 @@
+import { FunboxMetadata, FunboxName } from "./types";
+
+const list: Record<FunboxName, FunboxMetadata> = {
+ "58008": {
+ description: "A special mode for accountants.",
+ canGetPb: false,
+ difficultyLevel: 1,
+ properties: ["ignoresLanguage", "ignoresLayout", "noLetters"],
+ frontendForcedConfig: {
+ numbers: [false],
+ },
+ frontendFunctions: [
+ "getWord",
+ "punctuateWord",
+ "rememberSettings",
+ "handleChar",
+ ],
+ name: "58008",
+ alias: "numbers",
+ },
+ mirror: {
+ name: "mirror",
+ description: "Everything is mirrored!",
+ properties: ["hasCssFile"],
+ canGetPb: true,
+ difficultyLevel: 3,
+ },
+ upside_down: {
+ name: "upside_down",
+ description: "Everything is upside down!",
+ properties: ["hasCssFile"],
+ canGetPb: true,
+ difficultyLevel: 3,
+ },
+ nausea: {
+ name: "nausea",
+ description: "I think I'm gonna be sick.",
+ canGetPb: true,
+ difficultyLevel: 2,
+ properties: ["hasCssFile"],
+ },
+ round_round_baby: {
+ name: "round_round_baby",
+ description:
+ "...right round, like a record baby. Right, round round round.",
+ canGetPb: true,
+ difficultyLevel: 3,
+ properties: ["hasCssFile"],
+ },
+ simon_says: {
+ name: "simon_says",
+ description: "Type what simon says.",
+ canGetPb: true,
+ difficultyLevel: 1,
+ properties: ["hasCssFile", "changesWordsVisibility", "usesLayout"],
+ frontendForcedConfig: {
+ highlightMode: ["letter", "off"],
+ },
+ frontendFunctions: ["applyConfig", "rememberSettings"],
+ },
+
+ tts: {
+ canGetPb: true,
+ difficultyLevel: 1,
+ properties: ["hasCssFile", "changesWordsVisibility", "speaks"],
+ frontendForcedConfig: {
+ highlightMode: ["letter", "off"],
+ },
+ frontendFunctions: ["applyConfig", "rememberSettings", "toggleScript"],
+ name: "tts",
+ description: "Listen closely.",
+ },
+ choo_choo: {
+ canGetPb: true,
+ difficultyLevel: 2,
+ properties: ["hasCssFile", "noLigatures", "conflictsWithSymmetricChars"],
+ name: "choo_choo",
+ description: "All the letters are spinning!",
+ },
+ arrows: {
+ description: "Play it on a pad!",
+ canGetPb: false,
+ difficultyLevel: 1,
+ properties: [
+ "ignoresLanguage",
+ "ignoresLayout",
+ "nospace",
+ "noLetters",
+ "symmetricChars",
+ ],
+ frontendForcedConfig: {
+ punctuation: [false],
+ numbers: [false],
+ highlightMode: ["letter", "off"],
+ },
+ frontendFunctions: [
+ "getWord",
+ "rememberSettings",
+ "handleChar",
+ "isCharCorrect",
+ "preventDefaultEvent",
+ "getWordHtml",
+ ],
+ name: "arrows",
+ },
+ rAnDoMcAsE: {
+ description: "I kInDa LiKe HoW iNeFfIcIeNt QwErTy Is.",
+ canGetPb: false,
+ difficultyLevel: 2,
+ properties: ["changesCapitalisation"],
+ frontendFunctions: ["alterText"],
+ name: "rAnDoMcAsE",
+ },
+ capitals: {
+ description: "Capitalize Every Word.",
+ canGetPb: false,
+ difficultyLevel: 1,
+ properties: ["changesCapitalisation"],
+ frontendFunctions: ["alterText"],
+ name: "capitals",
+ },
+ layoutfluid: {
+ description:
+ "Switch between layouts specified below proportionately to the length of the test.",
+ canGetPb: true,
+ difficultyLevel: 1,
+ properties: ["changesLayout", "noInfiniteDuration"],
+ frontendFunctions: [
+ "applyConfig",
+ "rememberSettings",
+ "handleSpace",
+ "getResultContent",
+ "restart",
+ ],
+ name: "layoutfluid",
+ },
+ earthquake: {
+ description: "Everybody get down! The words are shaking!",
+ canGetPb: true,
+ difficultyLevel: 1,
+ properties: ["hasCssFile", "noLigatures"],
+ name: "earthquake",
+ },
+ space_balls: {
+ description: "In a galaxy far far away.",
+ canGetPb: true,
+ difficultyLevel: 0,
+ properties: ["hasCssFile"],
+ name: "space_balls",
+ },
+ gibberish: {
+ description: "Anvbuefl dizzs eoos alsb?",
+ canGetPb: false,
+ difficultyLevel: 1,
+ properties: ["ignoresLanguage", "unspeakable"],
+ frontendFunctions: ["getWord"],
+ name: "gibberish",
+ },
+ ascii: {
+ description: "Where was the ampersand again?. Only ASCII characters.",
+ canGetPb: false,
+ difficultyLevel: 1,
+ properties: ["ignoresLanguage", "noLetters", "unspeakable"],
+ frontendForcedConfig: {
+ punctuation: [false],
+ numbers: [false],
+ },
+ frontendFunctions: ["getWord"],
+ name: "ascii",
+ },
+ specials: {
+ description: "!@#$%^&*. Only special characters.",
+ canGetPb: false,
+ difficultyLevel: 1,
+ properties: ["ignoresLanguage", "noLetters", "unspeakable"],
+ frontendForcedConfig: {
+ punctuation: [false],
+ numbers: [false],
+ },
+ frontendFunctions: ["getWord"],
+ name: "specials",
+ },
+ plus_one: {
+ description: "Only one future word is visible.",
+ canGetPb: true,
+ difficultyLevel: 0,
+ properties: ["changesWordsVisibility", "toPush:2", "noInfiniteDuration"],
+ name: "plus_one",
+ },
+ plus_zero: {
+ description: "React quickly! Only the current word is visible.",
+ canGetPb: true,
+ difficultyLevel: 1,
+ properties: ["changesWordsVisibility", "toPush:1", "noInfiniteDuration"],
+ name: "plus_zero",
+ },
+ plus_two: {
+ description: "Only two future words are visible.",
+ canGetPb: true,
+ difficultyLevel: 0,
+ properties: ["changesWordsVisibility", "toPush:3", "noInfiniteDuration"],
+ name: "plus_two",
+ },
+ plus_three: {
+ description: "Only three future words are visible.",
+ canGetPb: true,
+ difficultyLevel: 0,
+ properties: ["changesWordsVisibility", "toPush:4", "noInfiniteDuration"],
+ name: "plus_three",
+ },
+ read_ahead_easy: {
+ description: "Only the current word is invisible.",
+ canGetPb: true,
+ difficultyLevel: 1,
+ properties: ["changesWordsVisibility", "hasCssFile"],
+ frontendForcedConfig: {
+ highlightMode: ["letter", "off"],
+ },
+ frontendFunctions: ["rememberSettings", "handleKeydown"],
+ name: "read_ahead_easy",
+ },
+ read_ahead: {
+ description: "Current and the next word are invisible!",
+ canGetPb: true,
+ difficultyLevel: 2,
+ properties: ["changesWordsVisibility", "hasCssFile"],
+ frontendForcedConfig: {
+ highlightMode: ["letter", "off"],
+ },
+ frontendFunctions: ["rememberSettings", "handleKeydown"],
+ name: "read_ahead",
+ },
+ read_ahead_hard: {
+ description: "Current and the next two words are invisible!",
+ canGetPb: true,
+ difficultyLevel: 3,
+ properties: ["changesWordsVisibility", "hasCssFile"],
+ frontendForcedConfig: {
+ highlightMode: ["letter", "off"],
+ },
+ frontendFunctions: ["rememberSettings", "handleKeydown"],
+ name: "read_ahead_hard",
+ },
+ memory: {
+ description: "Test your memory. Remember the words and type them blind.",
+ canGetPb: true,
+ difficultyLevel: 3,
+ properties: ["changesWordsVisibility", "noInfiniteDuration"],
+ frontendForcedConfig: {
+ mode: ["words", "quote", "custom"],
+ },
+ frontendFunctions: ["applyConfig", "rememberSettings", "start", "restart"],
+ name: "memory",
+ },
+ nospace: {
+ description: "Whoneedsspacesanyway?",
+ canGetPb: false,
+ difficultyLevel: 0,
+ properties: ["nospace"],
+ frontendForcedConfig: {
+ highlightMode: ["letter", "off"],
+ },
+ frontendFunctions: ["rememberSettings"],
+ name: "nospace",
+ },
+ poetry: {
+ description: "Practice typing some beautiful prose.",
+ canGetPb: false,
+ difficultyLevel: 0,
+ properties: ["noInfiniteDuration", "ignoresLanguage"],
+ frontendForcedConfig: {
+ punctuation: [false],
+ numbers: [false],
+ },
+ frontendFunctions: ["pullSection"],
+ name: "poetry",
+ },
+ wikipedia: {
+ description: "Practice typing wikipedia sections.",
+ canGetPb: false,
+ difficultyLevel: 0,
+ properties: ["noInfiniteDuration", "ignoresLanguage"],
+ frontendForcedConfig: {
+ punctuation: [false],
+ numbers: [false],
+ },
+ frontendFunctions: ["pullSection"],
+ name: "wikipedia",
+ },
+ weakspot: {
+ description: "Focus on slow and mistyped letters.",
+ canGetPb: false,
+ difficultyLevel: 0,
+ properties: ["changesWordsFrequency"],
+ frontendFunctions: ["getWord"],
+ name: "weakspot",
+ },
+ pseudolang: {
+ description: "Nonsense words that look like the current language.",
+ canGetPb: false,
+ difficultyLevel: 0,
+ properties: ["unspeakable", "ignoresLanguage"],
+ frontendFunctions: ["withWords"],
+ name: "pseudolang",
+ },
+ IPv4: {
+ alias: "network",
+ description: "For sysadmins.",
+ canGetPb: false,
+ difficultyLevel: 1,
+ properties: ["ignoresLanguage", "ignoresLayout", "noLetters"],
+ frontendForcedConfig: {
+ numbers: [false],
+ },
+ frontendFunctions: ["getWord", "punctuateWord", "rememberSettings"],
+ name: "IPv4",
+ },
+ IPv6: {
+ alias: "network",
+ description: "For sysadmins with a long beard.",
+ canGetPb: false,
+ difficultyLevel: 1,
+ properties: ["ignoresLanguage", "ignoresLayout", "noLetters"],
+ frontendForcedConfig: {
+ numbers: [false],
+ },
+ frontendFunctions: ["getWord", "punctuateWord", "rememberSettings"],
+ name: "IPv6",
+ },
+ binary: {
+ description:
+ "01000010 01100101 01100101 01110000 00100000 01100010 01101111 01101111 01110000 00101110",
+ canGetPb: false,
+ difficultyLevel: 1,
+ properties: ["ignoresLanguage", "ignoresLayout", "noLetters"],
+ frontendForcedConfig: {
+ numbers: [false],
+ punctuation: [false],
+ },
+ frontendFunctions: ["getWord"],
+ name: "binary",
+ },
+ hexadecimal: {
+ description:
+ "0x38 0x20 0x74 0x69 0x6D 0x65 0x73 0x20 0x6D 0x6F 0x72 0x65 0x20 0x62 0x6F 0x6F 0x70 0x21",
+ canGetPb: false,
+ difficultyLevel: 1,
+ properties: ["ignoresLanguage", "ignoresLayout", "noLetters"],
+ frontendForcedConfig: {
+ numbers: [false],
+ },
+ frontendFunctions: ["getWord", "punctuateWord", "rememberSettings"],
+ name: "hexadecimal",
+ },
+ zipf: {
+ description:
+ "Words are generated according to Zipf's law. (not all languages will produce Zipfy results, use with caution)",
+ canGetPb: false,
+ difficultyLevel: 0,
+ properties: ["changesWordsFrequency"],
+ frontendFunctions: ["getWordsFrequencyMode"],
+ name: "zipf",
+ },
+ morse: {
+ description: "-.../././.--./ -.../---/---/.--./-.-.--/ ",
+ canGetPb: false,
+ difficultyLevel: 1,
+ properties: ["ignoresLanguage", "ignoresLayout", "noLetters", "nospace"],
+ frontendFunctions: ["alterText"],
+ name: "morse",
+ },
+ crt: {
+ description: "Go back to the 1980s",
+ canGetPb: true,
+ difficultyLevel: 0,
+ properties: ["hasCssFile", "noLigatures"],
+ frontendFunctions: ["applyGlobalCSS", "clearGlobal"],
+ name: "crt",
+ },
+ backwards: {
+ description: "...sdrawkcab epyt ot yrt woN",
+ name: "backwards",
+ properties: [
+ "hasCssFile",
+ "noLigatures",
+ "conflictsWithSymmetricChars",
+ "wordOrder:reverse",
+ ],
+ canGetPb: true,
+ frontendFunctions: ["alterText"],
+ difficultyLevel: 3,
+ },
+ ddoouubblleedd: {
+ description: "TTyyppee eevveerryytthhiinngg ttwwiiccee..",
+ canGetPb: true,
+ difficultyLevel: 1,
+ properties: ["noLigatures"],
+ frontendFunctions: ["alterText"],
+ name: "ddoouubblleedd",
+ },
+ instant_messaging: {
+ description: "Who needs shift anyway?",
+ canGetPb: false,
+ difficultyLevel: 1,
+ properties: ["changesCapitalisation"],
+ frontendFunctions: ["alterText"],
+ name: "instant_messaging",
+ },
+};
+
+export function getFunbox(name: FunboxName): FunboxMetadata;
+export function getFunbox(names: FunboxName[]): FunboxMetadata[];
+export function getFunbox(
+ nameOrNames: FunboxName | FunboxName[]
+): FunboxMetadata | FunboxMetadata[] {
+ if (Array.isArray(nameOrNames)) {
+ const out = nameOrNames.map((name) => getObject()[name]);
+
+ //@ts-expect-error
+ if (out.includes(undefined)) {
+ throw new Error("One of the funboxes is invalid: " + nameOrNames);
+ }
+
+ return out;
+ } else {
+ const out = getObject()[nameOrNames];
+
+ if (out === undefined) {
+ throw new Error("Invalid funbox name: " + nameOrNames);
+ }
+
+ return out;
+ }
+}
+
+export function getObject(): Record<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[];
+}
diff --git a/packages/funbox/src/types.ts b/packages/funbox/src/types.ts
new file mode 100644
index 000000000..8a19d9117
--- /dev/null
+++ b/packages/funbox/src/types.ts
@@ -0,0 +1,74 @@
+export type FunboxName =
+ | "58008"
+ | "mirror"
+ | "upside_down"
+ | "nausea"
+ | "round_round_baby"
+ | "simon_says"
+ | "tts"
+ | "choo_choo"
+ | "arrows"
+ | "rAnDoMcAsE"
+ | "capitals"
+ | "layoutfluid"
+ | "earthquake"
+ | "space_balls"
+ | "gibberish"
+ | "ascii"
+ | "specials"
+ | "plus_one"
+ | "plus_zero"
+ | "plus_two"
+ | "plus_three"
+ | "read_ahead_easy"
+ | "read_ahead"
+ | "read_ahead_hard"
+ | "memory"
+ | "nospace"
+ | "poetry"
+ | "wikipedia"
+ | "weakspot"
+ | "pseudolang"
+ | "IPv4"
+ | "IPv6"
+ | "binary"
+ | "hexadecimal"
+ | "zipf"
+ | "morse"
+ | "crt"
+ | "backwards"
+ | "ddoouubblleedd"
+ | "instant_messaging";
+
+export type FunboxForcedConfig = Record<string, string[] | boolean[]>;
+
+export type FunboxProperty =
+ | "hasCssFile"
+ | "ignoresLanguage"
+ | "ignoresLayout"
+ | "noLetters"
+ | "changesLayout"
+ | "usesLayout"
+ | "nospace"
+ | "changesWordsVisibility"
+ | "changesWordsFrequency"
+ | "changesCapitalisation"
+ | "conflictsWithSymmetricChars"
+ | "symmetricChars"
+ | "speaks"
+ | "unspeakable"
+ | "noInfiniteDuration"
+ | "noLigatures"
+ | `toPush:${number}`
+ | "wordOrder:reverse";
+
+export type FunboxMetadata = {
+ name: FunboxName;
+ alias?: string;
+ description: string;
+ properties?: FunboxProperty[];
+ frontendForcedConfig?: FunboxForcedConfig;
+ frontendFunctions?: string[];
+ difficultyLevel: number;
+ canGetPb: boolean;
+};
diff --git a/packages/funbox/src/util.ts b/packages/funbox/src/util.ts
new file mode 100644
index 000000000..3b7a62a53
--- /dev/null
+++ b/packages/funbox/src/util.ts
@@ -0,0 +1,17 @@
+import { getList } from "./list";
+import { FunboxName } from "./types";
+
+export function stringToFunboxNames(names: string): FunboxName[] {
+ if (names === "none" || names === "") return [];
+ const unsafeNames = names.split("#").map((name) => name.trim());
+ const out: FunboxName[] = [];
+ const list = getList().map((f) => f.name);
+ for (const unsafeName of unsafeNames) {
+ if (list.includes(unsafeName as FunboxName)) {
+ out.push(unsafeName as FunboxName);
+ } else {
+ throw new Error("Invalid funbox name: " + unsafeName);
+ }
+ }
+ return out;
+}
diff --git a/packages/funbox/src/validation.ts b/packages/funbox/src/validation.ts
new file mode 100644
index 000000000..70cdaa5df
--- /dev/null
+++ b/packages/funbox/src/validation.ts
@@ -0,0 +1,154 @@
+import { intersect } from "@monkeytype/util/arrays";
+import { FunboxForcedConfig, FunboxName } from "./types";
+import { getFunbox } from "./list";
+
+export function checkCompatibility(
+ funboxNames: FunboxName[],
+ withFunbox?: FunboxName
+): boolean {
+ if (withFunbox === undefined || funboxNames.length === 0) return true;
+ let funboxesToCheck = getFunbox(funboxNames);
+ if (withFunbox !== undefined) {
+ funboxesToCheck = funboxesToCheck.concat(getFunbox(withFunbox));
+ }
+
+ const allFunboxesAreValid = getFunbox(funboxNames).every(
+ (f) => f !== undefined
+ );
+
+ const oneWordModifierMax =
+ funboxesToCheck.filter(
+ (f) =>
+ f.frontendFunctions?.includes("getWord") ??
+ f.frontendFunctions?.includes("pullSection") ??
+ f.frontendFunctions?.includes("withWords")
+ ).length <= 1;
+ const oneWordOrderMax =
+ funboxesToCheck.filter((f) =>
+ f.properties?.find((fp) => fp.startsWith("wordOrder"))
+ ).length <= 1;
+ const layoutUsability =
+ funboxesToCheck.filter((f) =>
+ f.properties?.find((fp) => fp === "changesLayout")
+ ).length === 0 ||
+ funboxesToCheck.filter((f) =>
+ f.properties?.find((fp) => fp === "ignoresLayout" || fp === "usesLayout")
+ ).length === 0;
+ const oneNospaceOrToPushMax =
+ funboxesToCheck.filter((f) =>
+ f.properties?.find((fp) => fp === "nospace" || fp.startsWith("toPush"))
+ ).length <= 1;
+ const oneChangesWordsVisibilityMax =
+ funboxesToCheck.filter((f) =>
+ f.properties?.find((fp) => fp === "changesWordsVisibility")
+ ).length <= 1;
+ const oneFrequencyChangesMax =
+ funboxesToCheck.filter((f) =>
+ f.properties?.find((fp) => fp === "changesWordsFrequency")
+ ).length <= 1;
+ const noFrequencyChangesConflicts =
+ funboxesToCheck.filter((f) =>
+ f.properties?.find((fp) => fp === "changesWordsFrequency")
+ ).length === 0 ||
+ funboxesToCheck.filter((f) =>
+ f.properties?.find((fp) => fp === "ignoresLanguage")
+ ).length === 0;
+ const capitalisationChangePosibility =
+ funboxesToCheck.filter((f) =>
+ f.properties?.find((fp) => fp === "noLetters")
+ ).length === 0 ||
+ funboxesToCheck.filter((f) =>
+ f.properties?.find((fp) => fp === "changesCapitalisation")
+ ).length === 0;
+ const noConflictsWithSymmetricChars =
+ funboxesToCheck.filter((f) =>
+ f.properties?.find((fp) => fp === "conflictsWithSymmetricChars")
+ ).length === 0 ||
+ funboxesToCheck.filter((f) =>
+ f.properties?.find((fp) => fp === "symmetricChars")
+ ).length === 0;
+ const oneCanSpeakMax =
+ funboxesToCheck.filter((f) => f.properties?.find((fp) => fp === "speaks"))
+ .length <= 1;
+ const hasLanguageToSpeakAndNoUnspeakable =
+ funboxesToCheck.filter((f) => f.properties?.find((fp) => fp === "speaks"))
+ .length === 0 ||
+ (funboxesToCheck.filter((f) => f.properties?.find((fp) => fp === "speaks"))
+ .length === 1 &&
+ funboxesToCheck.filter((f) =>
+ f.properties?.find((fp) => fp === "unspeakable")
+ ).length === 0) ||
+ funboxesToCheck.filter((f) =>
+ f.properties?.find((fp) => fp === "ignoresLanguage")
+ ).length === 0;
+ const oneToPushOrPullSectionMax =
+ funboxesToCheck.filter(
+ (f) =>
+ (f.properties?.find((fp) => fp.startsWith("toPush:")) ?? "") ||
+ f.frontendFunctions?.includes("pullSection")
+ ).length <= 1;
+ const oneCssFileMax =
+ funboxesToCheck.filter((f) =>
+ f.properties?.find((fp) => fp === "hasCssFile")
+ ).length <= 1;
+ const onePunctuateWordMax =
+ funboxesToCheck.filter((f) =>
+ f.frontendFunctions?.includes("punctuateWord")
+ ).length <= 1;
+ const oneCharCheckerMax =
+ funboxesToCheck.filter((f) =>
+ f.frontendFunctions?.includes("isCharCorrect")
+ ).length <= 1;
+ const oneCharReplacerMax =
+ funboxesToCheck.filter((f) => f.frontendFunctions?.includes("getWordHtml"))
+ .length <= 1;
+ const oneChangesCapitalisationMax =
+ funboxesToCheck.filter((f) =>
+ f.properties?.find((fp) => fp === "changesCapitalisation")
+ ).length <= 1;
+ const allowedConfig = {} as FunboxForcedConfig;
+ let noConfigConflicts = true;
+ for (const f of funboxesToCheck) {
+ if (!f.frontendForcedConfig) continue;
+ for (const key in f.frontendForcedConfig) {
+ if (allowedConfig[key]) {
+ if (
+ intersect<string | boolean>(
+ allowedConfig[key],
+ f.frontendForcedConfig[key] as string[] | boolean[],
+ true
+ ).length === 0
+ ) {
+ noConfigConflicts = false;
+ break;
+ }
+ } else {
+ allowedConfig[key] = f.frontendForcedConfig[key] as
+ | string[]
+ | boolean[];
+ }
+ }
+ }
+
+ return (
+ allFunboxesAreValid &&
+ oneWordModifierMax &&
+ layoutUsability &&
+ oneNospaceOrToPushMax &&
+ oneChangesWordsVisibilityMax &&
+ oneFrequencyChangesMax &&
+ noFrequencyChangesConflicts &&
+ capitalisationChangePosibility &&
+ noConflictsWithSymmetricChars &&
+ oneCanSpeakMax &&
+ hasLanguageToSpeakAndNoUnspeakable &&
+ oneToPushOrPullSectionMax &&
+ oneCssFileMax &&
+ onePunctuateWordMax &&
+ oneCharCheckerMax &&
+ oneCharReplacerMax &&
+ oneChangesCapitalisationMax &&
+ noConfigConflicts &&
+ oneWordOrderMax
+ );
+}
diff --git a/packages/funbox/tsconfig.json b/packages/funbox/tsconfig.json
new file mode 100644
index 000000000..de674c340
--- /dev/null
+++ b/packages/funbox/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "extends": "@monkeytype/typescript-config/base.json",
+ "compilerOptions": {
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "declaration": true,
+ "declarationMap": true,
+ "moduleResolution": "Bundler",
+ "module": "ES6",
+ "target": "ES2015",
+ "lib": ["es2016"]
+ },
+ "include": ["src"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/packages/funbox/vitest.config.js b/packages/funbox/vitest.config.js
new file mode 100644
index 000000000..d071c79ce
--- /dev/null
+++ b/packages/funbox/vitest.config.js
@@ -0,0 +1,11 @@
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ test: {
+ globals: true,
+ environment: "node",
+ coverage: {
+ include: ["**/*.ts"],
+ },
+ },
+});