This commit is contained in:
7
packages/funbox/.oxlintrc.json
Normal file
7
packages/funbox/.oxlintrc.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"ignorePatterns": ["node_modules", "dist", ".turbo"],
|
||||
"extends": [
|
||||
"../oxlint-config/index.jsonc"
|
||||
// "@monkeytype/oxlint-config"
|
||||
]
|
||||
}
|
||||
7
packages/funbox/__test__/tsconfig.json
Normal file
7
packages/funbox/__test__/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "@monkeytype/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["./**/*.ts", "./**/*.spec.ts", "./setup-tests.ts"]
|
||||
}
|
||||
160
packages/funbox/__test__/validation.spec.ts
Normal file
160
packages/funbox/__test__/validation.spec.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import * as List from "../src/list";
|
||||
import * as Validation from "../src/validation";
|
||||
import { FunboxMetadata } from "../src/types";
|
||||
|
||||
describe("validation", () => {
|
||||
describe("checkCompatibility", () => {
|
||||
const getFunboxMock = vi.spyOn(List, "getFunbox");
|
||||
|
||||
beforeEach(() => {
|
||||
getFunboxMock.mockClear();
|
||||
});
|
||||
|
||||
it("should pass without funboxNames", () => {
|
||||
//WHEN / THEN
|
||||
expect(Validation.checkCompatibility([])).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail for undefined funboxNames", () => {
|
||||
//GIVEN
|
||||
getFunboxMock.mockReturnValueOnce([
|
||||
{
|
||||
name: "plus_one",
|
||||
} as FunboxMetadata,
|
||||
undefined as unknown as FunboxMetadata,
|
||||
]);
|
||||
|
||||
//WHEN / THEN
|
||||
expect(Validation.checkCompatibility(["plus_one", "plus_two"])).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("should fail for undefined withFunbox param", () => {
|
||||
//GIVEN
|
||||
getFunboxMock
|
||||
.mockReturnValueOnce([])
|
||||
.mockReturnValue([undefined as unknown as FunboxMetadata]);
|
||||
|
||||
//WHEN / THEN
|
||||
expect(
|
||||
Validation.checkCompatibility(["plus_one", "plus_two"], "plus_three"),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("should check for optional withFunbox param ", () => {
|
||||
//GIVEN
|
||||
getFunboxMock
|
||||
.mockReturnValueOnce([
|
||||
{
|
||||
name: "plus_one",
|
||||
cssModifications: ["body", "main"],
|
||||
} as FunboxMetadata,
|
||||
{
|
||||
name: "plus_two",
|
||||
} as FunboxMetadata,
|
||||
])
|
||||
.mockReturnValueOnce([
|
||||
{
|
||||
name: "plus_three",
|
||||
cssModifications: ["main", "typingTest"],
|
||||
} as FunboxMetadata,
|
||||
]);
|
||||
|
||||
//WHEN
|
||||
const result = Validation.checkCompatibility(
|
||||
["plus_one", "plus_two"],
|
||||
"plus_three",
|
||||
);
|
||||
|
||||
//THEN
|
||||
expect(result).toBe(false);
|
||||
|
||||
expect(getFunboxMock).toHaveBeenNthCalledWith(1, [
|
||||
"plus_one",
|
||||
"plus_two",
|
||||
]);
|
||||
expect(getFunboxMock).toHaveBeenNthCalledWith(2, "plus_three");
|
||||
});
|
||||
|
||||
it("should reject two funboxes modifying the same css element", () => {
|
||||
//GIVEN
|
||||
getFunboxMock.mockReturnValueOnce([
|
||||
{
|
||||
name: "plus_one",
|
||||
cssModifications: ["body", "main"],
|
||||
} as FunboxMetadata,
|
||||
{
|
||||
name: "plus_two",
|
||||
cssModifications: ["main", "typingTest"],
|
||||
} as FunboxMetadata,
|
||||
]);
|
||||
|
||||
//WHEN / THEN
|
||||
expect(Validation.checkCompatibility(["plus_one", "plus_two"])).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("should allow two funboxes modifying different css elements", () => {
|
||||
//GIVEN
|
||||
getFunboxMock.mockReturnValueOnce([
|
||||
{
|
||||
name: "plus_one",
|
||||
cssModifications: ["body", "main"],
|
||||
} as FunboxMetadata,
|
||||
{
|
||||
name: "plus_two",
|
||||
cssModifications: ["words"],
|
||||
} as FunboxMetadata,
|
||||
]);
|
||||
|
||||
//WHEN / THEN
|
||||
expect(Validation.checkCompatibility(["plus_one", "plus_two"])).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
describe("should validate two funboxes modifying the wordset", () => {
|
||||
const testCases = [
|
||||
{
|
||||
firstFunction: "withWords",
|
||||
secondFunction: "withWords",
|
||||
compatible: false,
|
||||
},
|
||||
{
|
||||
firstFunction: "withWords",
|
||||
secondFunction: "getWord",
|
||||
compatible: false,
|
||||
},
|
||||
{
|
||||
firstFunction: "getWord",
|
||||
secondFunction: "pullSection",
|
||||
compatible: false,
|
||||
},
|
||||
];
|
||||
|
||||
it.for(testCases)(
|
||||
`expect $firstFunction and $secondFunction to be compatible $compatible`,
|
||||
({ firstFunction, secondFunction, compatible }) => {
|
||||
//GIVEN
|
||||
getFunboxMock.mockReturnValueOnce([
|
||||
{
|
||||
name: "plus_one",
|
||||
frontendFunctions: [firstFunction],
|
||||
} as FunboxMetadata,
|
||||
{
|
||||
name: "plus_two",
|
||||
frontendFunctions: [secondFunction],
|
||||
} as FunboxMetadata,
|
||||
]);
|
||||
|
||||
//WHEN / THEN
|
||||
expect(Validation.checkCompatibility(["plus_one", "plus_two"])).toBe(
|
||||
compatible,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
34
packages/funbox/package.json
Normal file
34
packages/funbox/package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "@monkeytype/funbox",
|
||||
"private": true,
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "tsup-node --watch",
|
||||
"build": "npm run madge && tsup-node",
|
||||
"test": "vitest run",
|
||||
"madge": " madge --circular --extensions ts ./src",
|
||||
"ts-check": "tsc --noEmit",
|
||||
"lint": "oxlint . --type-aware --type-check",
|
||||
"lint-fast": "oxlint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@monkeytype/schemas": "workspace:*",
|
||||
"@monkeytype/util": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@monkeytype/tsup-config": "workspace:*",
|
||||
"@monkeytype/typescript-config": "workspace:*",
|
||||
"madge": "8.0.0",
|
||||
"oxlint": "1.60.0",
|
||||
"oxlint-tsgolint": "0.21.0",
|
||||
"tsup": "8.4.0",
|
||||
"typescript": "6.0.2",
|
||||
"vitest": "4.1.0"
|
||||
}
|
||||
}
|
||||
15
packages/funbox/src/index.ts
Normal file
15
packages/funbox/src/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { FunboxName } from "@monkeytype/schemas/configs";
|
||||
import { getList, getFunbox, getObject, getFunboxNames } from "./list";
|
||||
import { FunboxMetadata, FunboxProperty } from "./types";
|
||||
import { checkCompatibility, checkForcedConfig } from "./validation";
|
||||
|
||||
export type { FunboxMetadata, FunboxProperty };
|
||||
export { checkCompatibility, checkForcedConfig, getFunbox, getFunboxNames };
|
||||
|
||||
export function getAllFunboxes(): FunboxMetadata[] {
|
||||
return getList();
|
||||
}
|
||||
|
||||
export function getFunboxObject(): Record<FunboxName, FunboxMetadata> {
|
||||
return getObject();
|
||||
}
|
||||
528
packages/funbox/src/list.ts
Normal file
528
packages/funbox/src/list.ts
Normal file
@@ -0,0 +1,528 @@
|
||||
import { FunboxName } from "@monkeytype/schemas/configs";
|
||||
import { FunboxMetadata } 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",
|
||||
"getEmulatedChar",
|
||||
],
|
||||
name: "58008",
|
||||
alias: "numbers",
|
||||
},
|
||||
mirror: {
|
||||
name: "mirror",
|
||||
description: "Everything is mirrored!",
|
||||
properties: ["hasCssFile"],
|
||||
canGetPb: true,
|
||||
difficultyLevel: 3,
|
||||
cssModifications: ["main"],
|
||||
},
|
||||
upside_down: {
|
||||
name: "upside_down",
|
||||
description: "Everything is upside down!",
|
||||
properties: ["hasCssFile"],
|
||||
canGetPb: true,
|
||||
difficultyLevel: 3,
|
||||
cssModifications: ["main"],
|
||||
},
|
||||
nausea: {
|
||||
name: "nausea",
|
||||
description: "I think I'm gonna be sick.",
|
||||
canGetPb: true,
|
||||
difficultyLevel: 2,
|
||||
properties: ["hasCssFile", "ignoreReducedMotion"],
|
||||
cssModifications: ["typingTest"],
|
||||
},
|
||||
round_round_baby: {
|
||||
name: "round_round_baby",
|
||||
description:
|
||||
"...right round, like a record baby. Right, round round round.",
|
||||
canGetPb: true,
|
||||
difficultyLevel: 3,
|
||||
properties: ["hasCssFile", "ignoreReducedMotion"],
|
||||
cssModifications: ["typingTest"],
|
||||
},
|
||||
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.",
|
||||
cssModifications: ["words"],
|
||||
},
|
||||
choo_choo: {
|
||||
canGetPb: true,
|
||||
difficultyLevel: 2,
|
||||
properties: [
|
||||
"hasCssFile",
|
||||
"noLigatures",
|
||||
"conflictsWithSymmetricChars",
|
||||
"ignoreReducedMotion",
|
||||
],
|
||||
name: "choo_choo",
|
||||
description: "All the letters are spinning!",
|
||||
cssModifications: ["words"],
|
||||
},
|
||||
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",
|
||||
"getEmulatedChar",
|
||||
"isCharCorrect",
|
||||
"getWordHtml",
|
||||
],
|
||||
name: "arrows",
|
||||
},
|
||||
rAnDoMcAsE: {
|
||||
description: "raNdomIze ThE CApitaLizatIon Of EveRY LeTtEr.",
|
||||
canGetPb: false,
|
||||
difficultyLevel: 2,
|
||||
properties: ["changesCapitalisation"],
|
||||
frontendFunctions: ["alterText"],
|
||||
name: "rAnDoMcAsE",
|
||||
},
|
||||
sPoNgEcAsE: {
|
||||
description: "I kInDa LiKe HoW iNeFfIcIeNt QwErTy Is.",
|
||||
canGetPb: false,
|
||||
difficultyLevel: 2,
|
||||
properties: ["changesCapitalisation"],
|
||||
frontendFunctions: ["alterText"],
|
||||
name: "sPoNgEcAsE",
|
||||
},
|
||||
capitals: {
|
||||
description: "Capitalize Every Word.",
|
||||
canGetPb: false,
|
||||
difficultyLevel: 1,
|
||||
properties: ["changesCapitalisation"],
|
||||
frontendFunctions: ["alterText"],
|
||||
name: "capitals",
|
||||
},
|
||||
layout_mirror: {
|
||||
description: "Mirror the keyboard layout",
|
||||
canGetPb: true,
|
||||
difficultyLevel: 3,
|
||||
properties: ["changesLayout"],
|
||||
frontendFunctions: ["applyConfig", "rememberSettings"],
|
||||
name: "layout_mirror",
|
||||
},
|
||||
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",
|
||||
],
|
||||
name: "layoutfluid",
|
||||
},
|
||||
earthquake: {
|
||||
description: "Everybody get down! The words are shaking!",
|
||||
canGetPb: true,
|
||||
difficultyLevel: 1,
|
||||
properties: ["hasCssFile", "noLigatures", "ignoreReducedMotion"],
|
||||
name: "earthquake",
|
||||
cssModifications: ["words"],
|
||||
},
|
||||
space_balls: {
|
||||
description: "In a galaxy far far away.",
|
||||
canGetPb: true,
|
||||
difficultyLevel: 0,
|
||||
properties: ["hasCssFile", "ignoreReducedMotion"],
|
||||
name: "space_balls",
|
||||
cssModifications: ["body"],
|
||||
},
|
||||
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",
|
||||
cssModifications: ["body"],
|
||||
},
|
||||
backwards: {
|
||||
description: "...sdrawkcab epyt ot yrt woN",
|
||||
name: "backwards",
|
||||
properties: [
|
||||
"hasCssFile",
|
||||
"conflictsWithSymmetricChars",
|
||||
"wordOrder:reverse",
|
||||
"reverseDirection",
|
||||
],
|
||||
canGetPb: true,
|
||||
frontendFunctions: ["alterText"],
|
||||
difficultyLevel: 3,
|
||||
cssModifications: ["words"],
|
||||
},
|
||||
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: 0,
|
||||
properties: ["changesCapitalisation"],
|
||||
frontendFunctions: ["alterText"],
|
||||
name: "instant_messaging",
|
||||
},
|
||||
underscore_spaces: {
|
||||
description: "Underscores_are_better.",
|
||||
canGetPb: false,
|
||||
difficultyLevel: 1,
|
||||
properties: ["ignoresLanguage", "ignoresLayout", "nospace"],
|
||||
frontendFunctions: ["alterText"],
|
||||
name: "underscore_spaces",
|
||||
},
|
||||
ALL_CAPS: {
|
||||
description: "WHY ARE WE SHOUTING?",
|
||||
canGetPb: false,
|
||||
difficultyLevel: 1,
|
||||
properties: ["changesCapitalisation"],
|
||||
frontendFunctions: ["alterText"],
|
||||
name: "ALL_CAPS",
|
||||
},
|
||||
polyglot: {
|
||||
description: "Use words from multiple languages in a single test.",
|
||||
canGetPb: false,
|
||||
difficultyLevel: 1,
|
||||
properties: ["ignoresLanguage"],
|
||||
frontendFunctions: ["withWords"],
|
||||
name: "polyglot",
|
||||
},
|
||||
asl: {
|
||||
description: "Practice american sign language.",
|
||||
canGetPb: true,
|
||||
difficultyLevel: 1,
|
||||
properties: ["hasCssFile", "noLigatures"],
|
||||
name: "asl",
|
||||
cssModifications: ["words"],
|
||||
},
|
||||
rot13: {
|
||||
description: "Vg znl abg or frpher, ohg vg vf sha gb glcr!",
|
||||
canGetPb: true,
|
||||
difficultyLevel: 1,
|
||||
properties: [],
|
||||
frontendFunctions: ["alterText"],
|
||||
name: "rot13",
|
||||
},
|
||||
no_quit: {
|
||||
description: "You can't restart the test.",
|
||||
canGetPb: true,
|
||||
difficultyLevel: 0,
|
||||
name: "no_quit",
|
||||
},
|
||||
};
|
||||
|
||||
export function getObject(): Record<FunboxName, FunboxMetadata> {
|
||||
return list;
|
||||
}
|
||||
|
||||
export function getFunboxNames(): FunboxName[] {
|
||||
return Object.keys(list) as FunboxName[];
|
||||
}
|
||||
|
||||
export function getList(): FunboxMetadata[] {
|
||||
const out: FunboxMetadata[] = [];
|
||||
for (const name of getFunboxNames()) {
|
||||
out.push(list[name]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function getFunbox(name: FunboxName): FunboxMetadata;
|
||||
export function getFunbox(names: FunboxName[]): FunboxMetadata[];
|
||||
export function getFunbox(
|
||||
nameOrNames: FunboxName | FunboxName[],
|
||||
): FunboxMetadata | FunboxMetadata[] {
|
||||
if (nameOrNames === undefined) return [];
|
||||
if (Array.isArray(nameOrNames)) {
|
||||
const out = nameOrNames.map((name) => getObject()[name]);
|
||||
|
||||
//@ts-expect-error sanity check
|
||||
if (out.includes(undefined)) {
|
||||
throw new Error(
|
||||
"One of the funboxes is invalid: " + nameOrNames.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
return out;
|
||||
} else {
|
||||
const out = getObject()[nameOrNames];
|
||||
|
||||
if (out === undefined) {
|
||||
throw new Error("Invalid funbox name: " + nameOrNames);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
}
|
||||
39
packages/funbox/src/types.ts
Normal file
39
packages/funbox/src/types.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { FunboxName } from "@monkeytype/schemas/configs";
|
||||
|
||||
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"
|
||||
| "reverseDirection"
|
||||
| "ignoreReducedMotion";
|
||||
|
||||
type FunboxCSSModification = "typingTest" | "words" | "body" | "main";
|
||||
|
||||
export type FunboxMetadata = {
|
||||
name: FunboxName;
|
||||
alias?: string;
|
||||
description: string;
|
||||
properties?: FunboxProperty[];
|
||||
frontendForcedConfig?: FunboxForcedConfig;
|
||||
frontendFunctions?: string[];
|
||||
difficultyLevel: number;
|
||||
canGetPb: boolean;
|
||||
cssModifications?: FunboxCSSModification[];
|
||||
};
|
||||
250
packages/funbox/src/validation.ts
Normal file
250
packages/funbox/src/validation.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
import { intersect } from "@monkeytype/util/arrays";
|
||||
import { FunboxForcedConfig, FunboxMetadata } from "./types";
|
||||
import { getFunbox } from "./list";
|
||||
import { ConfigValue, FunboxName } from "@monkeytype/schemas/configs";
|
||||
import { safeNumber } from "@monkeytype/util/numbers";
|
||||
|
||||
export function checkForcedConfig(
|
||||
key: string,
|
||||
value: ConfigValue,
|
||||
funboxes: FunboxMetadata[],
|
||||
): {
|
||||
result: boolean;
|
||||
forcedConfigs?: ConfigValue[];
|
||||
} {
|
||||
if (funboxes.length === 0) {
|
||||
return { result: true };
|
||||
}
|
||||
|
||||
if (key === "words" || key === "time") {
|
||||
if (value === 0) {
|
||||
const fb = funboxes.filter((f) =>
|
||||
f.properties?.includes("noInfiniteDuration"),
|
||||
);
|
||||
if (fb.length > 0) {
|
||||
return {
|
||||
result: false,
|
||||
forcedConfigs: [key === "words" ? 10 : 15],
|
||||
};
|
||||
} else {
|
||||
return { result: true };
|
||||
}
|
||||
} else {
|
||||
return { result: true };
|
||||
}
|
||||
} else {
|
||||
const forcedConfigs: Record<string, ConfigValue[]> = {};
|
||||
// collect all forced configs
|
||||
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 forcedKey in fb.frontendForcedConfig) {
|
||||
if (forcedConfigs[forcedKey] === undefined) {
|
||||
forcedConfigs[forcedKey] = fb.frontendForcedConfig[
|
||||
forcedKey
|
||||
] as ConfigValue[];
|
||||
} else {
|
||||
forcedConfigs[forcedKey] = intersect(
|
||||
forcedConfigs[forcedKey],
|
||||
fb.frontendForcedConfig[forcedKey] as ConfigValue[],
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//check if the key is in forcedConfigs, if it is check the value, if its not, return true
|
||||
if (forcedConfigs[key] === undefined) {
|
||||
return { result: true };
|
||||
} else {
|
||||
if (forcedConfigs[key]?.length === 0) {
|
||||
throw new Error("No intersection of forced configs");
|
||||
}
|
||||
return {
|
||||
result: (forcedConfigs[key] ?? []).includes(value),
|
||||
forcedConfigs: forcedConfigs[key],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function checkCompatibility(
|
||||
funboxNames: FunboxName[],
|
||||
withFunbox?: FunboxName,
|
||||
): boolean {
|
||||
if (funboxNames.length === 0) return true;
|
||||
|
||||
let funboxesToCheck: FunboxMetadata[];
|
||||
|
||||
try {
|
||||
funboxesToCheck = getFunbox(funboxNames);
|
||||
|
||||
if (withFunbox !== undefined) {
|
||||
const toAdd = getFunbox(withFunbox);
|
||||
funboxesToCheck = funboxesToCheck.concat(toAdd);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Error when getting funboxes for a compatibility check:",
|
||||
error,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const allFunboxesAreValid = funboxesToCheck.every((f) => f !== undefined);
|
||||
if (!allFunboxesAreValid) return false;
|
||||
|
||||
const oneWordModifierMax =
|
||||
funboxesToCheck.filter(
|
||||
(f) =>
|
||||
f.frontendFunctions?.includes("getWord") === true ||
|
||||
f.frontendFunctions?.includes("pullSection") === true ||
|
||||
f.frontendFunctions?.includes("withWords") === true,
|
||||
).length <= 1;
|
||||
const oneWordOrderMax =
|
||||
funboxesToCheck.filter(
|
||||
(f) =>
|
||||
f.properties?.find((fp) => fp.startsWith("wordOrder")) !== undefined,
|
||||
).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"),
|
||||
) !== undefined,
|
||||
).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:")) !== undefined ||
|
||||
f.frontendFunctions?.includes("pullSection"),
|
||||
).length <= 1;
|
||||
const onePunctuateWordMax =
|
||||
funboxesToCheck.filter((f) =>
|
||||
f.frontendFunctions?.includes("punctuateWord"),
|
||||
).length <= 1;
|
||||
const oneGetEmulatedCharMax =
|
||||
funboxesToCheck.filter((f) =>
|
||||
f.frontendFunctions?.includes("getEmulatedChar"),
|
||||
).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 oneCssModificationPerElement = Object.values(
|
||||
funboxesToCheck
|
||||
.map((f) => f.cssModifications)
|
||||
.filter((f) => f !== undefined)
|
||||
.flat()
|
||||
.reduce<Record<string, number>>((counts, cssModification) => {
|
||||
counts[cssModification] =
|
||||
(safeNumber(counts[cssModification]) ?? 0) + 1;
|
||||
return counts;
|
||||
}, {}),
|
||||
).every((c) => c <= 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 (
|
||||
oneWordModifierMax &&
|
||||
layoutUsability &&
|
||||
oneNospaceOrToPushMax &&
|
||||
oneChangesWordsVisibilityMax &&
|
||||
oneFrequencyChangesMax &&
|
||||
noFrequencyChangesConflicts &&
|
||||
capitalisationChangePosibility &&
|
||||
noConflictsWithSymmetricChars &&
|
||||
oneCanSpeakMax &&
|
||||
hasLanguageToSpeakAndNoUnspeakable &&
|
||||
oneToPushOrPullSectionMax &&
|
||||
onePunctuateWordMax &&
|
||||
oneGetEmulatedCharMax &&
|
||||
oneCharCheckerMax &&
|
||||
oneCharReplacerMax &&
|
||||
oneChangesCapitalisationMax &&
|
||||
oneCssModificationPerElement &&
|
||||
noConfigConflicts &&
|
||||
oneWordOrderMax
|
||||
);
|
||||
}
|
||||
13
packages/funbox/tsconfig.json
Normal file
13
packages/funbox/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "@monkeytype/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"moduleResolution": "Bundler",
|
||||
"module": "ES6",
|
||||
"target": "ES2015",
|
||||
"lib": ["es2019", "dom"]
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
3
packages/funbox/tsup.config.js
Normal file
3
packages/funbox/tsup.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import { extendConfig } from "@monkeytype/tsup-config";
|
||||
|
||||
export default extendConfig(() => ({ entry: ["src/index.ts"] }));
|
||||
10
packages/funbox/vitest.config.ts
Normal file
10
packages/funbox/vitest.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: "node",
|
||||
coverage: {
|
||||
include: ["**/*.ts"],
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user