Allow to put one field in heading

This commit is contained in:
louie 2020-02-24 13:39:31 +08:00
parent 3fba9ea13b
commit 7dcae54caa
4 changed files with 173 additions and 101 deletions

View file

@ -18,6 +18,7 @@
7. ~anki-editor-use-math-jax~ was replaced with 7. ~anki-editor-use-math-jax~ was replaced with
~anki-editor-latex-style~ ~anki-editor-latex-style~
8. Minor internal code refactoring 8. Minor internal code refactoring
9. Allows to put one field in heading
** v0.3.3 ** v0.3.3

View file

@ -62,12 +62,12 @@ there are any ambiguity or grammatical mistakes ;-)/
** Commands ** Commands
| Command | Description | | Command | Description |
|------------------------------------+---------------------------------------------------------------------------------------------------| |------------------------------------+--------------------------------------------------------------------------------------------------------------------------------|
| anki-editor-mode | Toggle this minor mode. | | anki-editor-mode | Toggle this minor mode. |
| anki-editor-push-notes | Push notes to Anki. Additional arguments can be used to restrict the range of notes. | | anki-editor-push-notes | Push notes to Anki. Additional arguments can be used to restrict the range of notes. |
| anki-editor-push-new-notes | Similar to ~anki-editor-push-notes~, but push those that are without ~ANKI_NOTE_ID~. | | anki-editor-push-new-notes | Similar to ~anki-editor-push-notes~, but push those that are without ~ANKI_NOTE_ID~. |
| anki-editor-retry-failed-notes | Similar to ~anki-editor-push-notes~, except that it only pushes notes with ~ANKI_FAILURE_REASON~. | | anki-editor-retry-failed-notes | Similar to ~anki-editor-push-notes~, except that it only pushes notes with ~ANKI_FAILURE_REASON~. |
| anki-editor-insert-note | Insert a note entry like ~M-RET~, interactively. | | anki-editor-insert-note | Insert a note entry like ~M-RET~, interactively. When note heading is not provided or is blank, it's used as the first field. |
| anki-editor-delete-notes | Delete notes or the note at point. | | anki-editor-delete-notes | Delete notes or the note at point. |
| anki-editor-cloze-dwim | Cloze current active region or a word the under the cursor. | | anki-editor-cloze-dwim | Cloze current active region or a word the under the cursor. |
| anki-editor-export-subtree-to-html | Export the subtree at point to HTML. | | anki-editor-export-subtree-to-html | Export the subtree at point to HTML. |

View file

