Use Org tags as Anki tags.

This commit is contained in:
louie 2018-05-06 14:01:06 +08:00
parent be212c74d8
commit 66889e6471
2 changed files with 60 additions and 121 deletions

View file

@ -64,7 +64,6 @@
(require 'ox)
(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-failure-reason :ANKI_FAILURE_REASON)
(defconst anki-editor-buffer-html-output "*AnkiEditor HTML Output*")
@ -73,10 +72,6 @@
"Customizations for anki-editor."
:group 'org)
(defcustom anki-editor-note-tag
"note"
"Headings with this tag will be considered as notes.")
(defcustom anki-editor-deck-tag
"deck"
"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.
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
nil
"If non-nil, creates deck before creating a note.")
@ -205,21 +196,22 @@ of that heading."
current-deck)
(org-map-entries
(lambda ()
(let ((current-tags (org-get-tags)))
(cond
((member anki-editor-deck-tag current-tags)
(setq current-deck (nth 4 (org-heading-components))))
((member anki-editor-note-tag current-tags)
(progn
(setq total (1+ total))
(message "Processing note at %d..." (point))
(anki-editor--clear-failure-reason)
(condition-case err
(anki-editor--process-note-heading current-deck)
(error (progn
(setq failed (1+ failed))
(anki-editor--set-failure-reason (error-message-string err))))))))))
(mapconcat 'identity `(,anki-editor-deck-tag ,anki-editor-note-tag) "|"))
(cond
;; 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))))
((org-entry-get (point) (anki-editor--keyword-name anki-editor-prop-note-type))
(progn
(setq total (1+ total))
(message "Processing note at %d..." (point))
(anki-editor--clear-failure-reason)
(condition-case err
(anki-editor--process-note-heading current-deck)
(error (progn
(setq failed (1+ failed))
(anki-editor--set-failure-reason (error-message-string err)))))))))
(concat (anki-editor--keyword-name anki-editor-prop-note-type) "<>\"\""))
(message (with-output-to-string
(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))))))
;;;###autoload
(defun anki-editor-insert-note ()
"Insert the skeleton of a note.
(defun anki-editor-insert-note (&optional prefix)
"Insert a note interactively.
The contents to be insrted are structured with a note heading
along with subheadings that correspond to fields.
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)
Where the note subtree is placed depends on PREFIX, which is the
same as how it is used by `M-RET'(org-insert-heading)."
(interactive "P")
(message "Fetching note types...")
(let ((note-types (sort (anki-editor--anki-connect-invoke-result "modelNames" 5) #'string-lessp))
note-type note-heading fields)
@ -276,53 +253,7 @@ Otherwise, it's inserted below current heading at point."
(message "Fetching note fields...")
(setq fields (anki-editor--anki-connect-invoke-result "modelFieldNames" 5 `((modelName . ,note-type)))
note-heading (read-from-minibuffer "Enter the heading: " "Item"))
;; 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))))))
(anki-editor--insert-note-skeleton prefix note-heading note-type fields)))
;;;###autoload
(defun anki-editor-cloze-region (&optional arg)
@ -395,13 +326,11 @@ DECK is used when the action is note creation."
(push `(deck . ,deck) note)
(anki-editor--save-note note)))
(defun anki-editor--insert-note-skeleton (heading note-type fields &optional demote)
"Insert a note skeleton with HEADING, NOTE-TYPE and FIELDS.
If DEMOTE is t, demote the inserted note heading."
(org-insert-heading-respect-content)
(when demote (org-do-demote))
(defun anki-editor--insert-note-skeleton (prefix heading note-type fields)
"Insert a note subtree (skeleton) with HEADING, NOTE-TYPE and FIELDS.
Where the subtree is created depends on PREFIX."
(org-insert-heading prefix)
(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)
(dolist (field fields)
(save-excursion
@ -463,35 +392,47 @@ If DEMOTE is t, demote the inserted note heading."
"Clear failure reason in property drawer at point."
(org-entry-delete nil (anki-editor--keyword-name anki-editor-prop-failure-reason)))
(defun anki-editor--inherited-tags ()
"Get tags from ancestors."
(org-with-wide-buffer
(let (tags)
(while (org-up-heading-safe)
(setq tags (append (org-entry-get-multivalued-property
(point)
(anki-editor--keyword-name anki-editor-prop-note-tags))
tags)))
tags)))
(defun anki-editor-all-tags ()
"Get all tags from Anki."
(anki-editor--anki-connect-invoke-result "getTags" 5))
(defun anki-editor--before-set-tags (&optional _ just-align)
"Build tag list for completion including tags from Anki.
When the value of `org-current-tag-alist' is non-nil, just append
to it.
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)
"Construct an alist representing a note for HEADING."
(let (note-id note-type tags fields)
(setq note-id (org-element-property anki-editor-prop-note-id heading)
note-type (org-element-property anki-editor-prop-note-type heading)
tags (org-entry-get-multivalued-property
(point)
(anki-editor--keyword-name anki-editor-prop-note-tags))
tags (org-get-tags-at)
fields (mapcar #'anki-editor--heading-to-note-field (anki-editor--get-subheadings heading)))
(unless note-type (error "Missing note type"))
(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-type . ,note-type)
(tags . ,tags)

View file

@ -2,10 +2,9 @@
** Vocabulary
*** Item :note:
*** Item :vocab:idioms:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic (and reversed card)
:ANKI_TAGS: vocab idioms
:END:
**** Front
@ -18,10 +17,9 @@
* Computing :deck:
** Item :note:
** Item :lisp:emacs:programming:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_TAGS: lisp emacs programming
:END:
*** Front
@ -51,7 +49,7 @@
* Mathematics :deck:
** Item1 :note:
** Item1
:PROPERTIES:
:ANKI_NOTE_TYPE: Cloze
:END:
@ -62,7 +60,7 @@
*** Extra
** Item2 :note:
** Item2
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END: