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) (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)

View file

@ -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: