Skip to content

Instantly share code, notes, and snippets.

@cashpw
Created September 15, 2022 17:08
Show Gist options
  • Save cashpw/85b1b6ceb09c35919abb62365cbbc525 to your computer and use it in GitHub Desktop.
Save cashpw/85b1b6ceb09c35919abb62365cbbc525 to your computer and use it in GitHub Desktop.
Emacs: Convert Anki cards (cashweaver/anki-editor) to org-fc
#+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