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
~anki-editor-latex-style~
8. Minor internal code refactoring
9. Allows to put one field in heading
** v0.3.3

View file

@ -61,22 +61,22 @@ there are any ambiguity or grammatical mistakes ;-)/
** Commands
| Command | Description |
|------------------------------------+---------------------------------------------------------------------------------------------------|
| 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-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-insert-note | Insert a note entry like ~M-RET~, interactively. |
| 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-export-subtree-to-html | Export the subtree at point to HTML. |
| anki-editor-convert-region-to-html | Convert and replace region to HTML. |
| anki-editor-api-check | Check if correct version of AnkiConnect is running. |
| anki-editor-api-upgrade | Upgrade AnkiConnect. |
| anki-editor-sync-collections | Synchronize your local anki collection. |
| anki-editor-gui-browse | Open Anki Browser with a query for current note or deck. |
| anki-editor-gui-add-cards | Open Anki Add Cards dialog with presets from current note entry. |
| Command | Description |
|------------------------------------+--------------------------------------------------------------------------------------------------------------------------------|
| 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-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-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-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-convert-region-to-html | Convert and replace region to HTML. |
| anki-editor-api-check | Check if correct version of AnkiConnect is running. |
| anki-editor-api-upgrade | Upgrade AnkiConnect. |
| anki-editor-sync-collections | Synchronize your local anki collection. |
| anki-editor-gui-browse | Open Anki Browser with a query for current note or deck. |
| anki-editor-gui-add-cards | Open Anki Add Cards dialog with presets from current note entry. |
** Variables

View file

@ -114,7 +114,10 @@ form entries."
(cl-defun anki-editor--fetch (url
&rest settings
&key type data success _error parser
&key
(type "GET")
data success _error
(parser 'buffer-string)
&allow-other-keys)
"This is a simplistic little function to make http requests using cURL.
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."
(let ((tempfile (make-temp-file "emacs-anki-editor"))
(responsebuf (generate-new-buffer " *anki-editor-curl*")))
(with-temp-file tempfile
(setq buffer-file-coding-system 'utf-8)
(set-buffer-multibyte t)
(insert data))
(when data
(with-temp-file tempfile
(setq buffer-file-coding-system 'utf-8)
(set-buffer-multibyte t)
(insert data)))
(unwind-protect
(with-current-buffer responsebuf
(apply #'call-process "curl" nil t nil (list
@ -142,7 +146,8 @@ more digging."
(concat "@" tempfile)))
(goto-char (point-min))
(apply success (list :data (funcall parser))))
(when success
(apply success (list :data (funcall parser)))))
(kill-buffer responsebuf)
(delete-file tempfile))))
@ -250,7 +255,7 @@ The result is the path to the newly stored media file."
(latex-environment . anki-editor--ox-latex))))
(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-loop for delims in ,table
@ -370,23 +375,70 @@ The implementation is borrowed and simplified from ox-html."
(t (throw 'giveup nil)))))
(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
(defconst anki-editor-prop-note-type "ANKI_NOTE_TYPE")
(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-tags "ANKI_TAGS")
(defconst anki-editor-prop-tags-plus (concat anki-editor-prop-tags "+"))
(defconst anki-editor-prop-failure-reason "ANKI_FAILURE_REASON")
(defconst anki-editor-org-tag-regexp "^\\([[:alnum:]_@#%]+\\)+$")
(defconst anki-editor-exporter-raw "raw")
(defconst anki-editor-exporter-default "default")
(cl-defstruct anki-editor-note
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)
"Simple wrapper that calls `org-map-entries' with
`&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))
(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)
"Insert a note subtree (skeleton) with HEADING, NOTE-TYPE and FIELDS.
(defun anki-editor--insert-note-skeleton (prefix deck heading type fields)
"Insert a note subtree (skeleton) with HEADING, TYPE and FIELDS.
Where the subtree is created depends on PREFIX."
(org-insert-heading prefix)
(insert heading)
@ -407,7 +459,7 @@ Where the subtree is created depends on PREFIX."
(and (not (string-blank-p 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-note-type note-type)
(org-set-property anki-editor-prop-note-type type)
(dolist (field fields)
(save-excursion
(org-insert-heading-respect-content)
@ -474,7 +526,7 @@ Where the subtree is created depends on PREFIX."
(pcase property
((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-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))
(_ nil)))
@ -511,20 +563,47 @@ Where the subtree is created depends on PREFIX."
"Get note types from Anki."
(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 ()
"Make a note struct from current entry."
(let ((org-trust-scanner-tags t)
(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-type (org-entry-get nil anki-editor-prop-note-type))
(tags (cl-set-difference (anki-editor--get-tags)
anki-editor-ignored-org-tags
:test 'string=))
:test #'string=))
(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 fields (error "Missing fields"))
(make-anki-editor-note :id note-id
:model note-type
@ -558,22 +637,18 @@ Return a list of cons of (FIELD-NAME . FIELD-CONTENT)."
for element = (org-element-at-point)
for heading = (substring-no-properties
(org-element-property :raw-value element))
for exporter = (or (org-entry-get-with-inheritance anki-editor-prop-exporter)
anki-editor-exporter-default)
for begin = (cond
((string= exporter anki-editor-exporter-raw)
;; contents-begin includes drawers and scheduling data,
;; which we'd like to ignore, here we skip these
;; elements and reset contents-begin.
(cl-loop for eoh = (org-element-property :contents-begin element)
then (org-element-property :end subelem)
for subelem = (progn
(goto-char eoh)
(org-element-context))
while (memq (org-element-type subelem)
'(drawer planning property-drawer))
finally return (org-element-property :begin subelem)))
(t (org-element-property :contents-begin element)))
for format = (anki-editor-entry-format)
;; contents-begin includes drawers and scheduling data,
;; which we'd like to ignore, here we skip these
;; elements and reset contents-begin.
for begin = (cl-loop for eoh = (org-element-property :contents-begin element)
then (org-element-property :end subelem)
for subelem = (progn
(goto-char eoh)
(org-element-context))
while (memq (org-element-type subelem)
'(drawer planning property-drawer))
finally return (org-element-property :begin subelem))
for end = (org-element-property :contents-end element)
for raw = (or (and begin
end
@ -584,21 +659,7 @@ Return a list of cons of (FIELD-NAME . FIELD-CONTENT)."
;; scope is `tree'
(min (point-max) end)))
"")
for content = (cond
((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)))
for content = (anki-editor--export-string raw format)
collect (cons heading content)
;; proceed to next field entry and check last-pt to
;; see if it's already the last entry
@ -686,16 +747,18 @@ of that heading."
(length (anki-editor-map-note-entries t match scope))))
(acc 0)
(failed 0))
(anki-editor-map-note-entries
(lambda ()
(message "[%d/%d] Processing notes in buffer \"%s\", wait a moment..."
(cl-incf acc) total (buffer-name))
(anki-editor--clear-failure-reason)
(condition-case-unless-debug err
(anki-editor--push-note (anki-editor-note-at-point))
(error (cl-incf failed)
(anki-editor--set-failure-reason (error-message-string err)))))
match scope)
(anki-editor--with-collection-data-updated
(anki-editor-map-note-entries
(lambda ()
(message "[%d/%d] Processing notes in buffer \"%s\", wait a moment..."
(cl-incf acc) total (buffer-name))
(anki-editor--clear-failure-reason)
(condition-case-unless-debug err
(anki-editor--push-note (anki-editor-note-at-point))
(error (cl-incf failed)
(anki-editor--set-failure-reason (error-message-string err)))))
match scope))
(message
(cond
@ -732,29 +795,22 @@ matching non-empty `ANKI_FAILURE_REASON' properties."
"Insert a note interactively.
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* ((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): ")))
same as how it is used by `M-RET'(org-insert-heading).
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
deck
(if (string-blank-p note-heading)
"Item"
note-heading)
note-type
fields)))
heading
type
(if (string-blank-p heading)
(cdr fields)
fields))))
(defun anki-editor-cloze-region (&optional arg hint)
"Cloze region with number ARG."

View file

@ -1,5 +1,7 @@
#+PROPERTY: ANKI_DECK Default
*Tip: Click the Raw button on the right to view the original Org file*
* Deck in file
:PROPERTIES:
:ANKI_NOTE_TYPE: Cloze
@ -30,19 +32,32 @@
** Back
:PROPERTIES:
:ANKI_EXPORTER: raw
:ANKI_FORMAT: nil
: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
whatever Anki supports, like HTML tags.
<br>
<br>
This property is retrieved with inheritance, meaning that it can be
set in any ancestor entries or at the top of the file with
<code>#+PROPERTY: ANKI_EXPORTER raw</code>, it's also possible to
override an outer level raw exporter with <code>:ANKI_EXPORTER:
default</code>.
<code>#+PROPERTY: ANKI_FORMAT nil</code>, it's also possible to
override an outer level nil format with <code>:ANKI_FORMAT: t</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
:PROPERTIES:
@ -80,12 +95,12 @@
1) That + 一个完整的句子, that无实际意义
2) 由疑问句改装而成
** Dialects
** Dialects :dialect:
*** Cantonese
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic (and reversed card)
:ANKI_TAGS: dialect cantonese
:ANKI_TAGS: cantonese
:END:
**** Front