-
Notifications
You must be signed in to change notification settings - Fork 1
/
ankifier.el
executable file
·412 lines (346 loc) · 13 KB
/
ankifier.el
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
;;; ankifier.el --- Efficiently create Anki flashcards from your notes -*- lexical-binding: t; -*-
;; Copyright (C) 2022 Adham Omran
;; Author: Adham Omran <[email protected]>
;; Maintainer: Adham Omran <[email protected]>
;; Created: December 30, 2021
;; Modified: July 9, 2023
;; Version: 1.4.2
;; Homepage: https://github.com/adham-omran/ankifier
;; Package-Requires: ((emacs "27.2"))
;; Keywords: convenience
;; This file is not part of GNU Emacs.
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; Efficiently create Anki flashcards from your notes
;; This package extendes anki-editor.el by Lei Tan. ankifier.el converts notes
;; written in a special format to Anki cards per the anki-editor.el format.
;; This does not send them to Anki, the user has to perform that by running
;; `anki-editor-push-tree'.
;;;; Installation
;;;;; Manual
;; Install these required packages:
;; (anki-editor)
;; Then put this file in your load-path, and put this in your init
;; file:
;; (require 'ankifier)
;;;; Usage
;; Run one of these commands after creating an active region:
;; `ankifier-create-from-region': Detect question type and use the appropriate format to create the cards.
;; `ankifier-create-basic-from-region': Create Basic question(s) from active region.
;; `ankifier-create-cloze-from-region': Create Cloze question(s) from active region.
;;;; Tips
;; + You can customize settings in the `ankifier' group.
;;;; Credits
;; This package would not have been possible without the following
;; packages: anki-editor[1], which allows the flash cards to be sent
;; to Anki in the first place.
;;
;; [1] https://github.com/louietan/anki-editor
;;; Code:
;;;; Requirements
;;; (require 'anki-editor)
(require 'org)
(require 'expand-region)
;;;; Customization
(defgroup ankifier nil
"Settings for `ankifier'."
:link '(url-link "https://github.com/adham-omran/ankifier")
:group 'convenience)
(defcustom ankifier-anki-deck "All"
"Anki deck to push to."
:type '(string))
(defcustom ankifier-anki-cloze-note-type "Cloze"
"Card type for cloze cards."
:type '(string))
(defcustom ankifier-anki-basic-note-type "Basic"
"Card type for basic cards."
:type '(string))
(defcustom ankifier-anki-tags ""
"Tags to be used for cards."
:type '(string))
(defcustom ankifier-insert-elsewhere nil
"If set to nil the templates insert in the same heading.
If set to t this will cause the templates to be inserted
into a special header whose name is determined by `ankifier-cards-heading'"
:type '(boolean))
(defcustom ankifier-cards-heading "Cards"
"Heading name in case `ankifier-insert-elsewhere' is set to t."
:type '(string))
(defcustom ankifier-context-question nil
"If set to t, parse the template context:question and split it."
:type '(boolean))
(defcustom ankifier-feedback nil
"If set to t, insert ANKIFIED before the start of each question."
:type '(boolean))
(defcustom ankifier-arabic nil
"If set to t, insert basic questions with ؟ instead of ?."
:type '(boolean))
;;;; Variables
(defvar ankifier--cloze-region-results ()
"Variable to store region capture results.")
(defvar ankifier--basic-region-results ()
"Variable to store region capture results.")
(defvar ankifier--all-region-results ()
"Variable to store region results from a split.")
(defvar ankifier--fail nil
"Variable to determine if there are basic or cloze questions.")
;;;; Functions
;;;;; Public
(defun ankifier-create-from-region ()
"Parse active region into cloze and basic questions."
(interactive)
(setq ankifier--basic-region-results nil
ankifier--cloze-region-results nil
ankifier--fail nil)
;; Check if there are questions to begin with
(ankifier--test-region)
(if ankifier--fail
(message "One or more paragraphs is malformed.")
;; Create a list containing all questions.
(ankifier--split-region-all)
;; If the question contains a {{ then it's a cloze question
;; else treat it as a basic front/back question.
;; Insert each type into its appropriate -results list
(dolist (item ankifier--all-region-results)
(if (string-match-p (regexp-quote "\{\{c") item)
(push item ankifier--cloze-region-results)
(push item ankifier--basic-region-results)))
; Insert questions
(if ankifier-insert-elsewhere
(progn
(save-excursion
(save-restriction
(widen)
(ankifier--elsewhere-check)
(ankifier--go-to-heading)
(ankifier--create-basic-question)
(ankifier--create-cloze))))
(ankifier--create-basic-question)
(ankifier--create-cloze))
; Feedback functionality
(dolist (item ankifier--all-region-results)
(insert "ANKIFIED " item "\n\n"))
(delete-char -2)))
(defun ankifier-create-basic-from-region ()
"Create a set of questions from the selected region.
1. `ankifier--split-region-basic' creates the list of questions.
2. Check if `ankifier-insert-elsewhere' is t or nil.
If t, go to * `ankifier-cards-heading' or create it then go to it
else, create the basic question in-place."
(interactive)
(ankifier--split-region-basic)
(if ankifier-insert-elsewhere
(progn
(save-excursion
(save-restriction
(widen)
(ankifier--elsewhere-check)
(ankifier--go-to-heading)
(ankifier--create-basic-question))))
(message "Inserting in place")
(ankifier--create-basic-question))
; Feedback
(when ankifier-feedback
(save-excursion
(ankifier--create-feedback-basic))))
(defun ankifier-create-cloze-from-region ()
"Create a set of clozes from the selected region.
1. `ankifier--split-region-cloze' creates the list of questions.
2. Check if `ankifier-insert-elsewhere' is t or nil.
If t, go to * `ankifier-cards-heading' or create it then go to it
else, create the cloze question in-place."
(interactive)
(ankifier--split-region-cloze)
(if ankifier-insert-elsewhere
(progn
(save-excursion
(save-restriction
(widen)
(ankifier--elsewhere-check)
(ankifier--go-to-heading)
(ankifier--create-cloze))))
(message "Inserting in place")
(ankifier--create-cloze))
; Feedback
(when ankifier-feedback
(save-excursion
(ankifier--create-feedback-cloze))))
(defun ankifier-find-to-be-ankified ()
"Find questions that haven't been ankified.
First search in the buffer for matching strings
Second ask the user if they want to ankify.
[a-z ]*:[a-z -]*\?"
;; TODO How to deal with clozes?
;; TODO Don't forward sentence when ankifying
;; TODO Test limiting the regex search to the current subtree
;; that or avoid the * Cards subtree somehow
;; TODO Case when no?
(interactive)
(while t
(re-search-forward "[a-z ]*:[a-z -]*\?" nil nil)
(er/expand-region 2)
;; ask to ankify
(let ((answer (read-char "ankify? (y/n): ")))
(cond ((char-equal answer 121) (ankifier-create-from-region))
((char-equal answer 110) (forward-sentence))))
(deactivate-mark)))
;;;;; Private
(defun ankifier--test-region ()
"Split REGION into paragraphs seperated by \\n\\n.
Test if there's a cloze or basic question."
(interactive)
(let (
(region-text
(buffer-substring-no-properties (region-beginning) (region-end))))
(let ((split-results (split-string region-text "\n\n"))) ; TODO optimize
(dolist (item split-results)
(cond ((string-match-p (regexp-quote "\{\{c") item) nil)
((string-match "[\?؟]\n?.*" item) nil)
(t (setq ankifier--fail t)))
))))
(defun ankifier--create-feedback-basic ()
"Feedback for basic cards."
(dolist (item ankifier--basic-region-results)
(insert "ANKIFIED " item "\n\n"))
(delete-char -2))
(defun ankifier--create-feedback-cloze ()
"FEEDBACK FOR CLOZE CARDS."
(dolist (item ankifier--cloze-region-results)
(insert "ANKIFIED " item "\n\n"))
(delete-char -2))
(defun ankifier--split-region-all ()
"Split REGION into paragraphs seperated by \\n\\n."
(let (
(region-text (buffer-substring-no-properties (region-beginning) (region-end))))
(setq ankifier--all-region-results (split-string region-text "\n\n")))
(when ankifier-feedback
(kill-region nil nil t))
(deactivate-mark))
(defun ankifier--split-region-basic ()
"Split REGION into paragraphs seperated by \\n\\n."
(let (
(region-text (buffer-substring-no-properties (region-beginning) (region-end))))
(setq ankifier--basic-region-results (split-string region-text "\n\n")))
(when ankifier-feedback
(kill-region nil nil t))
(deactivate-mark))
(defun ankifier--split-region-cloze ()
"Split REGION into paragraphs seperated by \\n\\n.
The results are stored in `ankifier--cloze-region-results'"
(let (
(region-text (buffer-substring-no-properties (region-beginning) (region-end))))
(setq ankifier--cloze-region-results (split-string region-text "\n\n")))
(when ankifier-feedback
(kill-region nil nil t))
(deactivate-mark))
(defun ankifier--create-cloze ()
"Split `ankifier--cloze-region-results' then insert card.
Splits the list of strings created by `ankifier--split-region-cloze' and
passes them to `ankifier--cloze-template' as parameters."
(dolist (item ankifier--cloze-region-results)
(let ((cloze item))
(ankifier--cloze-template cloze))))
(defun ankifier--cloze-template (cloze)
"Insert CLOZE into the anki-editor template."
(org-insert-subheading nil)
(insert "Cloze")
; Insert properties
(insert "\n"
":PROPERTIES:\n"
":ANKI_DECK: " ankifier-anki-deck "\n"
":ANKI_NOTE_TYPE: " ankifier-anki-cloze-note-type "\n"
":ANKI_TAGS: " ankifier-anki-tags "\n"
":END:")
(org-insert-subheading nil)
(insert "Text")
(condition-case nil
(if ankifier-context-question
(insert "\n"
(car (split-string cloze ":")) ;; insert context
"\n\n"
(mapconcat 'identity (cdr (split-string cloze ":")) ":")) ;; insert question
(insert "\n" cloze "?"))
(wrong-type-argument (message "Warning: `ankifier-context-question' is `t' but the question does not follow the form \"Context: Cloze\"")))
(org-insert-heading nil)
(insert "Back Extra")
(outline-up-heading 2)
(org-end-of-line))
;;; Basic Card creation
(defun ankifier--create-basic-question ()
"Split `ankifier--basic-region-results' then create card.
Splits the list of strings created by `ankifier--split-region-basic' and
passes them to `ankifier--basic-template' as parameters."
(dolist (item ankifier--basic-region-results)
(let (
(question (car (split-string item "[\?؟]")))
(answer (string-join
(cdr
(split-string item "[\?؟]")) "?")))
;; TODO This will break when the question mark in the answer is Arabic.
(ankifier--basic-template question answer))))
(defun ankifier--basic-template (question answer)
"Insert QUESTION and ANSWER into the anki-editor template."
(org-insert-subheading nil)
(insert "Basic")
(insert "\n"
":PROPERTIES:\n"
":ANKI_DECK: " ankifier-anki-deck "\n"
":ANKI_NOTE_TYPE: " ankifier-anki-basic-note-type "\n"
":ANKI_TAGS: " ankifier-anki-tags "\n"
":END: ")
(org-insert-subheading 1)
(insert "Front\n")
;; Insert question
(condition-case nil
(if ankifier-context-question
(insert
(car (split-string question ":"))
;; Use a line break to separate context from question.
"\n\n"
(mapconcat 'identity (cdr (split-string question ":")) ":")
(if ankifier-arabic "؟" ;; Whether or not the question
;; mark is Arabic
"?"))
(insert "\n" question)
(if ankifier-arabic (insert "؟")
(insert "?")))
(wrong-type-argument
(message "Warning: `ankifier-context-question' is `t' but the question does not follow the form \"Context: Question? Answer\"")))
;; Insert the answer
(org-insert-heading)
(insert "Back" "\n" answer)
;; Insert answer
(outline-up-heading 2)
(org-end-of-line))
;;; Go to pre-named heading
(defun ankifier--go-to-heading ()
"Go to `ankifier-cards-heading'."
(goto-char (point-min))
(search-forward (concat "* " ankifier-cards-heading) nil t))
(defun ankifier--elsewhere-check ()
"Check if the heading * `ankifier-cards-heading' exists.
If it does not, it creates it on a top level."
(unless (save-excursion
(goto-char (point-min))
(search-forward (concat "* " ankifier-cards-heading) nil t))
; Code that runs when no heading exists
(ankifier--create-cards-heading)))
(defun ankifier--create-cards-heading ()
"Create * `ankifier-cards-heading'."
(save-excursion
;; (org-insert-heading nil nil t)
(goto-char (point-max))
(insert "\n* ")
(insert ankifier-cards-heading)))
;;;; Footer
(provide 'ankifier)
;;; ankifier.el ends here