First read my article about Overlapping and Nested Clozes.

If you have nested clozes such as:

Cloze 1: In 14[...], Columbus Sailed the Ocean Blue (answer: 92)

and

Cloze 2: In [...], Columbus Sailed the Ocean Blue (answer: 1492)

Then remembering cloze 2 means that you remembered cloze 1 as well and both cloze deletions should be scored.

In my previous article, I published some code for making overlapping and nested clozes efficiently. Using that code, the nested cloze deletions above could be make like so:

In {2::14{1::92::1}::2}, Columbus Sailed the Ocean Blue

Here’s a new version of the cloze-handling functions along with a function I wrote to find the identity of any clozes that are nested within the cloze you’re currently being tested on.

This new mkClozes() function will call the findNestedClozes() function on the text of each cloze deletion.

mkClozes then returns a numbered dictionary of cards where each entry contains the question, the answer, and a list of IDs of any nested clozes. See the code below for a better description

New Hide-Me Feature

Create a cloze deletion with ‘hide-me’ as the hint, and it’ll be hidden in both the question and answer. This is useful when you have a long passage for the cloze deletion but you just want to focus on part of it for one of the cloze deletions.

e.g. In {1::1492::1}, Columbus Sailed the Ocean Blue. {1::And long passage about some other detail that you don’t really need to {2::see::2} when testing yourself on cloze 1. :hide-me:1}

This will produce a card like: In […], Columbus Sailed the Ocean Blue., but the long passage will only be hidden when you’re testing yourself on the date (cloze 1). When you test yourself on cloze 2, the entire passage will be shown.

cloze.py

#!/usr/bin/python3
# Cloze-Deletion Parsing Function
import re

# example cloze text
#s = "\\{1::(escaped cloze start) {1::\"hello world\":hint1:1} is \\::2}(escaped cloze end) {3::my {6::first:bite me:6} cloze:eat me:3} and {2::\"goodbye {4::world\":cruel:2} is my:dude, my penis is on fire!:4} {5::{6::second:third:6} cloze:c-zizzy:5} {100::yes any arbitrary number works::100}"

# function to remove the cloze codes from other cloze deletions
def removeClozeCodes(text):
    text = re.sub(r'(?<!\\){[0-9]+::','',text,0)
    text = re.sub(r'(?<!\\):[^:]*?:[0-9]+}','',text,0)
    text = re.sub(r'\\:',':',text,0)
    text = re.sub(r'\\{','{',text,0)
    return text

# function to find nested clozes
def findNestedClozes(s):
    startMatch = r'(?<!\\){([0-9]+)::'
    endMatch = r'(?<!\\):([^:]*?):([0-9]+)}'
    i = re.finditer(startMatch, s)
    starts = set()
    stops = set()
    for m in i:
        n = int(m.group(1))
        if not n in starts:
            starts.add(n)
    i = re.finditer(endMatch, s)
    for m in i:
        n = int(m.group(2))
        if not n in stops:
            stops.add(n)
    nested = set()
    for n in starts:
        if n in stops:
            nested.add(n)
    return nested


# mkCLozes
## Returns a numbered dictionary of cards
## cards[n] = card with cloze number n deleted
## cards[n][0] = list of tuples (string, type) for question side
## cards[n][1] = list of tuples (string, type) for answer side
## cards[n][2] = set of clozes nested within this cloze
## type codes:
##    0 = normal text
##    1 = cloze deletion text
##    2 = hint text
def mkClozes(s):
    startMatch = r'(?<!\\){([0-9]+)::'
    endMatch = r'(?<!\\):([^:]*?):([0-9]+)}'
    i = re.finditer(startMatch, s)
    starts = {}
    ends = {}
    clozeHint = {}
    cards = {}
    for m in i:
        n = int(m.group(1))
        if not n in starts:
            starts[n] = []
        starts[n].append(m.span())
    
    i = re.finditer(endMatch, s)
    for m in i:
        n = int(m.group(2))
        if n not in clozeHint:
            clozeHint[n] = []
        hint = m.group(1)
        clozeHint[n].append(hint)
        if not n in ends:
            ends[n] = []
        ends[n].append(m.span())
    
    if len(starts) != len(ends):
        raise Exception("Mismatching starts and ends to clozes in string: %s" % s)
        return None
    
    for n in starts:
        # the next two lists will be lists of tuples
        # (string, type) 0=normal  1=cloze 2=hint
        clozeQuestion = []
        clozeAnswer = []
        # the 'nested' list contains nested cloze IDs
        nested = []
        d = 0
        for i in range(len(starts[n])):
            a = starts[n][i][0]
            b = starts[n][i][1]
            clozeQ = s[d:a]
            clozeA = s[d:a]
            # all cloze tags left in new string should be removed
            # they are from other clozes
            clozeQ = removeClozeCodes(clozeQ)
            clozeA = removeClozeCodes(clozeA)
            # add strings to list
            clozeQuestion.append((clozeQ,0))
            clozeAnswer.append((clozeA,0))
            c = ends[n][i][0]
            d = ends[n][i][1]
            clz = s[b:c]
            # remove any cloze tags left by other cloze deletions
            if clozeHint[n][i] != 'hide-me':
                nested.extend(findNestedClozes(clz))
            clz = removeClozeCodes(clz)
            if clozeHint[n][i]:
                if clozeHint[n][i] == 'hide-me':
                    # secret code to hide passage for this cloze
                    pass
                else:
                    clozeAnswer.append((clz,1))
                    clozeQuestion.append(("[",1))
                    clozeQuestion.append((clozeHint[n][i],2))
                    clozeQuestion.append(("]",1))
            else:
                clozeAnswer.append((clz,1))
                clozeQuestion.append(("[...]",1))
        clozeQ = s[d:]
        clozeA = s[d:]
        # all cloze tags left in new string should be removed
        # they are from other clozes
        clozeQ = removeClozeCodes(clozeQ)
        clozeA = removeClozeCodes(clozeA)
        # add strings to list
        clozeQuestion.append((clozeQ,0))
        clozeAnswer.append((clozeA,0))
        # add strings to card dictionary
        cards[n] = (clozeQuestion, clozeAnswer, nested)
    return cards