Skip to content

Cloze deletion - #29

Merged
Aduneer merged 1 commit into
mainfrom
cloze-deletion
Aug 27, 2026
Merged

Cloze deletion#29
Aduneer merged 1 commit into
mainfrom
cloze-deletion

Conversation

@Aduneer

@Aduneer Aduneer commented Aug 27, 2026

Copy link
Copy Markdown
Owner

Wrap a word in {{braces}} and the card is asked with that word taken out.

The {{mitochondrion}} is the powerhouse of the cell,,biology
┌──────────────────────────────────────────┐
│ Box 1  ·  new  ·  biology                │
├──────────────────────────────────────────┤
│                                          │
│   The [...] is the powerhouse of the cell│
│                                          │
└──────────────────────────────────────────┘

It is a way of writing a card, not a new kind of card: one Leitner box, one
id, one due date, and tags, hints, undo, editing, images and the log all work
on it unchanged. New module src/cloze.*; still zero dependencies.

The deck format did not change, but it did gain a floor

The answer column is empty, because the answers live inside the question. That
makes a cloze card a whole card on a single field, so:

  • card_from_csv accepts a one-field line when — and only when — it holds a
    real deletion. Without that test every line of every text file would be a
    card, and the rule that keeps a mistyped path from being overwritten depends
    on that not happening.
  • card_to_csv lowers its three-column floor to one for such a card.

Both halves are needed, or a deck of bare sentences would be rewritten as
sentence,, the first time it was opened — the same rule that already keeps a
question,answer,tags deck from growing trailing commas. No new column, so
nothing about this can put a file-synced deck in conflict between versions.

The one syntax decision

Anki's syntax, so decks paste across in both directions: {{text}},
{{c1::text}}, {{c1::text::hint}}, and | alternatives inside a deletion.

The hint separator collides with the :: a C++ deck is made of. Resolved by
looking for a hint only after a cN:: prefix:

{{std::vector}}          → answer "std::vector"
{{c1::std::vector}}      → answer "std", hint "vector"   (Anki's own reading)
{{c1::std::vector::}}    → answer "std::vector", no hint

Several holes: one card, one schedule

They are asked one at a time within a single presentation, each finished blank
filled in for the next, with what it earned kept on screen underneath.

│ Box 1  ·  new  ·  geography  ·  blank 2 of 3             │
│   Paris is the capital of [...], on the [river]          │

  ✅  blank 1  Paris

The card is then scheduled once, on the worst of the answers. The
alternative — one prompt per hole, each moving the box — would promote a
three-blank card three boxes in one sitting. One review, one log event,
whatever the sentence is made of.

Structurally this is why the review loop now walks a vector<Ask> per card
instead of a single expected answer. That also collapsed the reversed and
normal paths into one thing built up front rather than rediscovered inside the
prompt loop, and it is what makes the hint/quit/audio keys withheld per
blank
rather than per card.

Two deliberate exclusions

  • Never asked reversed. A cloze card's answers are already inside its
    question, so there is nothing to turn round. A reversed session asks such
    cards forwards — which is what lets a mixed deck be studied backwards
    without splitting it in two — and logs them as n, because that is how they
    were actually asked.
  • --generate-audio skips them. A recording is of the question, and a
    cloze question read aloud is the answer read aloud; there is also no single
    rendering to make, since a sentence with three holes is asked three ways
    against a column that holds one file. The a key still works during review:
    the open blank is spoken as the word "blank", and the whole sentence is read
    once the card is done.

Tests

make check is now 1,344 unit checks and 58 golden cases (was 1,233 / 50).
Clean under gcc and clang with -Werror and -fsanitize=address,undefined -fno-sanitize-recover=undefined, under the env -i clean room, and from a
fresh clone.

Six new golden cases: a single blank; three blanks with a hint and a grouped
repeat; a reversed session over a mixed deck; undo restoring a part-answered
card; per-blank key withholding on a vim deck whose first blank is q; and
--generate-audio skipping one.

A seventh is unrelated and was in the way: the in-app help screen (h) had
no end-to-end coverage at all
— the existing help case is --help, which
is a different screen printed by different code. menu-help was added before
print_help was touched.

Also: examples/cloze-science.csv (12 cards, round-trips byte for byte), the
manage list shows a cloze card as it will be asked rather than as a row of
braces, and adding or editing one stops asking for an answer it would never
use.

Wrap a word in {{braces}} and the card is asked with that word taken out.
It is a way of writing a card, not a new kind of card: one Leitner box,
one id, one due date, and tags, hints, undo, editing, images and the log
all work on it unchanged.

The answer column is empty, because the answers live inside the question.
That makes a cloze card a whole card on a single field, so card_from_csv
accepts a one-field line when -- and only when -- it holds a real
deletion, and card_to_csv lowers its three-column floor to one for such a
card. Both halves are needed or a deck of bare sentences would be
rewritten as "sentence,," the first time it was opened, which is the same
rule that already keeps a question,answer,tags deck from growing trailing
commas.

The syntax is Anki's, so decks paste across in both directions. The one
place it needed a decision is the hint separator, which collides with the
"::" a C++ deck is made of. Resolved by looking for a hint only after a
"cN::" prefix: {{std::vector}} is one answer, {{c1::std::vector}} is Anki's
reading of answer-plus-hint, and {{c1::std::vector::}} says so explicitly.

A sentence with several holes is asked one hole at a time within a single
presentation and scheduled once, on the worst of the answers. The
alternative -- one prompt per hole, each moving the box -- would promote a
three-blank card three boxes in one sitting. So the review loop now walks
a vector<Ask> per card instead of a single expected answer, which is also
what collapsed the reversed and normal paths into one thing built up
front rather than rediscovered inside the prompt loop.

Two deliberate exclusions. A cloze card is never asked reversed: its
answers are already inside its question, so a reversed session asks it
forwards and logs it as 'n', because that is how it was actually asked.
And --generate-audio skips them, since a recording is of the question and
a cloze question read aloud is the answer read aloud; the `a` key still
works during review, speaking the open blank as the word "blank".

Also here, because they were in the way:

- The in-app help screen (`h`) had no golden coverage at all. The
  existing `help` case is `--help`, which is a different screen printed
  by different code. Added `menu-help` before touching print_help.
- Six cloze golden cases, covering a single blank, three blanks with a
  hint and a grouped repeat, a reversed session over a mixed deck, undo
  restoring a part-answered card, per-blank key withholding on a vim
  deck, and --generate-audio skipping one.
@Aduneer
Aduneer merged commit 81e4932 into main Aug 27, 2026
6 checks passed
@Aduneer
Aduneer deleted the cloze-deletion branch August 27, 2026 08:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant