From 7dcae54caa5a3dbb864b3ea70a211f3fee16b5f4 Mon Sep 17 00:00:00 2001 From: louie Date: Mon, 24 Feb 2020 13:39:31 +0800 Subject: [PATCH] Allow to put one field in heading --- Changelog.org | 1 + README.org | 32 ++++---- anki-editor.el | 212 +++++++++++++++++++++++++++++++------------------ examples.org | 29 +++++-- 4 files changed, 173 insertions(+), 101 deletions(-) diff --git a/Changelog.org b/Changelog.org index 287a724..587de8f 100644 --- a/Changelog.org +++ b/Changelog.org @@ -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 diff --git a/README.org b/README.org index 45095ce..0094c3c 100644 --- a/README.org +++ b/README.org @@ -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 diff --git a/anki-editor.el b/anki-editor.el index 8fbc91c..b90aef2 100644 --- a/anki-editor.el +++ b/anki-editor.el @@ -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." diff --git a/examples.org b/examples.org index 58e846c..3bb1c04 100644 --- a/examples.org +++ b/examples.org @@ -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 :ANKI_EXPORTER: raw, content of the + With property :ANKI_FORMAT: nil, content of the field will be sent to Anki unprocessed. You can use whatever Anki supports, like HTML tags.

This property is retrieved with inheritance, meaning that it can be set in any ancestor entries or at the top of the file with - #+PROPERTY: ANKI_EXPORTER raw, it's also possible to - override an outer level raw exporter with :ANKI_EXPORTER: - default. + #+PROPERTY: ANKI_FORMAT nil, it's also possible to + override an outer level nil format with :ANKI_FORMAT: t. + +* 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