summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJakob L. Kreuze <zerodaysfordays@sdf.org>2025-10-26 16:57:18 -0400
committerJakob L. Kreuze <zerodaysfordays@sdf.org>2025-10-26 16:57:18 -0400
commitb15a4163d8354feaafe6529c6a344e2b5f614f82 (patch)
tree09e4b536bcb5a39a8825cad03d18462dad4d3229
parent939594d1cfd2d11f7acc300f117204c050af2d13 (diff)
Add custom card types
-rw-r--r--README.md39
-rw-r--r--example-cards.json12
-rw-r--r--srs-anywhere.html1
-rw-r--r--srs-anywhere.js57
4 files changed, 102 insertions, 7 deletions
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 <strong>arbitrary</strong> <em>HTML</em>.",
+ "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 @@
</div>
<script src="srs-anywhere.js"></script>
+ <script src="srs-anywhere-custom-card-types.js"></script>
</body>
</html>
diff --git a/srs-anywhere.js b/srs-anywhere.js
index 6dc82bf..0931b07 100644
--- a/srs-anywhere.js
+++ b/srs-anywhere.js
@@ -22,6 +22,25 @@ function reportException(e) {
}
/***********************************************************************
+ * 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();
}