Saturday, March 14, 2015

Random Question Generator on Anki using JavaScript

Introduction

Anki is brilliant. It has really lived up to its motto ‘Remember Anything, Remember Anywhere, Remember Efficiently’ for being my go-to way to remember notes and other information. This is made all the more powerful through its HTML-based card system, which includes support for complex behaviour via JavaScript. Unfortunately, utilising JavaScript to perform such behaviour is less than straightforward.

Fortunately, I've done all the hard work for you, so here's a guide on using JavaScript in Anki. I used it to generate physics questions and associated solutions, but the general idea should be applicable to any use case.

Getting JavaScript into Anki

Although the ‘Add Note’ screen in Anki allows HTML and JavaScript input, any JavaScript input here is automatically executed during the editing stage, which is not exactly helpful for creating dynamic content.
Instead, JavaScript needs to be set up from within the card template. To do this, create a new note type by accessing the Add Note screen, then clicking the note type (Basic, in the images above), Manage, Add, Add: Basic, OK.
Give it a name like ‘Script’, then go back to the Choose Note Type window and choose the new note type.
Click the Cards… button, and at the beginning of the Front Template, add some JavaScript. If all goes well, the Front Preview should show the results of the JavaScript code.

Custom Per-Card JavaScript

Now creating a new note type for every single piece of JavaScript you want to add is impractical, so let's find a way to make the note type execute code based on a per-card field. At the beginning of the Front Template, replace the previous JavaScript code with:
var code = (function () {/* {{Script}} */}).toString();
code = code.replace(/^[^\/]+\/\*!?/, "").replace(/\*\/[^\/]+$/, ""); //Strip beginning/ending comments.
code = code.replace(/<div>/g, "\n").replace(/<\/div>/g, "\n").replace(/<br \/>/g, "\n"); //Strip HTML.
code = code.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&"); //Replace special symbols.
eval(code);
The first two lines take the HTML code of a per-card Script field and store it in a JavaScript variable. The third and fourth lines strip extraneous HTML code from the variable, hopefully turning it into executable JavaScript code. Depending on which symbols you use, you may need to add more replacements to the fourth line. The final line executes the JavaScript code.

Right now, the code won't work, because the Script field has not been declared. Close the Card Types for Script window, click the Fields… button, and add a new Script field.
Now, any code in the Script field will be executed each time the card is shown, once for the front, then again later for the back.
In order to access the contents of the card more easily from within JavaScript, some changes can be made to the Front Template and Back Template, giving each side an id attribute:
Note that if you want to access these, you will need to delay the call until after the DOM has finished loading, by wrapping your JavaScript in window.setTimeout(function(){}, 0); or something similar.

Data Persistence

Depending on what you want to do, the above technique will work perfectly. More likely, however, is that the fact the JavaScript is executed twice will cause problems for you. The images below show a common scenario, wherein the generated question changes when flipping sides of the card. Not useful for checking your answer!
Thus, we need a way of storing data within the JavaScript code across executions of the same card. Unfortunately, typical ways of doing this, localStorage and document.cookie are disabled. From a ‘normal’ JavaScript point of view, each execution is entirely separate from the other.

Thankfully, Anki exposes a behind-the-scenes window.py object. The internal functions exposed by the object are pretty useless, but the instance of the object is shared between executions of the same card (and reset if the card is shown again later), meaning that it can be used to store data between executions.

Using the below code to store randomly generated data between executions allows us to overcome this restriction.
window.setTimeout(function() {
var o = (typeof py === "undefined") ? {} : py; //Attempt to use Anki's Bridge object to store data across sides.
o.data = o.data || {};

o.data.num = o.data.num || Math.random();
if (document.getElementById("front")) document.getElementById("front").innerHTML = "The number on the front is " + o.data.num;
if (document.getElementById("back")) document.getElementById("back").innerHTML = "The number on the back is " + o.data.num;
}, 0); //Execute after Anki has loaded its Bridge object.


Anki+JavaScript in Action