An improved implementation of cloze to enable sequential uncovering

The approach used for this guide is based on 'Add Notes…' dialogue: replace selected text with (e.g.) all underscores? - #2 by kleinerpirat.

The first step before starting any scripting is to inspect what Anki provides us with.

Data inspection and preparation of HTML


To see the HTML of any Anki webview, use the add-on AnkiWebView Inspector.

As you can see, the hidden words aren’t hard-coded into the HTML:

<span class="cloze">[...]</span>

That means we cannot simply access the word via the element. What a bummer.
But we’re not limited to {{cloze:Text}}, we could also create an element

<div id="rawText">{{Text}}</div>

to get the full sentence without clozes.

We don’t need to see that element though, so we give it the attribute hidden to make it invisible:

<div id="rawText" hidden>{{Text}}</div>

Defining the goal


Now, it’s very important to define the problem we’d like to solve. We got <span> elements that do not contain words, but in the raw text we got {{ci::word}} syntax that does contain the data we need. Our goal is to match each of these words to the corresponding <span>, so we can incrementally reveal them on the front side.

Accessing data


We have prepared our HTML and defined a goal. Now let’s learn about two important ways to access data.

CSS selectors

In JS, you can select HTML elements the same way you would in CSS. The function is called querySelector for a single element and querySelectorAll for all elements. You can call these functions on any HTML element, for example document.

Let’s see how we can store the two elements that contain our data:

var clozeTextElement = document.querySelector("#clozeText");
var rawTextElement = document.querySelector("#rawText");
/**
#foo means: select the element with id "foo".
   alternatively, you can use the function getElementById, e.g.:
   var clozeTextElement = document.getElementById("clozeText");
*/

We can call querySelectorAll on clozeTextElement to get all these <span class="cloze">[...]</span>:

var clozes = clozeTextElement.querySelectorAll("span.cloze");
// get all span elements of class "cloze" within clozeTextElement

Nice. We stored a NodeList of all the cloze spans. Important: This is not an array! You can use the [...list] syntax to convert that list into an array. But how do we access clozes inside the raw text? It isn’t segmented into elements, just a single string:

image

This is where regular expressions come into play.

RegEx (regular expressions)

RegEx searching is performed on the string data type. There are lots of good internet resources to learn RegEx, so let’s not jump into this rabbit hole here. The following syntax will match every cloze with index 1 and store the contained word in a capturing group:

/\{\{c1::([^{]+)}}/g

It won’t work for nested clozes, but since Anki doesn’t support nesting either, we don’t need to worry about that. You can test it out on https://regexr.com/.

Of course, we also want incremental reveals to work for indices other than 1, so we have to get the current card index dynamically. Luckily for us, Anki hard-codes that index into the className of document.body:

image

So we can access that with another RegEx search:

var cardIdx = parseInt(document.body.className.match(/card(\d+)/)[1]);

// [1] is JS syntax to access an array element at index 1
// RegEx matches are arrays where [0] is the whole match and following indices are capturing groups

// parseInt turns a string into the int data type

There are different ways to declare regular expression objects:

var regEx = /\{\{c1::([^{]+)}}/g;

and

var regEx = new RegExp("\{\{c1::([^{]+)}}", "g");

create the same RegExp object. The advantage of the second syntax is that you can use template literals, i.e. you can use variables to create the RegExp dynamically.

Template literals

This is one of the most useful JS features. To appreciate template literals, you need to know how strings had to be concatenated before that special syntax was introduced. Say we want to create a some HTML dynamically with an index i and some text:

// old way
var spanHTML = "<span class=cloze-" + i + ">" + text + "</span>";

// template literal
var spanHTML = `<span class="cloze-${i}">${text}</span>`;

Script

We covered the basics, so you should be able to understand the following script now:

<script>
  setTimeout(() => {
    const cardIdx = parseInt(document.body.className.match(/card(\d+)/)[1]);
    // Array.from allows us to perform a function on each element of an iterable data type,
    const clozeWords = Array.from(
      document
        // get the element containing {{Text}}
        .getElementById("rawText")
        // templated regex search to get all clozes of index cardIdx
        .innerText.matchAll(new RegExp(`\{\{c${cardIdx}::([^{]+)}}`, "g")),
      // store capturing group 1 of each match in the array
      (match) => match[1]
    );
    // set the word as an attribute on each corresponding cloze span
    [...document.getElementsByClassName("cloze")].forEach((cloze, i) => {
      cloze.setAttribute("word", clozeWords[i]);
    });
  });
</script>

(setTimeout delays the function execution in the event loop, which is required to access Anki clozes.)

Resulting HTML

Now the clozed out words are hard-coded into our spans. This means we have the freedom to do whatever we want with that information.


That’s it for today. Stay tuned for part two, where we create a simple script to reveal the words incrementally on keypress. If you want to try it yourself, take a look at Card Templates: User Input 101 (buttons, keyboard shortcuts, etc.) [Guide].

4 Likes