Use Org tags as Anki tags.
This commit is contained in:
parent
be212c74d8
commit
66889e6471
2 changed files with 60 additions and 121 deletions
151
anki-editor.el
151
anki-editor.el
|
@ -64,7 +64,6 @@
|
||||||
(require 'ox)
|
(require 'ox)
|
||||||
|
|
||||||
(defconst anki-editor-prop-note-type :ANKI_NOTE_TYPE)
|
(defconst anki-editor-prop-note-type :ANKI_NOTE_TYPE)
|
||||||
(defconst anki-editor-prop-note-tags :ANKI_TAGS)
|
|
||||||
(defconst anki-editor-prop-note-id :ANKI_NOTE_ID)
|
(defconst anki-editor-prop-note-id :ANKI_NOTE_ID)
|
||||||
(defconst anki-editor-prop-failure-reason :ANKI_FAILURE_REASON)
|
(defconst anki-editor-prop-failure-reason :ANKI_FAILURE_REASON)
|
||||||
(defconst anki-editor-buffer-html-output "*AnkiEditor HTML Output*")
|
(defconst anki-editor-buffer-html-output "*AnkiEditor HTML Output*")
|
||||||
|
@ -73,10 +72,6 @@
|
||||||
"Customizations for anki-editor."
|
"Customizations for anki-editor."
|
||||||
:group 'org)
|
:group 'org)
|
||||||
|
|
||||||
(defcustom anki-editor-note-tag
|
|
||||||
"note"
|
|
||||||
"Headings with this tag will be considered as notes.")
|
|
||||||
|
|
||||||
(defcustom anki-editor-deck-tag
|
(defcustom anki-editor-deck-tag
|
||||||
"deck"
|
"deck"
|
||||||
"Headings with this tag will be considered as decks.")
|
"Headings with this tag will be considered as decks.")
|
||||||
|
@ -86,10 +81,6 @@
|
||||||
"If non-nil, consecutive `}' will be automatically separated by spaces to prevent early-closing of cloze.
|
"If non-nil, consecutive `}' will be automatically separated by spaces to prevent early-closing of cloze.
|
||||||
See https://apps.ankiweb.net/docs/manual.html#latex-conflicts.")
|
See https://apps.ankiweb.net/docs/manual.html#latex-conflicts.")
|
||||||
|
|
||||||
(defcustom anki-editor-inherit-tags
|
|
||||||
nil
|
|
||||||
"If non-nil, notes will inherit tags from all its ancestor headings.")
|
|
||||||
|
|
||||||
(defcustom anki-editor-create-decks
|
(defcustom anki-editor-create-decks
|
||||||
nil
|
nil
|
||||||
"If non-nil, creates deck before creating a note.")
|
"If non-nil, creates deck before creating a note.")
|
||||||
|
@ -205,11 +196,12 @@ of that heading."
|
||||||
current-deck)
|
current-deck)
|
||||||
(org-map-entries
|
(org-map-entries
|
||||||
(lambda ()
|
(lambda ()
|
||||||
(let ((current-tags (org-get-tags)))
|
|
||||||
(cond
|
(cond
|
||||||
((member anki-editor-deck-tag current-tags)
|
;; FIXME: decks won't get iterated any more, consider remove
|
||||||
|
;; the `deck' tags and use properties instead ?
|
||||||
|
((member anki-editor-deck-tag (org-get-tags))
|
||||||
(setq current-deck (nth 4 (org-heading-components))))
|
(setq current-deck (nth 4 (org-heading-components))))
|
||||||
((member anki-editor-note-tag current-tags)
|
((org-entry-get (point) (anki-editor--keyword-name anki-editor-prop-note-type))
|
||||||
(progn
|
(progn
|
||||||
(setq total (1+ total))
|
(setq total (1+ total))
|
||||||
(message "Processing note at %d..." (point))
|
(message "Processing note at %d..." (point))
|
||||||
|
@ -218,8 +210,8 @@ of that heading."
|
||||||
(anki-editor--process-note-heading current-deck)
|
(anki-editor--process-note-heading current-deck)
|
||||||
(error (progn
|
(error (progn
|
||||||
(setq failed (1+ failed))
|
(setq failed (1+ failed))
|
||||||
(anki-editor--set-failure-reason (error-message-string err))))))))))
|
(anki-editor--set-failure-reason (error-message-string err)))))))))
|
||||||
(mapconcat 'identity `(,anki-editor-deck-tag ,anki-editor-note-tag) "|"))
|
(concat (anki-editor--keyword-name anki-editor-prop-note-type) "<>\"\""))
|
||||||
|
|
||||||
(message (with-output-to-string
|
(message (with-output-to-string
|
||||||
(princ (format "Submitted %d notes, with %d failed." total failed))
|
(princ (format "Submitted %d notes, with %d failed." total failed))
|
||||||
|
@ -248,27 +240,12 @@ With PREFIX, only insert the deck name at point."
|
||||||
(anki-editor--insert-deck-heading deckname))))))
|
(anki-editor--insert-deck-heading deckname))))))
|
||||||
|
|
||||||
;;;###autoload
|
;;;###autoload
|
||||||
(defun anki-editor-insert-note ()
|
(defun anki-editor-insert-note (&optional prefix)
|
||||||
"Insert the skeleton of a note.
|
"Insert a note interactively.
|
||||||
|
|
||||||
The contents to be insrted are structured with a note heading
|
Where the note subtree is placed depends on PREFIX, which is the
|
||||||
along with subheadings that correspond to fields.
|
same as how it is used by `M-RET'(org-insert-heading)."
|
||||||
|
(interactive "P")
|
||||||
Where the note is inserted depends on where the point is.
|
|
||||||
|
|
||||||
When the point is somewhere inside a note heading, the new note
|
|
||||||
is inserted below this note with same heading level.
|
|
||||||
|
|
||||||
Or when the point is outside any note heading but inside a
|
|
||||||
heading that isn't tagged with 'deck' but under a deck heading,
|
|
||||||
the new note is one level lower to and is inserted at the bottom
|
|
||||||
of this heading.
|
|
||||||
|
|
||||||
Or when the point is inside a deck heading, the behavior is the
|
|
||||||
same as above.
|
|
||||||
|
|
||||||
Otherwise, it's inserted below current heading at point."
|
|
||||||
(interactive)
|
|
||||||
(message "Fetching note types...")
|
(message "Fetching note types...")
|
||||||
(let ((note-types (sort (anki-editor--anki-connect-invoke-result "modelNames" 5) #'string-lessp))
|
(let ((note-types (sort (anki-editor--anki-connect-invoke-result "modelNames" 5) #'string-lessp))
|
||||||
note-type note-heading fields)
|
note-type note-heading fields)
|
||||||
|
@ -276,53 +253,7 @@ Otherwise, it's inserted below current heading at point."
|
||||||
(message "Fetching note fields...")
|
(message "Fetching note fields...")
|
||||||
(setq fields (anki-editor--anki-connect-invoke-result "modelFieldNames" 5 `((modelName . ,note-type)))
|
(setq fields (anki-editor--anki-connect-invoke-result "modelFieldNames" 5 `((modelName . ,note-type)))
|
||||||
note-heading (read-from-minibuffer "Enter the heading: " "Item"))
|
note-heading (read-from-minibuffer "Enter the heading: " "Item"))
|
||||||
|
(anki-editor--insert-note-skeleton prefix note-heading note-type fields)))
|
||||||
;; find and go to the best position, then insert the note
|
|
||||||
(let ((cur-point (point))
|
|
||||||
pt-of-grp
|
|
||||||
inserted)
|
|
||||||
(anki-editor--visit-ancestor-headings
|
|
||||||
(lambda ()
|
|
||||||
(let ((tags (org-get-tags)))
|
|
||||||
(cond
|
|
||||||
;; if runs into a note heading, inserts the note heading with
|
|
||||||
;; the same level
|
|
||||||
((member anki-editor-note-tag tags)
|
|
||||||
(progn
|
|
||||||
(anki-editor--insert-note-skeleton note-heading note-type fields)
|
|
||||||
(setq inserted t)
|
|
||||||
t))
|
|
||||||
;; if runs into a deck heading, inserts the note heading one
|
|
||||||
;; level lower to current deck heading or to the group
|
|
||||||
;; heading that was visited before
|
|
||||||
((member anki-editor-deck-tag tags)
|
|
||||||
(progn
|
|
||||||
(when pt-of-grp (goto-char pt-of-grp))
|
|
||||||
(anki-editor--insert-note-skeleton note-heading note-type fields t)
|
|
||||||
(setq inserted t)
|
|
||||||
t))
|
|
||||||
;; otherwise, consider it as a group heading and save its
|
|
||||||
;; point for further consideration, then continue
|
|
||||||
(t (progn
|
|
||||||
(unless pt-of-grp (setq pt-of-grp (point)))
|
|
||||||
nil))))))
|
|
||||||
(unless inserted
|
|
||||||
(goto-char cur-point)
|
|
||||||
(anki-editor--insert-note-skeleton note-heading note-type fields)))))
|
|
||||||
|
|
||||||
;;;###autoload
|
|
||||||
(defun anki-editor-add-tags ()
|
|
||||||
"Add tags to property drawer of current heading with autocompletion."
|
|
||||||
(interactive)
|
|
||||||
(let ((tags (sort (anki-editor--anki-connect-invoke-result "getTags" 5) #'string-lessp))
|
|
||||||
(prop (anki-editor--keyword-name anki-editor-prop-note-tags)))
|
|
||||||
(while t
|
|
||||||
(org-entry-add-to-multivalued-property
|
|
||||||
(point) prop (completing-read "Choose a tag: "
|
|
||||||
(cl-set-difference
|
|
||||||
tags
|
|
||||||
(org-entry-get-multivalued-property (point) prop)
|
|
||||||
:test #'string-equal))))))
|
|
||||||
|
|
||||||
;;;###autoload
|
;;;###autoload
|
||||||
(defun anki-editor-cloze-region (&optional arg)
|
(defun anki-editor-cloze-region (&optional arg)
|
||||||
|
@ -395,13 +326,11 @@ DECK is used when the action is note creation."
|
||||||
(push `(deck . ,deck) note)
|
(push `(deck . ,deck) note)
|
||||||
(anki-editor--save-note note)))
|
(anki-editor--save-note note)))
|
||||||
|
|
||||||
(defun anki-editor--insert-note-skeleton (heading note-type fields &optional demote)
|
(defun anki-editor--insert-note-skeleton (prefix heading note-type fields)
|
||||||
"Insert a note skeleton with HEADING, NOTE-TYPE and FIELDS.
|
"Insert a note subtree (skeleton) with HEADING, NOTE-TYPE and FIELDS.
|
||||||
If DEMOTE is t, demote the inserted note heading."
|
Where the subtree is created depends on PREFIX."
|
||||||
(org-insert-heading-respect-content)
|
(org-insert-heading prefix)
|
||||||
(when demote (org-do-demote))
|
|
||||||
(insert heading)
|
(insert heading)
|
||||||
(anki-editor--set-tags-fix anki-editor-note-tag)
|
|
||||||
(org-set-property (anki-editor--keyword-name anki-editor-prop-note-type) note-type)
|
(org-set-property (anki-editor--keyword-name anki-editor-prop-note-type) note-type)
|
||||||
(dolist (field fields)
|
(dolist (field fields)
|
||||||
(save-excursion
|
(save-excursion
|
||||||
|
@ -463,35 +392,47 @@ If DEMOTE is t, demote the inserted note heading."
|
||||||
"Clear failure reason in property drawer at point."
|
"Clear failure reason in property drawer at point."
|
||||||
(org-entry-delete nil (anki-editor--keyword-name anki-editor-prop-failure-reason)))
|
(org-entry-delete nil (anki-editor--keyword-name anki-editor-prop-failure-reason)))
|
||||||
|
|
||||||
(defun anki-editor--inherited-tags ()
|
(defun anki-editor-all-tags ()
|
||||||
"Get tags from ancestors."
|
"Get all tags from Anki."
|
||||||
(org-with-wide-buffer
|
(anki-editor--anki-connect-invoke-result "getTags" 5))
|
||||||
(let (tags)
|
|
||||||
(while (org-up-heading-safe)
|
(defun anki-editor--before-set-tags (&optional _ just-align)
|
||||||
(setq tags (append (org-entry-get-multivalued-property
|
"Build tag list for completion including tags from Anki.
|
||||||
(point)
|
|
||||||
(anki-editor--keyword-name anki-editor-prop-note-tags))
|
When the value of `org-current-tag-alist' is non-nil, just append
|
||||||
tags)))
|
to it.
|
||||||
tags)))
|
|
||||||
|
Otherwise, advise function `org-get-buffer-tags' to append tags
|
||||||
|
from Anki to the result.
|
||||||
|
|
||||||
|
Do nothing when JUST-ALIGN is non-nil."
|
||||||
|
(unless just-align
|
||||||
|
(if org-current-tag-alist
|
||||||
|
(setq org-current-tag-alist
|
||||||
|
(org-tag-add-to-alist
|
||||||
|
(mapcar #'list (anki-editor-all-tags))
|
||||||
|
org-current-tag-alist))
|
||||||
|
(unless (advice-member-p 'anki-editor--get-buffer-tags #'org-get-buffer-tags)
|
||||||
|
(advice-add 'org-get-buffer-tags :around #'anki-editor--get-buffer-tags)))))
|
||||||
|
|
||||||
|
(defun anki-editor--get-buffer-tags (oldfun)
|
||||||
|
"Append tags from Anki to the result of applying OLDFUN."
|
||||||
|
(append (funcall oldfun) (mapcar #'list (anki-editor-all-tags))))
|
||||||
|
|
||||||
|
;; TODO: consider turn this package into a minor mode to enable it to stop advising ?
|
||||||
|
(advice-add 'org-set-tags :before #'anki-editor--before-set-tags)
|
||||||
|
|
||||||
(defun anki-editor--heading-to-note (heading)
|
(defun anki-editor--heading-to-note (heading)
|
||||||
"Construct an alist representing a note for HEADING."
|
"Construct an alist representing a note for HEADING."
|
||||||
(let (note-id note-type tags fields)
|
(let (note-id note-type tags fields)
|
||||||
(setq note-id (org-element-property anki-editor-prop-note-id heading)
|
(setq note-id (org-element-property anki-editor-prop-note-id heading)
|
||||||
note-type (org-element-property anki-editor-prop-note-type heading)
|
note-type (org-element-property anki-editor-prop-note-type heading)
|
||||||
tags (org-entry-get-multivalued-property
|
tags (org-get-tags-at)
|
||||||
(point)
|
|
||||||
(anki-editor--keyword-name anki-editor-prop-note-tags))
|
|
||||||
fields (mapcar #'anki-editor--heading-to-note-field (anki-editor--get-subheadings heading)))
|
fields (mapcar #'anki-editor--heading-to-note-field (anki-editor--get-subheadings heading)))
|
||||||
|
|
||||||
(unless note-type (error "Missing note type"))
|
(unless note-type (error "Missing note type"))
|
||||||
(unless fields (error "Missing fields"))
|
(unless fields (error "Missing fields"))
|
||||||
|
|
||||||
(when anki-editor-inherit-tags
|
|
||||||
(setq tags (append tags (anki-editor--inherited-tags))))
|
|
||||||
|
|
||||||
(setq tags (delete-dups tags))
|
|
||||||
|
|
||||||
`((note-id . ,(string-to-number (or note-id "-1")))
|
`((note-id . ,(string-to-number (or note-id "-1")))
|
||||||
(note-type . ,note-type)
|
(note-type . ,note-type)
|
||||||
(tags . ,tags)
|
(tags . ,tags)
|
||||||
|
|
10
examples.org
10
examples.org
|
@ -2,10 +2,9 @@
|
||||||
|
|
||||||
** Vocabulary
|
** Vocabulary
|
||||||
|
|
||||||
*** Item :note:
|
*** Item :vocab:idioms:
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
:ANKI_NOTE_TYPE: Basic (and reversed card)
|
:ANKI_NOTE_TYPE: Basic (and reversed card)
|
||||||
:ANKI_TAGS: vocab idioms
|
|
||||||
:END:
|
:END:
|
||||||
|
|
||||||
**** Front
|
**** Front
|
||||||
|
@ -18,10 +17,9 @@
|
||||||
|
|
||||||
* Computing :deck:
|
* Computing :deck:
|
||||||
|
|
||||||
** Item :note:
|
** Item :lisp:emacs:programming:
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
:ANKI_NOTE_TYPE: Basic
|
:ANKI_NOTE_TYPE: Basic
|
||||||
:ANKI_TAGS: lisp emacs programming
|
|
||||||
:END:
|
:END:
|
||||||
|
|
||||||
*** Front
|
*** Front
|
||||||
|
@ -51,7 +49,7 @@
|
||||||
|
|
||||||
* Mathematics :deck:
|
* Mathematics :deck:
|
||||||
|
|
||||||
** Item1 :note:
|
** Item1
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
:ANKI_NOTE_TYPE: Cloze
|
:ANKI_NOTE_TYPE: Cloze
|
||||||
:END:
|
:END:
|
||||||
|
@ -62,7 +60,7 @@
|
||||||
|
|
||||||
*** Extra
|
*** Extra
|
||||||
|
|
||||||
** Item2 :note:
|
** Item2
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
:ANKI_NOTE_TYPE: Basic
|
:ANKI_NOTE_TYPE: Basic
|
||||||
:END:
|
:END:
|
||||||
|
|
Loading…
Reference in a new issue