@ -114,7 +114,10 @@ form entries."
(cl-defun anki-editor--fetch (url (cl-defun anki-editor--fetch (url
&rest settings &rest settings
&key type data success _error parser &key
(type "GET")
data success _error
(parser 'buffer-string)
&allow-other-keys) &allow-other-keys)
"This is a simplistic little function to make http requests using cURL. "This is a simplistic little function to make http requests using cURL.
The api is borrowed from request.el. It exists because The api is borrowed from request.el. It exists because
@ -128,10 +131,11 @@ determine whether it's a bug in Emacs and make a patch requires
more digging." more digging."
(let ((tempfile (make-temp-file "emacs-anki-editor")) (let ((tempfile (make-temp-file "emacs-anki-editor"))
(responsebuf (generate-new-buffer " *anki-editor-curl*"))) (responsebuf (generate-new-buffer " *anki-editor-curl*")))
(when data
(with-temp-file tempfile (with-temp-file tempfile
(setq buffer-file-coding-system 'utf-8) (setq buffer-file-coding-system 'utf-8)
(set-buffer-multibyte t) (set-buffer-multibyte t)
(insert data)) (insert data)))
(unwind-protect (unwind-protect
(with-current-buffer responsebuf (with-current-buffer responsebuf
(apply #'call-process "curl" nil t nil (list (apply #'call-process "curl" nil t nil (list
@ -142,7 +146,8 @@ more digging."
(concat "@" tempfile))) (concat "@" tempfile)))
(goto-char (point-min)) (goto-char (point-min))
(apply success (list :data (funcall parser)))) (when success
(apply success (list :data (funcall parser)))))
(kill-buffer responsebuf) (kill-buffer responsebuf)
(delete-file tempfile)))) (delete-file tempfile))))
@ -250,7 +255,7 @@ The result is the path to the newly stored media file."
(latex-environment . anki-editor--ox-latex)))) (latex-environment . anki-editor--ox-latex))))
(defconst anki-editor--ox-export-ext-plist (defconst anki-editor--ox-export-ext-plist
'(:with-toc nil :anki-editor-mode t)) '(:with-toc nil :with-properties nil :with-planning nil :anki-editor-mode t))
(cl-macrolet ((with-table (table) (cl-macrolet ((with-table (table)
`(cl-loop for delims in ,table `(cl-loop for delims in ,table
@ -370,23 +375,70 @@ The implementation is borrowed and simplified from ox-html."
(t (throw 'giveup nil))))) (t (throw 'giveup nil)))))
(funcall oldfun link desc info))) (funcall oldfun link desc info)))
(defun anki-editor--export-string (src fmt)
(cl-ecase fmt
('nil src)
('t (or (org-export-string-as src
anki-editor--ox-anki-html-backend
t
anki-editor--ox-export-ext-plist)
;; 8.2.10 version of
;; `org-export-filter-apply-functions'
;; returns nil for an input of empty string,
;; which will cause AnkiConnect to fail
""))))
;;; Core primitives ;;; Core primitives
(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")
(defconst anki-editor-prop-exporter "ANKI_EXPORTER") (defconst anki-editor-prop-format "ANKI_FORMAT")
(defconst anki-editor-prop-deck "ANKI_DECK") (defconst anki-editor-prop-deck "ANKI_DECK")
(defconst anki-editor-prop-tags "ANKI_TAGS") (defconst anki-editor-prop-tags "ANKI_TAGS")
(defconst anki-editor-prop-tags-plus (concat anki-editor-prop-tags "+")) (defconst anki-editor-prop-tags-plus (concat anki-editor-prop-tags "+"))
(defconst anki-editor-prop-failure-reason "ANKI_FAILURE_REASON") (defconst anki-editor-prop-failure-reason "ANKI_FAILURE_REASON")
(defconst anki-editor-org-tag-regexp "^\\([[:alnum:]_@#%]+\\)+$") (defconst anki-editor-org-tag-regexp "^\\([[:alnum:]_@#%]+\\)+$")
(defconst anki-editor-exporter-raw "raw")
(defconst anki-editor-exporter-default "default")
(cl-defstruct anki-editor-note (cl-defstruct anki-editor-note
id model deck fields tags) id model deck fields tags)
(defvar anki-editor--collection-data-updated nil
"Whether or not collection data is updated from Anki. Used by `anki-editor--with-collection-data-updated' to avoid unnecessary updates.")
;; The following variables should only be used inside `anki-editor--with-collection-data-updated'.
(defvar anki-editor--model-names nil
"Note types from Anki.")
(defvar anki-editor--model-fields nil
"Alist of (NOTE-TYPE . FIELDS).")
(defmacro anki-editor--with-collection-data-updated (&rest body)
"Execute BODY with collection data updated from Anki.
Note that since we have no idea of whether BODY will update collection
data, BODY might read out-dated data. This doesn't matter right now
as note types won't change in BODY."
(declare (indent defun) (debug t))
`(if anki-editor--collection-data-updated
(progn ,@body)
(cl-destructuring-bind (models)
(anki-editor-api-with-multi
(anki-editor-api-enqueue 'modelNames))
(unwind-protect
(progn
(setq anki-editor--collection-data-updated t
anki-editor--model-names models
anki-editor--model-fields
(cl-loop for flds in (eval `(anki-editor-api-with-multi
,@(cl-loop for mod in models
collect `(anki-editor-api-enqueue 'modelFieldNames :modelName ,mod))))
for mod in models
collect (cons mod flds)))
,@body)
(setq anki-editor--collection-data-updated nil)))))
(defun anki-editor-map-note-entries (func &optional match scope &rest skip) (defun anki-editor-map-note-entries (func &optional match scope &rest skip)
"Simple wrapper that calls `org-map-entries' with "Simple wrapper that calls `org-map-entries' with
`&ANKI_NOTE_TYPE<>\"\"' appended to MATCH." `&ANKI_NOTE_TYPE<>\"\"' appended to MATCH."
@ -395,8 +447,8 @@ The implementation is borrowed and simplified from ox-html."
(let ((org-use-property-inheritance nil)) (let ((org-use-property-inheritance nil))
(org-map-entries func (concat match "&" anki-editor-prop-note-type "<>\"\"") scope skip))) (org-map-entries func (concat match "&" anki-editor-prop-note-type "<>\"\"") scope skip)))
(defun anki-editor--insert-note-skeleton (prefix deck heading note-type fields) (defun anki-editor--insert-note-skeleton (prefix deck heading type fields)
"Insert a note subtree (skeleton) with HEADING, NOTE-TYPE and FIELDS. "Insert a note subtree (skeleton) with HEADING, TYPE and FIELDS.
Where the subtree is created depends on PREFIX." Where the subtree is created depends on PREFIX."
(org-insert-heading prefix) (org-insert-heading prefix)
(insert heading) (insert heading)
@ -407,7 +459,7 @@ Where the subtree is created depends on PREFIX."
(and (not (string-blank-p deck)) (and (not (string-blank-p deck))
(string= deck (org-entry-get-with-inheritance anki-editor-prop-deck)))) (string= deck (org-entry-get-with-inheritance anki-editor-prop-deck))))
(org-set-property anki-editor-prop-deck deck)) (org-set-property anki-editor-prop-deck deck))
(org-set-property anki-editor-prop-note-type note-type) (org-set-property anki-editor-prop-note-type type)
(dolist (field fields) (dolist (field fields)
(save-excursion (save-excursion
(org-insert-heading-respect-content) (org-insert-heading-respect-content)
@ -474,7 +526,7 @@ Where the subtree is created depends on PREFIX."
(pcase property (pcase property
((pred (string= anki-editor-prop-deck)) (anki-editor-deck-names)) ((pred (string= anki-editor-prop-deck)) (anki-editor-deck-names))
((pred (string= anki-editor-prop-note-type)) (anki-editor-note-types)) ((pred (string= anki-editor-prop-note-type)) (anki-editor-note-types))
((pred (string= anki-editor-prop-exporter)) (list anki-editor-exporter-raw anki-editor-exporter-default)) ((pred (string= anki-editor-prop-format)) (list "t" "nil"))
((pred (string-match-p (format "%s\\+?" anki-editor-prop-tags))) (anki-editor-all-tags)) ((pred (string-match-p (format "%s\\+?" anki-editor-prop-tags))) (anki-editor-all-tags))
(_ nil))) (_ nil)))
@ -511,20 +563,47 @@ Where the subtree is created depends on PREFIX."
"Get note types from Anki." "Get note types from Anki."
(anki-editor-api-call-result 'modelNames)) (anki-editor-api-call-result 'modelNames))
(defun anki-editor-entry-format ()
(read (or (org-entry-get-with-inheritance anki-editor-prop-format t) "t")))
(defun anki-editor-toggle-format ()
"Cycle ANKI_FROMAT through \"nil\" and \"t\"."
(interactive)
(let ((val (pcase (org-entry-get nil anki-editor-prop-format nil t)
('nil "nil")
("nil" "t")
("t" nil)
(_ "nil"))))
(if val
(org-entry-put nil anki-editor-prop-format val)
(org-entry-delete nil anki-editor-prop-format))))
(defun anki-editor-note-at-point () (defun anki-editor-note-at-point ()
"Make a note struct from current entry." "Make a note struct from current entry."
(let ((org-trust-scanner-tags t) (let ((org-trust-scanner-tags t)
(deck (org-entry-get-with-inheritance anki-editor-prop-deck)) (deck (org-entry-get-with-inheritance anki-editor-prop-deck))
(format (anki-editor-entry-format))
(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 (cl-set-difference (anki-editor--get-tags) (tags (cl-set-difference (anki-editor--get-tags)
anki-editor-ignored-org-tags anki-editor-ignored-org-tags
:test 'string=)) :test #'string=))
(fields (anki-editor--build-fields))) (fields (anki-editor--build-fields)))
(unless deck (error "No deck specified")) (anki-editor--with-collection-data-updated
(when-let ((missing (cl-set-difference
(alist-get note-type anki-editor--model-fields nil nil #'string=)
(mapcar #'car fields)
:test #'string=)))
;; use heading as the missing field
(push (cons (car missing)
(anki-editor--export-string
(substring-no-properties (org-get-heading t t t))
format))
fields)))
(unless deck (error "Missing deck"))
(unless note-type (error "Missing note type")) (unless note-type (error "Missing note type"))
(unless fields (error "Missing fields"))
(make-anki-editor-note :id note-id (make-anki-editor-note :id note-id
:model note-type :model note-type
@ -558,22 +637,18 @@ Return a list of cons of (FIELD-NAME . FIELD-CONTENT)."
for element = (org-element-at-point) for element = (org-element-at-point)
for heading = (substring-no-properties for heading = (substring-no-properties
(org-element-property :raw-value element)) (org-element-property :raw-value element))
for exporter = (or (org-entry-get-with-inheritance anki-editor-prop-exporter) for format = (anki-editor-entry-format)
anki-editor-exporter-default)
for begin = (cond
((string= exporter anki-editor-exporter-raw)
;; contents-begin includes drawers and scheduling data, ;; contents-begin includes drawers and scheduling data,
;; which we'd like to ignore, here we skip these ;; which we'd like to ignore, here we skip these
;; elements and reset contents-begin. ;; elements and reset contents-begin.
(cl-loop for eoh = (org-element-property :contents-begin element) for begin = (cl-loop for eoh = (org-element-property :contents-begin element)
then (org-element-property :end subelem) then (org-element-property :end subelem)
for subelem = (progn for subelem = (progn
(goto-char eoh) (goto-char eoh)
(org-element-context)) (org-element-context))
while (memq (org-element-type subelem) while (memq (org-element-type subelem)
'(drawer planning property-drawer)) '(drawer planning property-drawer))
finally return (org-element-property :begin subelem))) finally return (org-element-property :begin subelem))
(t (org-element-property :contents-begin element)))
for end = (org-element-property :contents-end element) for end = (org-element-property :contents-end element)
for raw = (or (and begin for raw = (or (and begin
end end
@ -584,21 +659,7 @@ Return a list of cons of (FIELD-NAME . FIELD-CONTENT)."
;; scope is `tree' ;; scope is `tree'
(min (point-max) end))) (min (point-max) end)))
"") "")
for content = (cond for content = (anki-editor--export-string raw format)
((string= exporter anki-editor-exporter-raw)
raw)
((string= exporter anki-editor-exporter-default)
(or (org-export-string-as
raw
anki-editor--ox-anki-html-backend
t
anki-editor--ox-export-ext-plist)
;; 8.2.10 version of
;; `org-export-filter-apply-functions'
;; returns nil for an input of empty string,
;; which will cause AnkiConnect to fail
""))
(t (error "Invalid exporter: %s" exporter)))
collect (cons heading content) collect (cons heading content)
;; proceed to next field entry and check last-pt to ;; proceed to next field entry and check last-pt to
;; see if it's already the last entry ;; see if it's already the last entry
@ -686,6 +747,8 @@ of that heading."
(length (anki-editor-map-note-entries t match scope)))) (length (anki-editor-map-note-entries t match scope))))
(acc 0) (acc 0)
(failed 0)) (failed 0))
(anki-editor--with-collection-data-updated
(anki-editor-map-note-entries (anki-editor-map-note-entries
(lambda () (lambda ()
(message "[%d/%d] Processing notes in buffer \"%s\", wait a moment..." (message "[%d/%d] Processing notes in buffer \"%s\", wait a moment..."
@ -695,7 +758,7 @@ of that heading."
(anki-editor--push-note (anki-editor-note-at-point)) (anki-editor--push-note (anki-editor-note-at-point))
(error (cl-incf failed) (error (cl-incf failed)
(anki-editor--set-failure-reason (error-message-string err))))) (anki-editor--set-failure-reason (error-message-string err)))))
match scope) match scope))
(message (message
(cond (cond
@ -732,29 +795,22 @@ matching non-empty `ANKI_FAILURE_REASON' properties."
"Insert a note interactively. "Insert a note interactively.
Where the note subtree is placed depends on PREFIX, which is the 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")
(message "Fetching note types...")
(let* ((deck (or (org-entry-get-with-inheritance anki-editor-prop-deck)
(progn
(message "Fetching decks...")
(completing-read "Choose a deck: "
(sort (anki-editor-deck-names) #'string-lessp)))))
(note-type (completing-read "Choose a note type: "
(sort (anki-editor-note-types) #'string-lessp)))
(fields (progn
(message "Fetching note fields...")
(anki-editor-api-call-result 'modelFieldNames
:modelName note-type)))
(note-heading (read-from-minibuffer "Enter the note heading (optional): ")))
When note heading is not provided, it is used as the first field."
(interactive "P")
(let* ((deck (or (org-entry-get-with-inheritance anki-editor-prop-deck)
(completing-read "Deck: " (sort (anki-editor-deck-names) #'string-lessp))))
(type (completing-read "Note type: " (sort (anki-editor-note-types) #'string-lessp)))
(fields (anki-editor-api-call-result 'modelFieldNames :modelName type))
(heading (read-from-minibuffer "Note heading (optional): ")))
(anki-editor--insert-note-skeleton prefix (anki-editor--insert-note-skeleton prefix
deck deck
(if (string-blank-p note-heading) heading
"Item" type
note-heading) (if (string-blank-p heading)
note-type (cdr fields)
fields))) fields))))
(defun anki-editor-cloze-region (&optional arg hint) (defun anki-editor-cloze-region (&optional arg hint)
"Cloze region with number ARG." "Cloze region with number ARG."

View file

@ -1,5 +1,7 @@
#+PROPERTY: ANKI_DECK Default #+PROPERTY: ANKI_DECK Default
*Tip: Click the Raw button on the right to view the original Org file*
* Deck in file * Deck in file
:PROPERTIES: :PROPERTIES:
:ANKI_NOTE_TYPE: Cloze :ANKI_NOTE_TYPE: Cloze
@ -30,19 +32,32 @@
** Back ** Back
:PROPERTIES: :PROPERTIES:
:ANKI_EXPORTER: raw :ANKI_FORMAT: nil
:END: :END:
With property <code>:ANKI_EXPORTER: raw</code>, content of the With property <code>:ANKI_FORMAT: nil</code>, content of the
field will be sent to Anki <em>unprocessed</em>. You can use field will be sent to Anki <em>unprocessed</em>. You can use
whatever Anki supports, like HTML tags. whatever Anki supports, like HTML tags.
<br> <br>
<br> <br>
This property is retrieved with inheritance, meaning that it can be This property is retrieved with inheritance, meaning that it can be
set in any ancestor entries or at the top of the file with set in any ancestor entries or at the top of the file with
<code>#+PROPERTY: ANKI_EXPORTER raw</code>, it's also possible to <code>#+PROPERTY: ANKI_FORMAT nil</code>, it's also possible to
override an outer level raw exporter with <code>:ANKI_EXPORTER: override an outer level nil format with <code>:ANKI_FORMAT: t</code>.
default</code>.
* Is there a shorter way to write notes?
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Back
Yes, like this one, Front is missing, ~anki-editor~ will use note
heading as Front. This is neat as sometimes it's verbose to repeat
the same content in note heading and first field.
This works for all note types, just make one field absent and
~anki-editor~ will use note heading as that missing field.
* Languages * Languages
:PROPERTIES: :PROPERTIES:
@ -80,12 +95,12 @@
1) That + 一个完整的句子, that无实际意义 1) That + 一个完整的句子, that无实际意义
2) 由疑问句改装而成 2) 由疑问句改装而成
** Dialects ** Dialects :dialect:
*** Cantonese *** Cantonese
:PROPERTIES: :PROPERTIES:
:ANKI_NOTE_TYPE: Basic (and reversed card) :ANKI_NOTE_TYPE: Basic (and reversed card)
:ANKI_TAGS: dialect cantonese :ANKI_TAGS: cantonese
:END: :END:
**** Front **** Front