Putting some make up on my org-mode flashcards
Lately I’ve been playing with org-drill, an extension to Emacs org-mode implementing a spaced repetition algorithm for flashcard drills. That is, org-drill lets you write flashcards in the form of org outlines plus special syntax, and have study sessions where the cards are presented to you using some special algorithms that should improve retention. The flashcards can have hidden text/images, present hints, have multiple faces, together with other useful settings.
One of the main points of org-drill is of course its integration in org-mode, letting you keep your flashcards together with your other plain-text notes. The flashcards are no different from any other org outline, apart from some special properties which org-drill saves in the :PROPERTIES: drawer, and some syntax to indicate parts of text that should be hidden in the flashcard (to prompt a recall on the side of the student), and hints (to help the recall).
I find org-drill very useful to have quick sessions to strengthen key facts from what I’m studying. But I wanted to integrate it better with my org-mode workflow. That is, I wanted the key facts that I keep in my notes to be treated as flashcards when needed, and as normal notes otherwise. The org-drill clozes (with my customized delimiters) look something like this:
- The answer to the ultimate question of life, the universe and everything is !| 42 || six by nine |! , as computed by !| Deep Thought |!
If you are using org-drill and didn’t customize the delimiters, you’ll have [ and ] instead of !| and |! . You can probably already see that the !| fact || hint |! syntax can be a little annoying when reading the notes.
To solve this problem I’ve written some functions that get the special syntax out of the way when exporting the org-mode file, and when reading the notes as notes – not as flashcards. Elisp to the rescue!
Here’s a before-after comparison:
Much neater! As a plus, the special syntax is also hidden when viewing flash-cards, making them neater as well:
And the export works just as well:
I’m pretty happy with the result. It is a small thing but it makes a huge difference in that I can just write my notes as I normally do, very quickly make them flashcards if I need to, and seamlessly go back and forth between the two representations depending on the way I’m studying.
I’d like to push this further, as this little trick only works when my notes are a sequence of distinct and well-organized little pieces of information. This happens to be the case for the probability notes I used as an example here, but it needn’t always be. Sometimes it makes more sense to maybe have long-running paragraphs in a more discursive form, depending on the type of information the notes are capturing. It would be great to use a special syntax to create flashcards from tokenized concepts inside the paragraph, and hide/show that as well when need be. Who knows, maybe this will be a side project I’ll actually manage to work on!
Anyway, here is the small section of code accomplishing what I’ve described (and also my first attempt to write non-trivial emacs lisp… I hope I’m doing it the right way). Note that this code doesn’t assume the use of my custom delimiters, the default delimiters are supported. From the countless times I stole elisp snippets from someone’s blog, I know that there’s somebody out there that will appreciate this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
(require 'org-drill)
;; remove clozes when exporting ;;
(defun gsc/drill-compute-cloze-regexp ()
"Same regular expression as in org-drill-cloze-regexp,
but adding a group for the first delimiter, so that it can be
distinguished easily in a match."
(concat "\\("
(regexp-quote org-drill-left-cloze-delimiter)
"\\)\\([[:cntrl:][:graph:][:space:]]+?\\)\\(\\|"
(regexp-quote org-drill-hint-separator)
".+?\\)\\("
(regexp-quote org-drill-right-cloze-delimiter)
"\\)"))
(defun gsc/drill-cloze-removal (backend)
"Remove drill clozes in the current buffer.
BACKEND is the export back-end being used, as a symbol."
(while (re-search-forward (gsc/drill-compute-cloze-regexp) nil t)
;; (Copy-pasted this from org-drill-el)
;; Don't delete:
;; - org links, partly because they might contain inline
;; images which we want to keep visible.
;; - LaTeX math fragments
;; - the contents of SRC blocks
(unless (save-match-data
(or (org-pos-in-regexp (match-beginning 0)
org-bracket-link-regexp 1)
(org-in-src-block-p)
(org-inside-LaTeX-fragment-p)))
(replace-match "\\2" nil nil))))
(add-hook 'org-export-before-processing-hook 'gsc/drill-cloze-removal)
;; hide clozes in text ;;
(defvar gsc/drill-groups-to-hide '(1 3 4)
"Group 1 and 4 are the left and right delimiters respectively,
group 3 is the cloze hint.")
(setplist 'gsc/inv-cloze '(invisible t))
(defun gsc/hide-clozes-groups ()
(save-excursion
(goto-char (point-min))
(let ((cloze-regexp (gsc/drill-compute-cloze-regexp)))
(while (re-search-forward cloze-regexp nil t)
(loop for group in gsc/drill-groups-to-hide do
(overlay-put
(make-overlay (match-beginning group) (match-end group))
'category 'gsc/inv-cloze))))))
(defun gsc/show-clozes-all ()
(save-excursion
(goto-char (point-min))
(while (re-search-forward (gsc/drill-compute-cloze-regexp) nil t)
(remove-overlays
(match-beginning 1) (match-end 4) 'category 'gsc/inv-cloze))))
(defun gsc/hide-show-clozes (arg)
"If called with no argument, hides delimiters and hints
for org-drill clozes.
If called with the C-u universal argument, it shows them."
(interactive "p")
(case arg
(1 (gsc/hide-clozes-groups))
(4 (gsc/show-clozes-all))))
EMACS · ORG-MODE
emacs org-mode