refactor: refactor iteration over funboxes (@fehmer) (#6275)

This commit is contained in:
Christian Fehmer 2025-02-19 15:46:43 +01:00 committed by GitHub
parent ba7bf22d09
commit ef5263d646
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 212 additions and 123 deletions

View file

@ -48,7 +48,7 @@ import { navigate } from "./route-controller";
import { FirebaseError } from "firebase/app";
import * as PSA from "../elements/psa";
import defaultResultFilters from "../constants/default-result-filters";
import { getActiveFunboxes } from "../test/funbox/list";
import { getFunctionsFromActiveFunboxes } from "../test/funbox/list";
export const gmailProvider = new GoogleAuthProvider();
export const githubProvider = new GithubAuthProvider();
@ -174,8 +174,10 @@ async function getDataAndInit(): Promise<boolean> {
UpdateConfig.saveFullConfigToLocalStorage(true);
//funboxes might be different and they wont activate on the account page
for (const fb of getActiveFunboxes()) {
fb.functions?.applyGlobalCSS?.();
for (const applyGlobalCSS of getFunctionsFromActiveFunboxes(
"applyGlobalCSS"
)) {
applyGlobalCSS();
}
}
AccountButton.loading(false);

View file

@ -35,7 +35,11 @@ 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";
import {
findSingleActiveFunboxWithFunction,
getFunctionsFromActiveFunboxes,
isFunboxActiveWithProperty,
} from "../test/funbox/list";
let dontInsertSpace = false;
let correctShiftUsed = true;
@ -145,7 +149,7 @@ function backspaceToPrevious(): void {
TestInput.input.current = TestInput.input.popHistory();
TestInput.corrected.popHistory();
if (getActiveFunboxes().find((f) => f.properties?.includes("nospace"))) {
if (isFunboxActiveWithProperty("nospace")) {
TestInput.input.current = TestInput.input.current.slice(0, -1);
setWordsInput(" " + TestInput.input.current + " ");
}
@ -194,8 +198,8 @@ async function handleSpace(): Promise<void> {
const currentWord: string = TestWords.words.getCurrent();
for (const fb of getActiveFunboxes()) {
fb.functions?.handleSpace?.();
for (const handleSpace of getFunctionsFromActiveFunboxes("handleSpace")) {
handleSpace();
}
dontInsertSpace = true;
@ -204,9 +208,7 @@ async function handleSpace(): Promise<void> {
void LiveBurst.update(Math.round(burst));
TestInput.pushBurstToHistory(burst);
const nospace =
getActiveFunboxes().find((f) => f.properties?.includes("nospace")) !==
undefined;
const nospace = isFunboxActiveWithProperty("nospace");
//correct word or in zen mode
const isWordCorrect: boolean =
@ -406,8 +408,8 @@ function isCharCorrect(char: string, charIndex: number): boolean {
return true;
}
const funbox = getActiveFunboxes().find((fb) => fb.functions?.isCharCorrect);
if (funbox?.functions?.isCharCorrect) {
const funbox = findSingleActiveFunboxWithFunction("isCharCorrect");
if (funbox) {
return funbox.functions.isCharCorrect(char, originalChar);
}
@ -492,15 +494,11 @@ function handleChar(
const isCharKorean: boolean = TestInput.input.getKoreanStatus();
for (const fb of getActiveFunboxes()) {
if (fb.functions?.handleChar) {
char = fb.functions.handleChar(char);
}
for (const handleChar of getFunctionsFromActiveFunboxes("handleChar")) {
char = handleChar(char);
}
const nospace =
getActiveFunboxes().find((f) => f.properties?.includes("nospace")) !==
undefined;
const nospace = isFunboxActiveWithProperty("nospace") !== undefined;
if (char !== "\n" && char !== "\t" && /\s/.test(char)) {
if (nospace) return;
@ -904,10 +902,8 @@ $(document).on("keydown", async (event) => {
return;
}
for (const fb of getActiveFunboxes()) {
if (fb.functions?.handleKeydown) {
void fb.functions.handleKeydown(event);
}
for (const handleKeydown of getFunctionsFromActiveFunboxes("handleKeydown")) {
void handleKeydown(event);
}
//autofocus
@ -1157,20 +1153,20 @@ $(document).on("keydown", async (event) => {
}
}
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);
}
for (const preventDefaultEvent of getFunctionsFromActiveFunboxes(
"preventDefaultEvent"
)) {
if (
await 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

@ -7,7 +7,7 @@ import * as AccountButton from "./elements/account-button";
//@ts-expect-error
import Konami from "konami";
import * as ServerConfiguration from "./ape/server-configuration";
import { getActiveFunboxes } from "./test/funbox/list";
import { getFunctionsFromActiveFunboxes } from "./test/funbox/list";
import { loadPromise } from "./config";
$(async (): Promise<void> => {
@ -19,8 +19,10 @@ $(async (): Promise<void> => {
$("body").css("transition", "background .25s, transform .05s");
MerchBanner.showIfNotClosedBefore();
for (const fb of getActiveFunboxes()) {
fb.functions?.applyGlobalCSS?.();
for (const applyGlobalCSS of getFunctionsFromActiveFunboxes(
"applyGlobalCSS"
)) {
applyGlobalCSS();
}
$("#app")

View file

@ -9,21 +9,28 @@ import * as FunboxMemory from "./funbox-memory";
import { HighlightMode } from "@monkeytype/contracts/schemas/configs";
import { Mode } from "@monkeytype/contracts/schemas/shared";
import { FunboxName, checkCompatibility } from "@monkeytype/funbox";
import { getActiveFunboxes, getActiveFunboxNames, get } from "./list";
import {
getActiveFunboxes,
getActiveFunboxNames,
get,
getFunctionsFromActiveFunboxes,
isFunboxActiveWithProperty,
getActiveFunboxesWithProperty,
} from "./list";
import { checkForcedConfig } from "./funbox-validation";
export function toggleScript(...params: string[]): void {
if (Config.funbox === "none") return;
for (const fb of getActiveFunboxes()) {
fb.functions?.toggleScript?.(params);
for (const toggleScript of getFunctionsFromActiveFunboxes("toggleScript")) {
toggleScript(params);
}
}
export function setFunbox(funbox: string): boolean {
if (funbox === "none") {
for (const fb of getActiveFunboxes()) {
fb.functions?.clearGlobal?.();
for (const clearGlobal of getFunctionsFromActiveFunboxes("clearGlobal")) {
clearGlobal();
}
}
FunboxMemory.load();
@ -125,9 +132,7 @@ export async function activate(funbox?: string): Promise<boolean | undefined> {
}
if (language.ligatures) {
if (
getActiveFunboxes().find((f) => f.properties?.includes("noLigatures"))
) {
if (isFunboxActiveWithProperty("noLigatures")) {
Notifications.add(
"Current language does not support this funbox mode",
0
@ -194,16 +199,18 @@ export async function activate(funbox?: string): Promise<boolean | undefined> {
}
ManualRestart.set();
for (const fb of getActiveFunboxes()) {
fb.functions?.applyConfig?.();
for (const applyConfig of getFunctionsFromActiveFunboxes("applyConfig")) {
applyConfig();
}
// ModesNotice.update();
return true;
}
export async function rememberSettings(): Promise<void> {
for (const fb of getActiveFunboxes()) {
fb.functions?.rememberSettings?.();
for (const rememberSettings of getFunctionsFromActiveFunboxes(
"rememberSettings"
)) {
rememberSettings();
}
}
@ -220,11 +227,7 @@ async function setFunboxBodyClasses(): Promise<boolean> {
?.split(/\s+/)
.filter((it) => !it.startsWith("fb-")) ?? [];
if (
getActiveFunboxes().some((it) =>
it.properties?.includes("ignoreReducedMotion")
)
) {
if (isFunboxActiveWithProperty("ignoreReducedMotion")) {
currentClasses.push("ignore-reduced-motion");
}
@ -238,14 +241,12 @@ async function setFunboxBodyClasses(): Promise<boolean> {
async function applyFunboxCSS(): Promise<boolean> {
$(".funBoxTheme").remove();
for (const funbox of getActiveFunboxes()) {
if (funbox.properties?.includes("hasCssFile")) {
const css = document.createElement("link");
css.classList.add("funBoxTheme");
css.rel = "stylesheet";
css.href = "funbox/" + funbox.name + ".css";
document.head.appendChild(css);
}
for (const funbox of getActiveFunboxesWithProperty("hasCssFile")) {
const css = document.createElement("link");
css.classList.add("funBoxTheme");
css.rel = "stylesheet";
css.href = "funbox/" + funbox.name + ".css";
document.head.appendChild(css);
}
return true;
}

View file

@ -59,14 +59,102 @@ export function getActiveFunboxNames(): FunboxName[] {
return stringToFunboxNames(Config.funbox);
}
/**
* Get all active funboxes defining the given property
* @param property
* @returns list of matching funboxes, empty list if none matching
*/
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]);
/**
* Find a single active funbox defining the given property
* @param property
* @returns the active funbox if any, `undefined` otherwise.
* @throws Error if there are multiple funboxes defining the given property
*/
export function findSingleActiveFunboxWithProperty(
property: FunboxProperty
): FunboxMetadataWithFunctions | undefined {
const matching = getActiveFunboxesWithProperty(property);
if (matching.length == 0) return undefined;
if (matching.length == 1) return matching[0];
throw new Error(
`Expecting exactly one funbox with property "${property} but found ${matching.length}`
);
}
/**
* Check if there is an active funbox with the given property name
* @param property property name
* @returns
*/
export function isFunboxActiveWithProperty(property: FunboxProperty): boolean {
return getActiveFunboxesWithProperty(property).length > 0;
}
type MandatoryFunboxFunction<F extends keyof FunboxFunctions> = Exclude<
FunboxFunctions[F],
undefined
>;
type FunboxWithFunction<F extends keyof FunboxFunctions> =
FunboxMetadataWithFunctions & {
functions: Record<F, MandatoryFunboxFunction<F>>;
};
/**
* Get all active funboxes implementing the given function
* @param functionName function name
* @returns list of matching funboxes, empty list if none matching
*/
export function getActiveFunboxesWithFunction<F extends keyof FunboxFunctions>(
functionName: F
): FunboxWithFunction<F>[] {
return getActiveFunboxes().filter(
(fb) => fb.functions?.[functionName]
) as FunboxWithFunction<F>[];
}
/**
* Get requested, implemented functions from all active funboxes
* @param functionName name of the function
* @returns array of each implemented requested function of all active funboxes
*/
export function getFunctionsFromActiveFunboxes<F extends keyof FunboxFunctions>(
functionName: F
): MandatoryFunboxFunction<F>[] {
return getActiveFunboxesWithFunction(functionName).map(
(it) => it.functions[functionName]
);
}
/**
* Check if there is an active funbox implemenging the given function
* @param functionName function name
* @returns
*/
export function isFunboxActiveWithFunction(
functionName: keyof FunboxFunctions
): boolean {
return getActiveFunboxesWithFunction(functionName).length > 0;
}
/**
* Find a single active funbox implementing the given function name
* @param functionName
* @returns the active funbox if any, `undefined` otherwise.
* @throws Error if there are multiple funboxes implementing the function name
*/
export function findSingleActiveFunboxWithFunction<
F extends keyof FunboxFunctions
>(functionName: F): FunboxWithFunction<F> | undefined {
const matching = getActiveFunboxesWithFunction(functionName);
if (matching.length == 0) return undefined;
if (matching.length == 1) return matching[0] as FunboxWithFunction<F>;
throw new Error(
`Expecting exactly one funbox implementing "${functionName} but found ${matching.length}`
);
}

View file

@ -37,7 +37,11 @@ import type {
} from "chartjs-plugin-annotation";
import Ape from "../ape";
import { CompletedEvent } from "@monkeytype/contracts/schemas/results";
import { getActiveFunboxes, getFromString } from "./funbox/list";
import {
getActiveFunboxes,
getFromString,
isFunboxActiveWithProperty,
} from "./funbox/list";
import { getFunboxesFromString } from "@monkeytype/funbox";
let result: CompletedEvent;
@ -678,10 +682,7 @@ function updateTestType(randomQuote: Quote | null): void {
testType += " " + ["short", "medium", "long", "thicc"][randomQuote.group];
}
}
const ignoresLanguage =
getActiveFunboxes().find((f) =>
f.properties?.includes("ignoresLanguage")
) !== undefined;
const ignoresLanguage = isFunboxActiveWithProperty("ignoresLanguage");
if (Config.mode !== "custom" && !ignoresLanguage) {
testType += "<br>" + Strings.getLanguageDisplayString(result.language);
}

View file

@ -64,7 +64,11 @@ import {
CustomTextDataWithTextLen,
} from "@monkeytype/contracts/schemas/results";
import * as XPBar from "../elements/xp-bar";
import { getActiveFunboxes } from "./funbox/list";
import {
findSingleActiveFunboxWithFunction,
getActiveFunboxes,
getFunctionsFromActiveFunboxes,
} from "./funbox/list";
import { getFunboxesFromString } from "@monkeytype/funbox";
import * as CompositionState from "../states/composition";
@ -108,8 +112,8 @@ export function startTest(now: number): boolean {
TestTimer.clear();
Monkey.show();
for (const fb of getActiveFunboxes()) {
fb.functions?.start?.();
for (const start of getFunctionsFromActiveFunboxes("start")) {
start();
}
try {
@ -331,8 +335,8 @@ export function restart(options = {} as RestartOptions): void {
await init();
await PaceCaret.init();
for (const fb of getActiveFunboxes()) {
fb.functions?.restart?.();
for (const restart of getFunctionsFromActiveFunboxes("restart")) {
restart();
}
if (Config.showAverage !== "off") {
@ -558,12 +562,8 @@ export async function addWord(): Promise<void> {
console.debug("Not adding word, all words generated");
return;
}
const sectionFunbox = getActiveFunboxes().find(
(f) => f.functions?.pullSection
);
if (sectionFunbox?.functions?.pullSection) {
const sectionFunbox = findSingleActiveFunboxWithFunction("pullSection");
if (sectionFunbox) {
if (TestWords.words.length - TestState.activeWordIndex < 20) {
const section = await sectionFunbox.functions.pullSection(
Config.language

View file

@ -9,7 +9,7 @@ import {
CompletedEvent,
IncompleteTest,
} from "@monkeytype/contracts/schemas/results";
import { getActiveFunboxes } from "./funbox/list";
import { isFunboxActiveWithProperty } from "./funbox/list";
type CharCount = {
spaces: number;
@ -357,7 +357,7 @@ function countChars(): CharCount {
spaces++;
}
}
if (getActiveFunboxes().find((f) => f.properties?.includes("nospace"))) {
if (isFunboxActiveWithProperty("nospace")) {
spaces = 0;
correctspaces = 0;
}

View file

@ -30,7 +30,10 @@ import {
TimerOpacity,
} from "@monkeytype/contracts/schemas/configs";
import { convertRemToPixels } from "../utils/numbers";
import { getActiveFunboxes } from "./funbox/list";
import {
findSingleActiveFunboxWithFunction,
getFunctionsFromActiveFunboxes,
} from "./funbox/list";
import * as TestState from "./test-state";
async function gethtml2canvas(): Promise<typeof import("html2canvas").default> {
@ -337,10 +340,10 @@ function getWordHTML(word: string): string {
let newlineafter = false;
let retval = `<div class='word'>`;
const funbox = getActiveFunboxes().find((f) => f.functions?.getWordHtml);
const funbox = findSingleActiveFunboxWithFunction("getWordHtml");
const chars = Strings.splitIntoCharacters(word);
for (const char of chars) {
if (funbox?.functions?.getWordHtml) {
if (funbox) {
retval += funbox.functions.getWordHtml(char, true);
} else if (char === "\t") {
retval += `<letter class='tabChar'><i class="fas fa-long-arrow-alt-right fa-fw"></i></letter>`;
@ -653,8 +656,10 @@ export async function screenshot(): Promise<void> {
}
(document.querySelector("html") as HTMLElement).style.scrollBehavior =
"smooth";
for (const fb of getActiveFunboxes()) {
fb.functions?.applyGlobalCSS?.();
for (const applyGlobalCSS of getFunctionsFromActiveFunboxes(
"applyGlobalCSS"
)) {
applyGlobalCSS();
}
}
@ -695,8 +700,8 @@ export async function screenshot(): Promise<void> {
$(".highlightContainer").addClass("hidden");
if (revertCookie) $("#cookiesModal").addClass("hidden");
for (const fb of getActiveFunboxes()) {
fb.functions?.clearGlobal?.();
for (const clearGlobal of getFunctionsFromActiveFunboxes("clearGlobal")) {
clearGlobal();
}
(document.querySelector("html") as HTMLElement).style.scrollBehavior = "auto";
@ -844,7 +849,7 @@ export async function updateActiveWordLetters(
}
}
const funbox = getActiveFunboxes().find((fb) => fb.functions?.getWordHtml);
const funbox = findSingleActiveFunboxWithFunction("getWordHtml");
const inputChars = Strings.splitIntoCharacters(input);
const currentWordChars = Strings.splitIntoCharacters(currentWord);
@ -854,8 +859,8 @@ export async function updateActiveWordLetters(
let currentLetter = currentWordChars[i] as string;
let tabChar = "";
let nlChar = "";
if (funbox?.functions?.getWordHtml) {
const cl = funbox.functions?.getWordHtml(currentLetter);
if (funbox) {
const cl = funbox.functions.getWordHtml(currentLetter);
if (cl !== "") {
currentLetter = cl;
}

View file

@ -16,7 +16,12 @@ 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";
import {
findSingleActiveFunboxWithFunction,
getActiveFunboxes,
getFunctionsFromActiveFunboxes,
isFunboxActiveWithFunction,
} from "./funbox/list";
function shouldCapitalize(lastChar: string): boolean {
return /[?!.؟]/.test(lastChar);
@ -35,9 +40,8 @@ export async function punctuateWord(
const lastChar = Strings.getLastChar(previousWord);
const funbox = getActiveFunboxes()?.find((fb) => fb.functions?.punctuateWord);
if (funbox?.functions?.punctuateWord) {
const funbox = findSingleActiveFunboxWithFunction("punctuateWord");
if (funbox) {
return funbox.functions.punctuateWord(word);
}
if (
@ -301,10 +305,8 @@ async function applyEnglishPunctuationToWord(word: string): Promise<string> {
}
function getFunboxWordsFrequency(): Wordset.FunboxWordsFrequency | undefined {
const funbox = getActiveFunboxes().find(
(fb) => fb.functions?.getWordsFrequencyMode
);
if (funbox?.functions?.getWordsFrequencyMode) {
const funbox = findSingleActiveFunboxWithFunction("getWordsFrequencyMode");
if (funbox) {
return funbox.functions.getWordsFrequencyMode();
}
return undefined;
@ -313,9 +315,9 @@ function getFunboxWordsFrequency(): Wordset.FunboxWordsFrequency | undefined {
async function getFunboxSection(): Promise<string[]> {
const ret = [];
const funbox = getActiveFunboxes().find((fb) => fb.functions?.pullSection);
const funbox = findSingleActiveFunboxWithFunction("pullSection");
if (funbox?.functions?.pullSection) {
if (funbox) {
const section = await funbox.functions.pullSection(Config.language);
if (section === false || section === undefined) {
@ -338,9 +340,9 @@ function getFunboxWord(
wordIndex: number,
wordset?: Wordset.Wordset
): string {
const funbox = getActiveFunboxes()?.find((fb) => fb.functions?.getWord);
const funbox = findSingleActiveFunboxWithFunction("getWord");
if (funbox?.functions?.getWord) {
if (funbox) {
word = funbox.functions.getWord(wordset, wordIndex);
}
return word;
@ -351,10 +353,8 @@ function applyFunboxesToWord(
wordIndex: number,
wordsBound: number
): string {
for (const fb of getActiveFunboxes()) {
if (fb.functions?.alterText) {
word = fb.functions.alterText(word, wordIndex, wordsBound);
}
for (const alterText of getFunctionsFromActiveFunboxes("alterText")) {
word = alterText(word, wordIndex, wordsBound);
}
return word;
}
@ -609,11 +609,7 @@ export async function generateWords(
hasNewline: false,
};
const sectionFunbox = getActiveFunboxes().find(
(fb) => fb.functions?.pullSection
);
isCurrentlyUsingFunboxSection =
sectionFunbox?.functions?.pullSection !== undefined;
isCurrentlyUsingFunboxSection = isFunboxActiveWithFunction("pullSection");
const wordOrder = getWordOrder();
console.debug("Word order", wordOrder);
@ -634,8 +630,8 @@ export async function generateWords(
wordList = wordList.reverse();
}
const funbox = getActiveFunboxes().find((fb) => fb.functions?.withWords);
if (funbox?.functions?.withWords) {
const funbox = findSingleActiveFunboxWithFunction("withWords");
if (funbox) {
currentWordset = await funbox.functions.withWords(wordList);
} else {
currentWordset = await Wordset.withWords(wordList);
@ -864,9 +860,7 @@ export async function getNextWord(
throw new WordGenError("Random word contains spaces");
}
const usingFunboxWithGetWord = getActiveFunboxes().some(
(fb) => fb.functions?.getWord
);
const usingFunboxWithGetWord = isFunboxActiveWithFunction("getWord");
if (
Config.mode !== "custom" &&