Created
September 15, 2022 17:08
-
-
Save cashpw/85b1b6ceb09c35919abb62365cbbc525 to your computer and use it in GitHub Desktop.
Emacs: Convert Anki cards (cashweaver/anki-editor) to org-fc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#+begin_src emacs-lisp | |
(defun cashweaver/anki-to-fc () | |
(interactive) | |
(let ((anki-card-heading-points (org-map-entries | |
(lambda () | |
(point)) | |
"+LEVEL=2-fc+ANKI_NOTE_ID={.}"))) | |
(dolist (pom (reverse anki-card-heading-points)) | |
(goto-char pom) | |
(cashweaver/anki-to-fc--single)))) | |
(defun cashweaver/anki-to-fc--single (&optional pom) | |
(interactive) | |
(let* ((pom (or pom (point))) | |
(heading-text (org-entry-get pom "ITEM")) | |
(anki-note-type (org-entry-get pom "ANKI_NOTE_TYPE")) | |
(anki-note-id (org-entry-get pom "ANKI_NOTE_ID")) | |
(data (cashweaver/anki-to-fc--get-data anki-note-id)) | |
(card-created-time (plist-get (nth 0 data) :card-created-time))) | |
(if (or (string= "AKA" anki-note-type) | |
(string= "Cloze with Source" anki-note-type) | |
(string= "Definition" anki-note-type) | |
(string= "Denotes" anki-note-type)) | |
(org-fc--add-tag "orgfc_migration_safetodelete") | |
(org-fc--add-tag "orgfc_migration_needswork")) | |
(org-insert-heading) | |
(org-set-tags ":fc:") | |
(org-id-get-create) | |
(org-set-property "ANKI_NOTE_ID" | |
anki-note-id) | |
(org-set-property "FC_CREATED" | |
(cashweaver/anki-to-fc--time-to-fc-time card-created-time)) | |
(cond | |
((string= "AKA" anki-note-type) | |
(cashweaver/anki-to-fc--aka heading-text | |
data | |
anki-note-id)) | |
((string= "Cloze with Source" anki-note-type) | |
(cashweaver/anki-to-fc--cloze-with-source heading-text | |
data | |
anki-note-id)) | |
;; ((string= "Compare/Contrast" anki-note-type) | |
;; (cashweaver/anki-to-fc--compare-contrast heading-text | |
;; data | |
;; anki-note-id)) | |
((string= "Definition" anki-note-type) | |
(cashweaver/anki-to-fc--definition heading-text | |
data | |
anki-note-id)) | |
((string= "Denotes" anki-note-type) | |
(cashweaver/anki-to-fc--denotes heading-text | |
data | |
anki-note-id)) | |
;; ((string= "Equivalence" anki-note-type) | |
;; (cashweaver/anki-to-fc--equivalence heading-text | |
;; data | |
;; anki-note-id)) | |
(t | |
(cashweaver/anki-to-fc--default heading-text | |
data | |
anki-note-id | |
anki-note-type))))) | |
(defun cashweaver/anki-to-fc--general (heading-text review-data fc-type) | |
(insert (s-lex-format " ${heading-text}")) | |
(org-set-property "FC_TYPE" fc-type) | |
(org-fc-review-data-set review-data)) | |
(defun cashweaver/anki-to-fc--normal (heading-text review-data front back &optional extra source) | |
(cashweaver/anki-to-fc--general heading-text | |
review-data | |
"normal") | |
(save-excursion | |
;; Jump below the drawers | |
(org-insert-subheading nil) | |
;; Delete the heading we just created | |
(delete-backward-char 4) | |
(insert front)) | |
(save-excursion | |
;; Jump below the drawers | |
(org-insert-subheading nil) | |
(insert "Back") | |
(newline) | |
(insert back)) | |
(when extra | |
(save-excursion | |
(org-insert-subheading nil) | |
(insert "Extra") | |
(newline) | |
(insert extra))) | |
(when source | |
(save-excursion | |
(org-insert-subheading nil) | |
(insert "Source") | |
(newline) | |
(insert source)))) | |
(defun cashweaver/anki-to-fc--double (heading-text review-data front back &optional extra source) | |
(cashweaver/anki-to-fc--normal heading-text | |
review-data | |
front | |
back | |
extra | |
source) | |
(org-set-property "FC_TYPE" "double")) | |
(defun cashweaver/anki-to-fc--cloze (heading-text review-data &optional body extra source) | |
(let* ((fc-cloze-max (number-to-string | |
(length review-data))) | |
(heading-text (cashweaver/anki-to-fc--convert-text | |
heading-text))) | |
(cashweaver/anki-to-fc--general heading-text | |
review-data | |
"cloze") | |
(org-set-property "FC_CLOZE_MAX" fc-cloze-max) | |
(org-set-property "FC_CLOZE_TYPE" "deletion") | |
(when body | |
(save-excursion | |
;; Jump below the drawers | |
(org-insert-subheading nil) | |
;; Delete the heading we just created | |
(delete-backward-char 4) | |
(insert body))) | |
(when extra | |
(save-excursion | |
(org-insert-subheading nil) | |
(insert "Extra") | |
(newline) | |
(insert extra))) | |
(when source | |
(save-excursion | |
(org-insert-subheading nil) | |
(insert "Source") | |
(newline) | |
(insert source))))) | |
(defun cashweaver/anki-to-fc--default (heading-text data anki-note-id anki-note-type) | |
(let* ((review-data (mapcar | |
(lambda (datum) | |
(list (plist-get datum :position) | |
(plist-get datum :ease) | |
(plist-get datum :box) | |
(plist-get datum :interval) | |
(plist-get datum :due))) | |
data))) | |
(cashweaver/anki-to-fc--normal anki-note-type | |
review-data | |
heading-text | |
"TODO: Back") | |
(org-set-tags ":fc:todo:"))) | |
(defun cashweaver/anki-to-fc--cloze-with-source (heading-text data anki-note-id) | |
(let* ((review-data (mapcar | |
(lambda (datum) | |
(list (plist-get datum :position) | |
(plist-get datum :ease) | |
(plist-get datum :box) | |
(plist-get datum :interval) | |
(plist-get datum :due))) | |
data)) | |
(fields (cashweaver/anki-to-fc--get-fields anki-note-id))) | |
(cl-destructuring-bind (text extra source) fields | |
(let ((extra (if (> (length extra) 0) | |
(cashweaver/anki-to-fc--convert-text extra) | |
nil)) | |
(source (if (> (length source) 0) | |
source | |
;; "TODO: Source" | |
nil)))) | |
(cashweaver/anki-to-fc--cloze heading-text | |
review-data | |
;; body | |
nil | |
extra | |
source)))) | |
(defun cashweaver/anki-to-fc--compare-contrast (heading-text data anki-note-id) | |
(let* ((fields (cashweaver/anki-to-fc--get-fields anki-note-id)) | |
(review-data (mapcar | |
(lambda (datum) | |
(list "front" | |
(plist-get datum :ease) | |
(plist-get datum :box) | |
(plist-get datum :interval) | |
(plist-get datum :due))) | |
data))) | |
(cl-destructuring-bind (concepts context comparisons-contrasts source) fields | |
(let* ((concepts (cashweaver/anki-to-fc--convert-text concepts)) | |
(context (if (> (length context) 0) | |
(cashweaver/anki-to-fc--convert-text context) | |
nil)) | |
(comparisons-contrasts (if (> (length comparison-contrasts) 0) | |
(cashweaver/anki-to-fc--convert-text comparison-contrasts) | |
nil)) | |
(source (if (> (length source) 0) | |
source | |
;; "TODO: Source" | |
nil)) | |
(heading-text (if context | |
(s-lex-format "Compare/Contrast (${context})") | |
"Compare/Contrast")) | |
(front-text concepts) | |
(back-text comparisons-contrasts)) | |
(cashweaver/anki-to-fc--normal heading-text | |
review-data | |
front | |
back | |
;; extra | |
nil | |
source))))) | |
(defun cashweaver/anki-to-fc--definition (heading-text data anki-note-id) | |
(let* ((fields (cashweaver/anki-to-fc--get-fields anki-note-id)) | |
(review-data (mapcar | |
(lambda (datum) | |
(list (if (= 0 (plist-get datum :position)) | |
"back" | |
"front") | |
(plist-get datum :ease) | |
(plist-get datum :box) | |
(plist-get datum :interval) | |
(plist-get datum :due))) | |
data))) | |
(cl-destructuring-bind (term-val context definition extra source) fields | |
(let* ((term-val (cashweaver/anki-to-fc--convert-text term-val)) | |
(context (if (> (length context) 0) | |
(cashweaver/anki-to-fc--convert-text context) | |
nil)) | |
(definition (if (> (length definition) 0) | |
(cashweaver/anki-to-fc--convert-text definition) | |
nil)) | |
(extra (if (> (length extra) 0) | |
(cashweaver/anki-to-fc--convert-text extra) | |
nil)) | |
(source (if (> (length source) 0) | |
source | |
;; "TODO: Source" | |
nil)) | |
(heading-text (if context | |
(s-lex-format "Definition (${context})") | |
"Definition")) | |
(front term-val) | |
(back definition)) | |
(cashweaver/anki-to-fc--double heading-text | |
review-data | |
front | |
back | |
extra | |
source))))) | |
(defun cashweaver/anki-to-fc--denotes (heading-text data anki-note-id) | |
(let* ((review-data (mapcar (lambda (datum) | |
(list (plist-get datum :position) | |
(plist-get datum :ease) | |
(plist-get datum :box) | |
(plist-get datum :interval) | |
(plist-get datum :due))) | |
data)) | |
(fc-cloze-max (number-to-string | |
(length review-data))) | |
(fields (cashweaver/anki-to-fc--get-fields anki-note-id))) | |
(cl-destructuring-bind (symbol-1 symbol-2 symbol-3 symbol-4 context description extra source) fields | |
(let* ((symbol-1 (cashweaver/anki-to-fc--convert-text symbol-1)) | |
(symbol-2 (if (> (length symbol-2) 0) | |
(cashweaver/anki-to-fc--convert-text symbol-2) | |
nil)) | |
(symbol-3 (if (> (length symbol-3) 0) | |
(cashweaver/anki-to-fc--convert-text symbol-3) | |
nil)) | |
(symbol-4 (if (> (length symbol-4) 0) | |
(cashweaver/anki-to-fc--convert-text symbol-4) | |
nil)) | |
(context (if (> (length context) 0) | |
(cashweaver/anki-to-fc--convert-text context) | |
nil)) | |
(description (if (> (length description) 0) | |
(cashweaver/anki-to-fc--convert-text description) | |
nil)) | |
(extra (if (> (length extra) 0) | |
(cashweaver/anki-to-fc--convert-text extra) | |
nil)) | |
(source (if (> (length source) 0) | |
(cashweaver/anki-to-fc--convert-text source) | |
nil)) | |
(heading-text (if context | |
(s-lex-format "Denotes (${context})") | |
"Denotes")) | |
(body | |
(concat (s-lex-format "- {{${symbol-1}}@0}\n") | |
(if symbol-2 (s-lex-format "- {{${symbol-2}}@1}\n") "") | |
(if symbol-3 (s-lex-format "- {{${symbol-3}}@2}\n") "") | |
(if symbol-4 (s-lex-format "- {{${symbol-4}}@3}\n") "") | |
(s-lex-format "\n${description}")))) | |
(cashweaver/anki-to-fc--cloze heading-text | |
review-data | |
body | |
extra | |
source))))) | |
(defun cashweaver/anki-to-fc--aka (heading-text data anki-note-id) | |
(let* ((review-data (mapcar | |
(lambda (datum) | |
(list (plist-get datum :position) | |
(plist-get datum :ease) | |
(plist-get datum :box) | |
(plist-get datum :interval) | |
(plist-get datum :due))) | |
data)) | |
(fc-cloze-max (number-to-string | |
(length review-data))) | |
(fields (cashweaver/anki-to-fc--get-fields anki-note-id))) | |
(cl-destructuring-bind (term-1 term-2 term-3 term-4 term-5 term-6 context extra source) fields | |
(let* ((term-1 (cashweaver/anki-to-fc--convert-text term-1)) | |
(term-2 (if (> (length term-2) 0) | |
(cashweaver/anki-to-fc--convert-text term-2) | |
nil)) | |
(term-3 (if (> (length term-3) 0) | |
(cashweaver/anki-to-fc--convert-text term-3) | |
nil)) | |
(term-4 (if (> (length term-4) 0) | |
(cashweaver/anki-to-fc--convert-text term-4) | |
nil)) | |
(term-5 (if (> (length term-5) 0) | |
(cashweaver/anki-to-fc--convert-text term-5) | |
nil)) | |
(term-6 (if (> (length term-6) 0) | |
(cashweaver/anki-to-fc--convert-text term-6) | |
nil)) | |
(context (if (> (length context) 0) | |
(cashweaver/anki-to-fc--convert-text context) | |
nil)) | |
(extra (if (> (length extra) 0) | |
(cashweaver/anki-to-fc--convert-text extra) | |
nil)) | |
(source (if (> (length source) 0) | |
(cashweaver/anki-to-fc--convert-text source) | |
nil)) | |
(heading-text (if context | |
(s-lex-format "AKA (${context})") | |
"AKA")) | |
(body | |
(concat (s-lex-format "- {{${term-1}}@0}\n") | |
(if term-2 (s-lex-format "- {{${term-2}}@1}\n") "") | |
(if term-3 (s-lex-format "- {{${term-3}}@2}\n") "") | |
(if term-4 (s-lex-format "- {{${term-4}}@3}\n") "") | |
(if term-5 (s-lex-format "- {{${term-5}}@4}\n") "") | |
(if term-6 (s-lex-format "- {{${term-6}}@5}\n") "")) | |
)) | |
(cashweaver/anki-to-fc--cloze heading-text | |
review-data | |
body | |
extra | |
source))))) | |
(defun cashweaver/anki-to-fc--convert-text (text) | |
(cashweaver/anki-to-fc--convert-cloze | |
(cashweaver/anki-to-fc--convert-roam-link text))) | |
(defun cashweaver/anki-to-fc--convert-roam-link (text) | |
(replace-regexp-in-string "<a href=\".*?\\#ID-\\(.*?\\)\">\\(.*?\\)<\\/a>" | |
"[[\\1][\\2]]" | |
text)) | |
(defun cashweaver/anki-to-fc--convert-latex (text) | |
"LaTeX code in cloze delections can't contain a }} , to work around this limitation, insert a space between the braces. | |
Example: \frac{1}{\sqrt{2} } | |
See: https://www.leonrische.me/fc/card_types.html" | |
(replace-regexp-in-string "}}" "} }" text)) | |
(defun cashweaver/anki-to-fc--convert-cloze (cloze-text) | |
(replace-regexp-in-string | |
"}@\\([0-9]+\\)" | |
(lambda (match) | |
(cl-destructuring-bind (prefix cloze-id) (s-split "@" match) | |
(concat prefix | |
"@" | |
(number-to-string | |
(1- (string-to-number | |
cloze-id)))))) | |
(replace-regexp-in-string | |
"{{\\(.*?\\)::\\(.*?\\)}@" "{{\\1}{\\2}@" | |
(replace-regexp-in-string | |
"{{c\\([0-9]+\\)::\\(.*?\\)}}" | |
"{{\\2}@\\1}" | |
cloze-text)))) | |
(defun cashweaver/anki-to-fc--time-to-fc-time (time) | |
(format-time-string "%FT%TZ" time "UTC0")) | |
(defun cashweaver/anki-to-fc--get-fields (anki-note-id) | |
(let* ((anki-field-separator "") | |
(db "/home/cashweaver/collection.anki2") | |
(query (s-lex-format "select flds from notes where id=${anki-note-id}")) | |
(command (s-lex-format "sqlite3 ${db} \"${query}\"")) | |
(fields (shell-command-to-string command))) | |
(s-split anki-field-separator fields))) | |
(defun cashweaver/anki-to-fc--query-db (query) | |
(let* ((db "/home/cashweaver/collection.anki2") | |
(command (s-lex-format "sqlite3 ${db} \"${query}\"")) | |
(results (shell-command-to-string command)) | |
(lines (s-split "\n" | |
results | |
'omit-nulls))) | |
lines)) | |
(defun cashweaver/anki-to-fc--get-data (note-id) | |
"Get due,ivl information from the anki card at point." | |
(let* ((anki-collection-creation-time | |
;; https://github.com/ankidroid/Anki-Android/wiki/Database-Structure#collection | |
(seconds-to-time 1553518800)) | |
(query | |
;; https://github.com/ankidroid/Anki-Android/wiki/Database-Structure | |
(s-lex-format "select due,ivl,factor,reps,lapses,ord,cards.id from notes inner join cards on notes.id = cards .nid where notes.id = ${note-id};")) | |
(lines (cashweaver/anki-to-fc--query-db query)) | |
;; (anki-field-separator "") | |
(initial-ease 2.5) | |
(positions (mapcar | |
(lambda (line) | |
(cl-destructuring-bind (due interval factor reps lapses ordinal card-id) (s-split "|" line) | |
(let* ((due (cashweaver/anki-to-fc--time-to-fc-time | |
(time-add anki-collection-creation-time | |
(days-to-time (string-to-number due))))) | |
(reps (string-to-number reps)) | |
(lapses (string-to-number lapses)) | |
(interval (string-to-number interval)) | |
(last-sm2-interval 6.0) | |
(last-sm2-interval-index 3) | |
(box (if (> interval last-sm2-interval) | |
(max (- reps lapses) | |
(1+ last-sm2-interval-index)) | |
(- reps lapses))) | |
(factor (string-to-number factor)) | |
(ease (if (= factor 0) | |
initial-ease | |
(/ factor 1000.0))) | |
(pos (string-to-number ordinal)) | |
(card-created-time (seconds-to-time (/ (string-to-number card-id) | |
1000)))) | |
`(:card-created-time ,card-created-time | |
:position ,pos | |
:ease ,ease | |
:box ,box | |
:interval ,interval | |
:due ,due)))) | |
lines))) | |
positions)) | |
#+end_src |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment