From b15a4163d8354feaafe6529c6a344e2b5f614f82 Mon Sep 17 00:00:00 2001 From: "Jakob L. Kreuze" Date: Sun, 26 Oct 2025 16:57:18 -0400 Subject: Add custom card types --- README.md | 39 +++++++++++++++++++++++++++++++++++++ example-cards.json | 12 ++++++++++++ srs-anywhere.html | 1 + srs-anywhere.js | 57 +++++++++++++++++++++++++++++++++++++++++++++++------- 4 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 example-cards.json diff --git a/README.md b/README.md index f2c843c..043d1c7 100644 --- a/README.md +++ b/README.md @@ -39,3 +39,42 @@ Keys generated and modified by SRS Anywhere are prefixed by the `_` character. } ] ``` + +### Custom Cards + +Custom card types can be defined in a `srs-anywhere-custom-card-types.js` file. +To register a new card type, add it as a new entry to the `CARD_TYPES` object, where the key is the the name of the card type. +A card type should be an object containing at least the following keys: `validate`, `front`, and `back`. +`validate` takes the card and throws an `Error` if it is malformed. +`front` returns the desired HTML code for the front of the card. +`back` returns the desired HTML code for the back of the card. +Optionally, `showFront` and `showBack` are functions that are evaluted when the front or back of the card are shown, respectively. + +For example, to define a card type for a [cloze deletion test](https://en.wikipedia.org/wiki/Cloze_test): + +```javascript +const CLOZE_REGEX = /\[.*?\]/ig; +CARD_TYPES["clozeAll"] = { + "validate": function (card) { + if (typeof card.back !== "string") { + throw new Error(`malformed card ${card.id}: invalid back (should be string)`) + } + if (!CLOZE_REGEX.test(card.back)) { + throw new Error(`malformed card ${card.id}: contains no cloze; +please enclose deletion in brackets (e.g., [...])`) + } + }, + "front": (card) => card.back.replaceAll(CLOZE_REGEX, '[...]'), + "back": (card) => card.back +}; +``` + +The `type` key in the cards JSON file associates an entry with the custom card type. + +```javascript +{ + "id": "cca013c4-450a-41a1-abed-28abdc73413a", + "type": "clozeAll", + "back": "This is a card containing a [cloze deletion]!" +} +``` diff --git a/example-cards.json b/example-cards.json new file mode 100644 index 0000000..9b856f0 --- /dev/null +++ b/example-cards.json @@ -0,0 +1,12 @@ +[ + { + "id": "d8968711-cd50-42e3-9360-f17a35cc944a", + "front": "This is the front of a card.", + "back": "And this is the back of a card" + }, + { + "id": "a8942790-aea7-41d1-afd1-3cc3c3ecb664", + "front": "Cards can contain arbitrary HTML.", + "back": "Back." + } +] diff --git a/srs-anywhere.html b/srs-anywhere.html index 1e4aa4e..06101ce 100644 --- a/srs-anywhere.html +++ b/srs-anywhere.html @@ -74,5 +74,6 @@ + diff --git a/srs-anywhere.js b/srs-anywhere.js index 6dc82bf..0931b07 100644 --- a/srs-anywhere.js +++ b/srs-anywhere.js @@ -21,6 +21,25 @@ function reportException(e) { showToast(`An error has occurred!\n\n${error}\n\n${error.stack}`); } +/*********************************************************************** + * Card types and dispatch * + ***********************************************************************/ + +const CARD_TYPES = { + "default": { + "validate": function (card) { + if (!card.front) { + throw new Error(`malformed card ${card.id}: missing front`) + } + if (!card.back) { + throw new Error(`malformed card ${card.id}: missing back`) + } + }, + "front": (card) => card.front, + "back": (card) => card.back + } +}; + /*********************************************************************** * Actual FSRS5 algorithm * ***********************************************************************/ @@ -148,11 +167,26 @@ Current session: ${pad(minutes)}:${pad(seconds)}`; function loadCard(card) { window.card.revealed = false; - window.card.front.innerHTML = card.front; window.card.front.style.display = "block"; - window.card.back.innerHTML = card.back; window.card.back.style.display = "none"; window.buttons.style.display = "none"; + + const cardType = CARD_TYPES[card.type]; + + window.card.front.innerHTML = cardType.front(card); + window.card.back.innerHTML = cardType.back(card); + if (typeof cardType.showFront === "function") { + window.card.showFront = () => cardType.showFront(card); + } else { + window.card.showFront = () => null; + } + if (typeof cardType.showBack === "function") { + window.card.showBack = () => cardType.showBack(card); + } else { + window.card.showBack = () => null; + } + + window.card.showFront(); } function flipCard() { @@ -162,13 +196,14 @@ function flipCard() { window.card.revealed = true; - // Reveal the "back". if (window.card.front.style.display !== "block") { window.card.front.style.display = "block"; window.card.back.style.display = "none"; + window.card.showFront(); } else { window.card.front.style.display = "none"; window.card.back.style.display = "block"; + window.card.showBack(); } // Reveal the buttons. @@ -280,11 +315,19 @@ function checkCards(cards) { const card = cards[i]; if (!card.id) { throw new Error("malformed card: missing id") - } else if (!card.front) { - throw new Error(`malformed card ${card.id}: missing front`) - } else if (!card.back) { - throw new Error(`malformed card ${card.id}: missing bacak`) } + + if (!card.type) { + card.type = "default"; + } + if (typeof CARD_TYPES[card.type].front !== "function") { + throw new Error(`malformed card type ${card.type}: front not callable`); + } + if (typeof CARD_TYPES[card.type].back !== "function") { + throw new Error(`malformed card type ${card.type}: back not callable`); + } + CARD_TYPES[card.type].validate(card); + if (!card._scheduled) { card._scheduled = today().toJSON(); } -- cgit v1.3