Combine multiple actions in one http request.
This commit is contained in:
parent
8a1cfd3bfe
commit
7b4076dbfc
1 changed files with 138 additions and 106 deletions
220
anki-editor.el
220
anki-editor.el
|
@ -1,4 +1,4 @@
|
||||||
;;; anki-editor.el --- Make Anki Cards in Org-mode
|
;;; anki-editor.el --- Make Anki Cards in Org-mode -*- lexical-binding: t; -*-
|
||||||
;;
|
;;
|
||||||
;; Copyright (C) 2018 Louie Tan <louietanlei@gmail.com>
|
;; Copyright (C) 2018 Louie Tan <louietanlei@gmail.com>
|
||||||
;;
|
;;
|
||||||
|
@ -6,7 +6,7 @@
|
||||||
;; Description: Make Anki Cards in Org-mode
|
;; Description: Make Anki Cards in Org-mode
|
||||||
;; Author: Louie Tan
|
;; Author: Louie Tan
|
||||||
;; Version: 0.2.1
|
;; Version: 0.2.1
|
||||||
;; Package-Requires: ((emacs "25") (request "0.3.0"))
|
;; Package-Requires: ((emacs "25") (request "0.3.0") (dash "2.12.0"))
|
||||||
;; URL: https://github.com/louietan/anki-editor
|
;; URL: https://github.com/louietan/anki-editor
|
||||||
;;
|
;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
@ -60,11 +60,11 @@
|
||||||
;;; Code:
|
;;; Code:
|
||||||
|
|
||||||
(require 'cl-lib)
|
(require 'cl-lib)
|
||||||
|
(require 'dash)
|
||||||
(require 'json)
|
(require 'json)
|
||||||
(require 'org-element)
|
(require 'org-element)
|
||||||
(require 'ox)
|
(require 'ox)
|
||||||
(require 'request)
|
(require 'request)
|
||||||
(require 'seq)
|
|
||||||
|
|
||||||
(defconst anki-editor-prop-note-type "ANKI_NOTE_TYPE")
|
(defconst anki-editor-prop-note-type "ANKI_NOTE_TYPE")
|
||||||
(defconst anki-editor-prop-note-id "ANKI_NOTE_ID")
|
(defconst anki-editor-prop-note-id "ANKI_NOTE_ID")
|
||||||
|
@ -97,14 +97,32 @@ See https://apps.ankiweb.net/docs/manual.html#latex-conflicts.")
|
||||||
|
|
||||||
;;; AnkiConnect
|
;;; AnkiConnect
|
||||||
|
|
||||||
(defun anki-editor--anki-connect-invoke (action version &optional params)
|
(defun anki-editor--anki-connect-invoke-multi (&rest actions)
|
||||||
|
(-zip-with (lambda (result handler) (and handler (funcall handler result)))
|
||||||
|
(anki-editor--anki-connect-invoke-result
|
||||||
|
"multi" `((actions . ,(mapcar #'car actions))))
|
||||||
|
(mapcar #'cdr actions)))
|
||||||
|
|
||||||
|
(defun anki-editor--anki-connect-action (action &optional params version)
|
||||||
|
(let (a)
|
||||||
|
(when version
|
||||||
|
(push `(version . ,version) a))
|
||||||
|
(when params
|
||||||
|
(push `(params . ,params) a))
|
||||||
|
(push `(action . ,action) a)))
|
||||||
|
|
||||||
|
(defun anki-editor--anki-connect-invoke-queue ()
|
||||||
|
(let (action-queue)
|
||||||
|
(lambda (&optional action params handler)
|
||||||
|
(if action
|
||||||
|
(push (cons (anki-editor--anki-connect-action action params) handler) action-queue)
|
||||||
|
(when action-queue
|
||||||
|
(apply #'anki-editor--anki-connect-invoke-multi (nreverse action-queue))
|
||||||
|
(setq action-queue nil))))))
|
||||||
|
|
||||||
|
(defun anki-editor--anki-connect-invoke (action &optional params)
|
||||||
"Invoke AnkiConnect with ACTION, VERSION and PARAMS."
|
"Invoke AnkiConnect with ACTION, VERSION and PARAMS."
|
||||||
(let* ((data `(("action" . ,action)
|
(let ((request-body (json-encode (anki-editor--anki-connect-action action params 5)))
|
||||||
("version" . ,version)))
|
|
||||||
(request-body (json-encode
|
|
||||||
(if params
|
|
||||||
(push `("params" . ,params) data)
|
|
||||||
data)))
|
|
||||||
(request-backend 'curl)
|
(request-backend 'curl)
|
||||||
(json-array-type 'list)
|
(json-array-type 'list)
|
||||||
reply err)
|
reply err)
|
||||||
|
@ -125,11 +143,9 @@ See https://apps.ankiweb.net/docs/manual.html#latex-conflicts.")
|
||||||
|
|
||||||
(defmacro anki-editor--anki-connect-invoke-result (&rest args)
|
(defmacro anki-editor--anki-connect-invoke-result (&rest args)
|
||||||
"Invoke AnkiConnect with ARGS, return the result from response or raise an error."
|
"Invoke AnkiConnect with ARGS, return the result from response or raise an error."
|
||||||
`(let* ((resp (anki-editor--anki-connect-invoke ,@args))
|
`(let-alist (anki-editor--anki-connect-invoke ,@args)
|
||||||
(rslt (alist-get 'result resp))
|
(when .error (error .error))
|
||||||
(err (alist-get 'error resp)))
|
.result))
|
||||||
(when err (error err))
|
|
||||||
rslt))
|
|
||||||
|
|
||||||
(defun anki-editor--anki-connect-map-note (note)
|
(defun anki-editor--anki-connect-map-note (note)
|
||||||
"Convert NOTE to the form that AnkiConnect accepts."
|
"Convert NOTE to the form that AnkiConnect accepts."
|
||||||
|
@ -146,7 +162,7 @@ See https://apps.ankiweb.net/docs/manual.html#latex-conflicts.")
|
||||||
(defun anki-editor--anki-connect-store-media-file (path)
|
(defun anki-editor--anki-connect-store-media-file (path)
|
||||||
"Store media file for PATH, which is an absolute file name.
|
"Store media file for PATH, which is an absolute file name.
|
||||||
The result is the path to the newly stored media file."
|
The result is the path to the newly stored media file."
|
||||||
(unless (every #'executable-find '("base64" "sha1sum"))
|
(unless (-all? #'executable-find '("base64" "sha1sum"))
|
||||||
(error "Please make sure `base64' and `sha1sum' are available from your shell, which are required for storing media files"))
|
(error "Please make sure `base64' and `sha1sum' are available from your shell, which are required for storing media files"))
|
||||||
|
|
||||||
(let* ((content (string-trim
|
(let* ((content (string-trim
|
||||||
|
@ -162,11 +178,12 @@ The result is the path to the newly stored media file."
|
||||||
hash
|
hash
|
||||||
(file-name-extension path t))))
|
(file-name-extension path t))))
|
||||||
(anki-editor--anki-connect-invoke-result
|
(anki-editor--anki-connect-invoke-result
|
||||||
"storeMediaFile" 5
|
"storeMediaFile"
|
||||||
`((filename . ,media-file-name)
|
`((filename . ,media-file-name)
|
||||||
(data . ,content)))
|
(data . ,content)))
|
||||||
media-file-name))
|
media-file-name))
|
||||||
|
|
||||||
|
|
||||||
;;; Minor mode
|
;;; Minor mode
|
||||||
|
|
||||||
;;;###autoload
|
;;;###autoload
|
||||||
|
@ -188,13 +205,14 @@ The result is the path to the newly stored media file."
|
||||||
(when (advice-member-p 'anki-editor--get-buffer-tags #'org-get-buffer-tags)
|
(when (advice-member-p 'anki-editor--get-buffer-tags #'org-get-buffer-tags)
|
||||||
(advice-remove 'org-get-buffer-tags #'anki-editor--get-buffer-tags)))
|
(advice-remove 'org-get-buffer-tags #'anki-editor--get-buffer-tags)))
|
||||||
|
|
||||||
|
|
||||||
;;; Commands
|
;;; Commands
|
||||||
|
|
||||||
(defun anki-editor-submit (&optional arg match scope)
|
(defun anki-editor-push-notes (&optional arg match scope)
|
||||||
"Build notes from headings that can be matched by MATCH within SCOPE and send them to Anki.
|
"Build notes from headings that can be matched by MATCH within SCOPE and push them to Anki.
|
||||||
|
|
||||||
The default search condition `&ANKI_NOTE_TYPE<>\"\"' will be
|
The default search condition `&ANKI_NOTE_TYPE<>\"\"' will always
|
||||||
appended to MATCH.
|
be appended to MATCH.
|
||||||
|
|
||||||
For notes that already exist in Anki (i.e. has `ANKI_NOTE_ID'
|
For notes that already exist in Anki (i.e. has `ANKI_NOTE_ID'
|
||||||
property), only their fields and tags will be updated, change of
|
property), only their fields and tags will be updated, change of
|
||||||
|
@ -240,25 +258,24 @@ of that heading."
|
||||||
scope)
|
scope)
|
||||||
|
|
||||||
(message (if (= 0 failed)
|
(message (if (= 0 failed)
|
||||||
(format "Successfully submitted %d notes to Anki." acc)
|
(format "Successfully pushed %d notes to Anki." acc)
|
||||||
(format "Submitted %d notes, %d of which are failed. Check property drawers for failure reasons. Once you've fixed the issues, you could use `anki-editor-retry-failure-notes' to re-submit the failed notes."
|
(format "Pushed %d notes, %d of which are failed. Check property drawers for failure reasons. Once you've fixed the issues, you could use `anki-editor-retry-failure-notes' to re-push the failed notes."
|
||||||
acc
|
acc failed)))))
|
||||||
failed)))))
|
|
||||||
|
|
||||||
(defun anki-editor-retry-failure-notes (&optional arg scope)
|
(defun anki-editor-retry-failure-notes (&optional arg scope)
|
||||||
"Re-submit notes that were failed.
|
"Retry pushing notes that were failed.
|
||||||
This command just calls `anki-editor-submit' with match string
|
This command just calls `anki-editor-submit' with match string
|
||||||
matching non-empty `ANKI_FAILURE_REASON' properties."
|
matching non-empty `ANKI_FAILURE_REASON' properties."
|
||||||
(interactive "P")
|
(interactive "P")
|
||||||
(anki-editor-submit arg (concat anki-editor-prop-failure-reason "<>\"\"") scope))
|
(anki-editor-push-notes arg (concat anki-editor-prop-failure-reason "<>\"\"") scope))
|
||||||
|
|
||||||
(defun anki-editor-insert-deck (&optional arg)
|
(defun anki-editor-insert-deck (&optional arg)
|
||||||
"Insert a deck heading interactively.
|
"Insert a deck heading interactively.
|
||||||
ARG is used the same way as `M-RET' (org-insert-heading)."
|
ARG is used the same way as `M-RET' (org-insert-heading)."
|
||||||
(interactive "P")
|
(interactive "P")
|
||||||
(message "Fetching decks...")
|
(message "Fetching decks...")
|
||||||
(let* ((decks (sort (anki-editor-deck-names) #'string-lessp))
|
(let ((deckname (completing-read "Choose a deck: "
|
||||||
(deckname (completing-read "Choose a deck: " decks)))
|
(sort (anki-editor-deck-names) #'string-lessp))))
|
||||||
(org-insert-heading arg)
|
(org-insert-heading arg)
|
||||||
(insert deckname)
|
(insert deckname)
|
||||||
(org-set-property anki-editor-prop-deck deckname)))
|
(org-set-property anki-editor-prop-deck deckname)))
|
||||||
|
@ -270,13 +287,18 @@ Where the note subtree is placed depends on PREFIX, which is the
|
||||||
same as how it is used by `M-RET'(org-insert-heading)."
|
same as how it is used by `M-RET'(org-insert-heading)."
|
||||||
(interactive "P")
|
(interactive "P")
|
||||||
(message "Fetching note types...")
|
(message "Fetching note types...")
|
||||||
(let ((note-types (sort (anki-editor-note-types) #'string-lessp))
|
(let ((note-type (completing-read "Choose a note type: "
|
||||||
note-type note-heading fields)
|
(sort (anki-editor-note-types) #'string-lessp)))
|
||||||
(setq note-type (completing-read "Choose a note type: " note-types))
|
note-heading fields)
|
||||||
(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" `((modelName . ,note-type)))
|
||||||
note-heading (read-from-minibuffer "Enter the heading: " "Item"))
|
note-heading (read-from-minibuffer "Enter the heading: "))
|
||||||
(anki-editor--insert-note-skeleton prefix note-heading note-type fields)))
|
(anki-editor--insert-note-skeleton prefix
|
||||||
|
(if (string-blank-p note-heading)
|
||||||
|
"Item"
|
||||||
|
note-heading)
|
||||||
|
note-type
|
||||||
|
fields)))
|
||||||
|
|
||||||
(defun anki-editor-cloze-region (&optional arg)
|
(defun anki-editor-cloze-region (&optional arg)
|
||||||
"Cloze region with number ARG."
|
"Cloze region with number ARG."
|
||||||
|
@ -288,7 +310,7 @@ same as how it is used by `M-RET'(org-insert-heading)."
|
||||||
(delete-region (region-beginning) (region-end))
|
(delete-region (region-beginning) (region-end))
|
||||||
(insert (with-output-to-string
|
(insert (with-output-to-string
|
||||||
(princ (format "{{c%d::%s" (or arg 1) region))
|
(princ (format "{{c%d::%s" (or arg 1) region))
|
||||||
(unless (string-empty-p (string-trim hint)) (princ (format "::%s" hint)))
|
(unless (string-blank-p hint) (princ (format "::%s" hint)))
|
||||||
(princ "}}"))))))
|
(princ "}}"))))))
|
||||||
|
|
||||||
(defun anki-editor-export-subtree-to-html ()
|
(defun anki-editor-export-subtree-to-html ()
|
||||||
|
@ -297,7 +319,7 @@ same as how it is used by `M-RET'(org-insert-heading)."
|
||||||
(org-export-to-buffer
|
(org-export-to-buffer
|
||||||
anki-editor--ox-anki-html-backend
|
anki-editor--ox-anki-html-backend
|
||||||
anki-editor-buffer-html-output nil t nil t nil
|
anki-editor-buffer-html-output nil t nil t nil
|
||||||
(lambda () (html-mode))))
|
#'html-mode))
|
||||||
|
|
||||||
(defun anki-editor-convert-region-to-html ()
|
(defun anki-editor-convert-region-to-html ()
|
||||||
"Convert and replace region to HTML."
|
"Convert and replace region to HTML."
|
||||||
|
@ -315,7 +337,7 @@ This is useful when new version of this package depends on the
|
||||||
bugfixes or new features of AnkiConnect."
|
bugfixes or new features of AnkiConnect."
|
||||||
(interactive)
|
(interactive)
|
||||||
(when (yes-or-no-p "NOTE: This will download the latest codebase of AnkiConnect to your system, which is not guaranteed to be safe or stable. Generally, you don't need this command, this is useful only when new version of this package requires the updates of AnkiConnect that are not released yet. Do you still want to continue?")
|
(when (yes-or-no-p "NOTE: This will download the latest codebase of AnkiConnect to your system, which is not guaranteed to be safe or stable. Generally, you don't need this command, this is useful only when new version of this package requires the updates of AnkiConnect that are not released yet. Do you still want to continue?")
|
||||||
(let ((result (anki-editor--anki-connect-invoke-result "upgrade" 5)))
|
(let ((result (anki-editor--anki-connect-invoke-result "upgrade")))
|
||||||
(when (and (booleanp result) result)
|
(when (and (booleanp result) result)
|
||||||
(message "AnkiConnect has been upgraded, you might have to restart Anki to make it in effect.")))))
|
(message "AnkiConnect has been upgraded, you might have to restart Anki to make it in effect.")))))
|
||||||
|
|
||||||
|
@ -323,20 +345,20 @@ bugfixes or new features of AnkiConnect."
|
||||||
|
|
||||||
(defun anki-editor--process-note-heading ()
|
(defun anki-editor--process-note-heading ()
|
||||||
"Process note heading at point."
|
"Process note heading at point."
|
||||||
(let (note-elem note)
|
(-->
|
||||||
(setq note-elem (org-element-at-point)
|
(org-element-at-point)
|
||||||
note-elem (let ((content (buffer-substring
|
(let ((content (buffer-substring
|
||||||
(org-element-property :begin note-elem)
|
(org-element-property :begin it)
|
||||||
;; in case the buffer is narrowed,
|
;; in case the buffer is narrowed,
|
||||||
;; e.g. by `org-map-entries' when
|
;; e.g. by `org-map-entries' when
|
||||||
;; scope is `tree'
|
;; scope is `tree'
|
||||||
(min (point-max) (org-element-property :end note-elem)))))
|
(min (point-max) (org-element-property :end it)))))
|
||||||
(with-temp-buffer
|
(with-temp-buffer
|
||||||
(org-mode)
|
(org-mode)
|
||||||
(insert content)
|
(insert content)
|
||||||
(car (org-element-contents (org-element-parse-buffer)))))
|
(car (org-element-contents (org-element-parse-buffer)))))
|
||||||
note (anki-editor--heading-to-note note-elem))
|
(anki-editor--heading-to-note it)
|
||||||
(anki-editor--save-note note)))
|
(anki-editor--save-note it)))
|
||||||
|
|
||||||
(defun anki-editor--insert-note-skeleton (prefix heading note-type fields)
|
(defun anki-editor--insert-note-skeleton (prefix heading note-type fields)
|
||||||
"Insert a note subtree (skeleton) with HEADING, NOTE-TYPE and FIELDS.
|
"Insert a note subtree (skeleton) with HEADING, NOTE-TYPE and FIELDS.
|
||||||
|
@ -356,53 +378,57 @@ Where the subtree is created depends on PREFIX."
|
||||||
(anki-editor--create-note note)
|
(anki-editor--create-note note)
|
||||||
(anki-editor--update-note note)))
|
(anki-editor--update-note note)))
|
||||||
|
|
||||||
|
(defun anki-editor--set-note-id (id)
|
||||||
|
(unless id
|
||||||
|
(error "Note creation failed for unknown reason"))
|
||||||
|
(org-set-property anki-editor-prop-note-id (number-to-string id)))
|
||||||
|
|
||||||
(defun anki-editor--create-note (note)
|
(defun anki-editor--create-note (note)
|
||||||
"Request AnkiConnect for creating NOTE."
|
"Request AnkiConnect for creating NOTE."
|
||||||
|
(let ((queue (anki-editor--anki-connect-invoke-queue)))
|
||||||
(when anki-editor-create-decks
|
(when anki-editor-create-decks
|
||||||
(anki-editor--create-deck (alist-get 'deck note)))
|
(funcall queue
|
||||||
|
'createDeck
|
||||||
|
`((deck . ,(alist-get 'deck note)))))
|
||||||
|
|
||||||
(let* ((response (anki-editor--anki-connect-invoke
|
(funcall queue
|
||||||
"addNote" 5 `((note . ,(anki-editor--anki-connect-map-note note)))))
|
'addNote
|
||||||
(result (alist-get 'result response))
|
`((note . ,(anki-editor--anki-connect-map-note note)))
|
||||||
(err (alist-get 'error response)))
|
#'anki-editor--set-note-id)
|
||||||
(if result
|
|
||||||
;; put ID of newly created note in property drawer
|
(funcall queue)))
|
||||||
(org-set-property anki-editor-prop-note-id
|
|
||||||
(format "%d" (alist-get 'result response)))
|
|
||||||
(error (or err "Sorry, the operation was unsuccessful and detailed information is unavailable.")))))
|
|
||||||
|
|
||||||
(defun anki-editor--update-note (note)
|
(defun anki-editor--update-note (note)
|
||||||
"Request AnkiConnect for updating fields and tags of NOTE."
|
"Request AnkiConnect for updating fields and tags of NOTE."
|
||||||
(anki-editor--anki-connect-invoke-result
|
|
||||||
"updateNoteFields" 5 `((note . ,(anki-editor--anki-connect-map-note note))))
|
|
||||||
|
|
||||||
|
(let ((queue (anki-editor--anki-connect-invoke-queue)))
|
||||||
|
(funcall queue
|
||||||
|
'updateNoteFields
|
||||||
|
`((note . ,(anki-editor--anki-connect-map-note note))))
|
||||||
|
|
||||||
|
(funcall queue
|
||||||
|
'notesInfo
|
||||||
|
`((notes . (,(alist-get 'note-id note))))
|
||||||
|
(lambda (result)
|
||||||
;; update tags
|
;; update tags
|
||||||
(let (existing-note added-tags removed-tags)
|
(let* ((existing-note (car result))
|
||||||
(setq existing-note (car (anki-editor--anki-connect-invoke-result
|
(tags-to-add (-difference (alist-get 'tags note) (alist-get 'tags existing-note)))
|
||||||
"notesInfo" 5 `(("notes" . (,(alist-get 'note-id note))))))
|
(tags-to-remove (-difference (alist-get 'tags existing-note) (alist-get 'tags note)))
|
||||||
added-tags (cl-set-difference (alist-get 'tags note) (alist-get 'tags existing-note) :test #'string-equal)
|
(tag-queue (anki-editor--anki-connect-invoke-queue)))
|
||||||
removed-tags (cl-set-difference (alist-get 'tags existing-note) (alist-get 'tags note) :test #'string-equal))
|
|
||||||
|
|
||||||
(when added-tags
|
(when tags-to-add
|
||||||
(anki-editor--anki-connect-invoke-result
|
(funcall tag-queue
|
||||||
"addTags" 5 `(("notes" . (,(alist-get 'note-id note)))
|
'addTags `((notes . (,(alist-get 'note-id note)))
|
||||||
("tags" . ,(mapconcat #'identity added-tags " ")))))
|
(tags . ,(mapconcat #'identity tags-to-add " ")))))
|
||||||
(when removed-tags
|
|
||||||
(anki-editor--anki-connect-invoke-result
|
|
||||||
"removeTags" 5 `(("notes" . (,(alist-get 'note-id note)))
|
|
||||||
("tags" . ,(mapconcat #'identity removed-tags " ")))))))
|
|
||||||
|
|
||||||
(defun anki-editor--get-allowed-values-for-property (property)
|
(when tags-to-remove
|
||||||
"Get allowed values for PROPERTY."
|
(funcall tag-queue
|
||||||
(pcase property
|
'removeTags `((notes . (,(alist-get 'note-id note)))
|
||||||
((pred (string= anki-editor-prop-deck)) (anki-editor-deck-names))
|
(tags . ,(mapconcat #'identity tags-to-remove " ")))))
|
||||||
((pred (string= anki-editor-prop-note-type)) (anki-editor-note-types))
|
|
||||||
(_ nil)))
|
|
||||||
|
|
||||||
|
(funcall tag-queue))))
|
||||||
|
|
||||||
(defun anki-editor--create-deck (deck-name)
|
(funcall queue)))
|
||||||
"Request AnkiConnect for creating a deck named DECK-NAME."
|
|
||||||
(anki-editor--anki-connect-invoke-result "createDeck" 5 `((deck . ,deck-name))))
|
|
||||||
|
|
||||||
(defun anki-editor--set-failure-reason (reason)
|
(defun anki-editor--set-failure-reason (reason)
|
||||||
"Set failure reason to REASON in property drawer at point."
|
"Set failure reason to REASON in property drawer at point."
|
||||||
|
@ -412,6 +438,13 @@ Where the subtree is created depends on PREFIX."
|
||||||
"Clear failure reason in property drawer at point."
|
"Clear failure reason in property drawer at point."
|
||||||
(org-entry-delete nil anki-editor-prop-failure-reason))
|
(org-entry-delete nil anki-editor-prop-failure-reason))
|
||||||
|
|
||||||
|
(defun anki-editor--get-allowed-values-for-property (property)
|
||||||
|
"Get allowed values for PROPERTY."
|
||||||
|
(pcase property
|
||||||
|
((pred (string= anki-editor-prop-deck)) (anki-editor-deck-names))
|
||||||
|
((pred (string= anki-editor-prop-note-type)) (anki-editor-note-types))
|
||||||
|
(_ nil)))
|
||||||
|
|
||||||
(defun anki-editor-is-valid-org-tag (tag)
|
(defun anki-editor-is-valid-org-tag (tag)
|
||||||
"Check if string TAG can be used as an Org tag."
|
"Check if string TAG can be used as an Org tag."
|
||||||
(string-match-p anki-editor-org-tag-regexp tag))
|
(string-match-p anki-editor-org-tag-regexp tag))
|
||||||
|
@ -420,13 +453,13 @@ Where the subtree is created depends on PREFIX."
|
||||||
"Get all tags from Anki."
|
"Get all tags from Anki."
|
||||||
(let (anki-tags)
|
(let (anki-tags)
|
||||||
(prog1
|
(prog1
|
||||||
(setq anki-tags (anki-editor--anki-connect-invoke-result "getTags" 5))
|
(setq anki-tags (anki-editor--anki-connect-invoke-result "getTags"))
|
||||||
(unless (seq-every-p #'anki-editor-is-valid-org-tag anki-tags)
|
(unless (-all? #'anki-editor-is-valid-org-tag anki-tags)
|
||||||
(warn "Some tags from Anki contain characters that are not valid in Org tags.")))))
|
(warn "Some tags from Anki contain characters that are not valid in Org tags.")))))
|
||||||
|
|
||||||
(defun anki-editor-deck-names ()
|
(defun anki-editor-deck-names ()
|
||||||
"Get all decks names from Anki."
|
"Get all decks names from Anki."
|
||||||
(anki-editor--anki-connect-invoke-result "deckNames" 5))
|
(anki-editor--anki-connect-invoke-result "deckNames"))
|
||||||
|
|
||||||
(defun anki-editor--before-set-tags (&optional _ just-align)
|
(defun anki-editor--before-set-tags (&optional _ just-align)
|
||||||
"Build tag list for completion including tags from Anki.
|
"Build tag list for completion including tags from Anki.
|
||||||
|
@ -441,7 +474,7 @@ Do nothing when JUST-ALIGN is non-nil."
|
||||||
(unless just-align
|
(unless just-align
|
||||||
(if org-current-tag-alist
|
(if org-current-tag-alist
|
||||||
(setq org-current-tag-alist
|
(setq org-current-tag-alist
|
||||||
(org-tag-add-to-alist
|
(org--tag-add-to-alist
|
||||||
(mapcar #'list (anki-editor-all-tags))
|
(mapcar #'list (anki-editor-all-tags))
|
||||||
org-current-tag-alist))
|
org-current-tag-alist))
|
||||||
(unless (advice-member-p 'anki-editor--get-buffer-tags #'org-get-buffer-tags)
|
(unless (advice-member-p 'anki-editor--get-buffer-tags #'org-get-buffer-tags)
|
||||||
|
@ -453,17 +486,16 @@ Do nothing when JUST-ALIGN is non-nil."
|
||||||
|
|
||||||
(defun anki-editor-note-types ()
|
(defun anki-editor-note-types ()
|
||||||
"Get note types from Anki."
|
"Get note types from Anki."
|
||||||
(anki-editor--anki-connect-invoke-result "modelNames" 5))
|
(anki-editor--anki-connect-invoke-result "modelNames"))
|
||||||
|
|
||||||
(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 ((org-trust-scanner-tags t)
|
(let ((org-trust-scanner-tags t)
|
||||||
deck note-id note-type tags fields)
|
(deck (org-entry-get-with-inheritance anki-editor-prop-deck))
|
||||||
(setq deck (org-entry-get-with-inheritance anki-editor-prop-deck)
|
(note-id (org-entry-get nil anki-editor-prop-note-id))
|
||||||
note-id (org-entry-get nil anki-editor-prop-note-id)
|
(note-type (org-entry-get nil anki-editor-prop-note-type))
|
||||||
note-type (org-entry-get nil anki-editor-prop-note-type)
|
(tags (org-get-tags-at))
|
||||||
tags (org-get-tags-at)
|
(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 deck (error "No deck specified"))
|
(unless deck (error "No deck specified"))
|
||||||
(unless note-type (error "Missing note type"))
|
(unless note-type (error "Missing note type"))
|
||||||
|
@ -487,6 +519,7 @@ Do nothing when JUST-ALIGN is non-nil."
|
||||||
(org-element-interpret-data contents)
|
(org-element-interpret-data contents)
|
||||||
anki-editor--ox-anki-html-backend t))))
|
anki-editor--ox-anki-html-backend t))))
|
||||||
|
|
||||||
|
|
||||||
;;; Org Export Backend
|
;;; Org Export Backend
|
||||||
|
|
||||||
(defconst anki-editor--ox-anki-html-backend
|
(defconst anki-editor--ox-anki-html-backend
|
||||||
|
@ -510,7 +543,7 @@ Do nothing when JUST-ALIGN is non-nil."
|
||||||
"Wrap CONTENT with Anki-style latex markers."
|
"Wrap CONTENT with Anki-style latex markers."
|
||||||
(format "[latex]%s[/latex]" content))
|
(format "[latex]%s[/latex]" content))
|
||||||
|
|
||||||
(defun anki-editor--ox-latex (latex contents info)
|
(defun anki-editor--ox-latex (latex _contents _info)
|
||||||
"Transcode LATEX from Org to HTML.
|
"Transcode LATEX from Org to HTML.
|
||||||
CONTENTS is nil. INFO is a plist holding contextual information."
|
CONTENTS is nil. INFO is a plist holding contextual information."
|
||||||
(let* ((code (org-element-property :value latex))
|
(let* ((code (org-element-property :value latex))
|
||||||
|
@ -519,7 +552,7 @@ CONTENTS is nil. INFO is a plist holding contextual information."
|
||||||
(dolist (map anki-editor--anki-latex-syntax-map)
|
(dolist (map anki-editor--anki-latex-syntax-map)
|
||||||
(setq code (replace-regexp-in-string (car map) (cdr map) code t t)))
|
(setq code (replace-regexp-in-string (car map) (cdr map) code t t)))
|
||||||
|
|
||||||
(when (equal copy code)
|
(when (string= copy code)
|
||||||
(setq code (anki-editor--wrap-latex
|
(setq code (anki-editor--wrap-latex
|
||||||
(if (eq (org-element-type latex) 'latex-fragment)
|
(if (eq (org-element-type latex) 'latex-fragment)
|
||||||
code
|
code
|
||||||
|
@ -628,8 +661,6 @@ ox-html.el :)"
|
||||||
;; Fuzzy link points to a target or an element.
|
;; Fuzzy link points to a target or an element.
|
||||||
(_
|
(_
|
||||||
(let* ((ref (org-export-get-reference destination info))
|
(let* ((ref (org-export-get-reference destination info))
|
||||||
(org-html-standalone-image-predicate
|
|
||||||
#'org-html--has-caption-p)
|
|
||||||
(number (cond
|
(number (cond
|
||||||
(desc nil)
|
(desc nil)
|
||||||
((org-html-standalone-image-p destination info)
|
((org-html-standalone-image-p destination info)
|
||||||
|
@ -670,6 +701,7 @@ ox-html.el :)"
|
||||||
;; No path, only description. Try to do something useful.
|
;; No path, only description. Try to do something useful.
|
||||||
(t (format "<i>%s</i>" desc)))))
|
(t (format "<i>%s</i>" desc)))))
|
||||||
|
|
||||||
|
|
||||||
;;; Utilities
|
;;; Utilities
|
||||||
|
|
||||||
(defun anki-editor--get-subheadings (heading)
|
(defun anki-editor--get-subheadings (heading)
|
||||||
|
|
Loading…
Reference in a new issue