From b6065cd2f7d771779f9aa4ffe05487a2bf2f223b Mon Sep 17 00:00:00 2001 From: louie Date: Mon, 23 Sep 2019 15:41:07 +0800 Subject: [PATCH 01/19] Add more commands --- anki-editor.el | 131 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 99 insertions(+), 32 deletions(-) diff --git a/anki-editor.el b/anki-editor.el index 609ad94..6783f59 100644 --- a/anki-editor.el +++ b/anki-editor.el @@ -1,6 +1,6 @@ ;;; anki-editor.el --- Minor mode for making Anki cards with Org -*- lexical-binding: t; -*- ;; -;; Copyright (C) 2018 Lei Tan +;; Copyright (C) 2018-2019 Lei Tan ;; ;; Description: Make Anki Cards in Org-mode ;; Author: Lei Tan @@ -76,6 +76,7 @@ (defconst anki-editor-prop-failure-reason "ANKI_FAILURE_REASON") (defconst anki-editor-buffer-html-output "*AnkiEditor HTML Output*") (defconst anki-editor-org-tag-regexp "^\\([[:alnum:]_@#%]+\\)+$") +(defconst anki-editor-ankiconnect-version 5) (defgroup anki-editor nil "Customizations for anki-editor." @@ -136,7 +137,8 @@ See https://apps.ankiweb.net/docs/manual.html#latex-conflicts.") (defun anki-editor--anki-connect-invoke (action &optional params) "Invoke AnkiConnect with ACTION and PARAMS." - (let ((request-body (json-encode (anki-editor--anki-connect-action action params 5))) + (let ((request-body (json-encode + (anki-editor--anki-connect-action action params anki-editor-ankiconnect-version))) (request-backend 'curl) (json-array-type 'list) reply err) @@ -170,6 +172,7 @@ See https://apps.ankiweb.net/docs/manual.html#latex-conflicts.") .result)) (defun anki-editor--anki-connect-invoke-multi (&rest actions) + "Invoke AnkiConnect with ACTIONS, a list of (action . result-handler) pairs." (-zip-with (lambda (result handler) (when-let ((_ (listp result)) (err (alist-get 'error result))) @@ -203,7 +206,7 @@ The result is the path to the newly stored media file." (when (equal :json-false (anki-editor--anki-connect-invoke-result "retrieveMediaFile" `((filename . ,media-file-name)))) - (message "Storing media file to Anki for %s..." path) + (message "Storing media file %s to Anki, this might take a while" path) (setq content (base64-encode-string (with-temp-buffer (insert-file-contents path) @@ -433,7 +436,6 @@ Where the subtree is created depends on PREFIX." (defun anki-editor--update-note (note) "Request AnkiConnect for updating fields and tags of NOTE." - (let ((queue (anki-editor--anki-connect-invoke-queue))) (funcall queue 'updateNoteFields @@ -550,7 +552,9 @@ Where the subtree is created depends on PREFIX." (mapcar #'org-entry-restore-space values))) (defun anki-editor--build-fields () - "Build a list of fields from subheadings of current heading, each element of which is a cons cell, the car of which is field name and the cdr of which is field content." + "Build a list of fields from subheadings of current heading, +each element of which is a cons cell, the car of which is field +name and the cdr of which is field content." (save-excursion (let (fields (point-of-last-child (point))) @@ -620,6 +624,7 @@ Where the subtree is created depends on PREFIX." (defun anki-editor-setup-minor-mode () "Set up this minor mode." + (anki-editor-anki-connect-check) (add-hook 'org-property-allowed-value-functions #'anki-editor--get-allowed-values-for-property nil t) (advice-add 'org-set-tags :before #'anki-editor--before-set-tags) (advice-add 'org-get-buffer-tags :around #'anki-editor--get-buffer-tags) @@ -632,7 +637,7 @@ Where the subtree is created depends on PREFIX." ;;; Commands -(defun anki-editor-push-notes (&optional arg match scope) +(defun anki-editor-push-notes (&optional scope match) "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 always @@ -654,21 +659,18 @@ See doc string of `org-map-entries' for what these different options mean. If one fails, the failure reason will be set in property drawer of that heading." - (interactive "P") + (interactive (list (cond + ((region-active-p) 'region) + ((equal current-prefix-arg '(4)) 'tree) + ((equal current-prefix-arg '(16)) 'file) + ((equal current-prefix-arg '(64)) 'agenda) + (t nil)))) - (unless scope - (setq scope (cond - ((region-active-p) 'region) - ((equal arg '(4)) 'tree) - ((equal arg '(16)) 'file) - ((equal arg '(64)) 'agenda) - (t nil)))) - - (let* ((total (progn - (message "Counting notes...") - (length (anki-editor-map-note-entries t match scope)))) - (acc 0) - (failed 0)) + (let ((total (progn + (message "Counting notes...") + (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..." @@ -678,20 +680,39 @@ of that heading." (anki-editor--push-note (anki-editor-note-at-point)) (error (cl-incf failed) (anki-editor--set-failure-reason (error-message-string err))))) - match - scope) + match scope) - (message (if (= 0 failed) - (format "Successfully pushed %d notes to Anki." acc) - (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 failed))))) + (message + (cond + ((zerop total) "Nothing to push") + ((zerop failed) (format "Pushed %d notes to Anki successfully" acc)) + (t (format "Pushed %d notes in total, among which %d were failed. Check property drawers for failure reasons. +When the issues are resolved, you could repush the failed ones with `anki-editor-retry-failed-notes'." + acc failed)))))) -(defun anki-editor-retry-failure-notes (&optional arg scope) - "Retry pushing notes that were failed. +(defun anki-editor-push-new-notes (&optional scope) + "Push note entries without ANKI_NOTE_ID in SCOPE to Anki." + (interactive) + (anki-editor-push-notes scope (concat anki-editor-prop-note-id "=\"\""))) + +(defun anki-editor-retry-failed-notes (&optional scope) + "Retry pushing notes marked as failed. This command just calls `anki-editor-submit' with match string matching non-empty `ANKI_FAILURE_REASON' properties." - (interactive "P") - (anki-editor-push-notes arg (concat anki-editor-prop-failure-reason "<>\"\"") scope)) + (interactive) + (anki-editor-push-notes scope (concat anki-editor-prop-failure-reason "<>\"\""))) + +(defun anki-editor-delete-notes (noteids) + "Delete notes in NOTEIDS or the note at point." + (interactive (list (list (org-entry-get nil anki-editor-prop-note-id)))) + (when (or (not (called-interactively-p 'interactive)) + (yes-or-no-p (format "Do you really want to delete note %s? The deletion can't be undone. " (nth 0 noteids)))) + (anki-editor--anki-connect-invoke-result + "deleteNotes" + `((notes . ,noteids))) + (org-entry-delete nil anki-editor-prop-note-id) + (when (called-interactively-p 'interactive) + (message "Deleted note %s" (nth 0 noteids))))) (defun anki-editor-insert-note (&optional prefix) "Insert a note interactively. @@ -757,6 +778,19 @@ same as how it is used by `M-RET'(org-insert-heading)." (interactive) (org-export-replace-region-by anki-editor--ox-anki-html-backend)) + +;;; More utilities + +(defun anki-editor-anki-connect-check () + "Check if correct version of AnkiConnect is serving." + (interactive) + (let ((ver (anki-editor--anki-connect-invoke-result "version"))) + (if (<= anki-editor-ankiconnect-version ver) + (when (called-interactively-p 'interactive) + (message "AnkiConnect v.%d is running" ver)) + (error "anki-editor requires minimal version %d of AnkiConnect installed" + anki-editor-ankiconnect-version)))) + (defun anki-editor-anki-connect-upgrade () "Upgrade AnkiConnect to the latest version. @@ -767,11 +801,44 @@ code in the master branch of its Github repo. This is useful when new version of this package depends on the bugfixes or new features of AnkiConnect." (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 "This is going to download the latest AnkiConnect from the Internet to your computer, do you want to continue? ") (let ((result (anki-editor--anki-connect-invoke-result "upgrade"))) (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 for the changes to take effect."))))) +(defun anki-editor-sync-collections () + "Synchronizes the local anki collections with ankiweb." + (interactive) + (anki-editor--anki-connect-invoke "sync")) + +(defun anki-editor-gui-browse (&optional query) + "Open Anki Browser with QUERY. +When called interactively, it will try to set QUERY to current +note or deck." + (interactive (list (pcase (org-entry-get-with-inheritance anki-editor-prop-note-id) + ((and (pred stringp) nid) (format "nid:%s" nid)) + (_ (format "deck:%s" + (or (org-entry-get-with-inheritance anki-editor-prop-deck) + "current")))))) + (anki-editor--anki-connect-invoke "guiBrowse" `((query . ,(or query ""))))) + +(defun anki-editor-gui-add-cards () + "Open Anki Add Cards dialog with presets from current note +entry." + (interactive) + (anki-editor--anki-connect-invoke-result + "guiAddCards" + `((note . ,(cons '(options . ((closeAfterAdding . t))) + (anki-editor--anki-connect-map-note + (anki-editor-note-at-point))))))) + +(defun anki-editor-find-notes (&optional query) + "Find notes with QUERY." + (interactive "sQuery: ") + (let ((nids (anki-editor--anki-connect-invoke-result "findNotes" `((query . ,query))))) + (if (called-interactively-p 'interactive) + (message "%S" nids) + nids))) (provide 'anki-editor) From b8f6986ab933567162bd3367a6f27d87adaf4f80 Mon Sep 17 00:00:00 2001 From: louie Date: Mon, 23 Sep 2019 17:13:10 +0800 Subject: [PATCH 02/19] Doc stuff --- README.org | 98 ++++++++++++++++++++++++++------------------------ anki-editor.el | 80 +++++++++++++++++------------------------ 2 files changed, 84 insertions(+), 94 deletions(-) diff --git a/README.org b/README.org index 71c4904..1a0af29 100644 --- a/README.org +++ b/README.org @@ -1,40 +1,40 @@ [[http://melpa.org/#/anki-editor][file:http://melpa.org/packages/anki-editor-badge.svg]] -anki-editor -- Emacs minor mode for making Anki cards with Org +anki-editor -- An Emacs minor mode for making Anki cards with Org /Since I'm not a native English speaker, feel free to correct me if -there were any ambiguity or grammatical mistakes ;-)/ +there are any ambiguity or grammatical mistakes ;-)/ * Installation *Requirements* - - [[https://github.com/FooSoft/anki-connect#installation][anki-connect]], - an Anki add-on required by this package to interact with Anki. + - [[https://github.com/FooSoft/anki-connect#installation][AnkiConnect]], + an Anki add-on required by this package to interact with Anki - curl - If you have [[http://melpa.org/][MELPA]] in your =package-archives=, - just =M-x package-install RET anki-editor RET=, or install it + If you have [[http://melpa.org/][MELPA]] in your ~package-archives~, + just ~M-x package-install RET anki-editor RET~, or install it manually by downloading and visiting [[./anki-editor.el][anki-editor.el]] in your - emacs buffer, and =M-x package-install-from-buffer RET=. + emacs buffer, and ~M-x package-install-from-buffer RET~. * Usage ** The Layout of Notes - Now you can compose Anki notes in Org syntax, e.g. lists, code - examples, tables, embedded latex, when being submitted to Anki, - they will be converted to HTML by Org-mode's HTML backend with - specific markers (e.g. latex) translated to Anki style. + The power of this mode comes from the builtin HTML export backend + provided by Org, which enables you to use almost all the Org + constructs for writing Anki notes: lists, code blocks, tables, + latex and so on. - The structure of a note is as follow, which is inspired by - =org-drill=. More examples can be found in [[./examples.org][examples.org]]. + The structure of a note is as follows, which is inspired by + ~org-drill~. Check out [[./examples.org][examples.org]] for more examples. #+BEGIN_SRC org - ,* Idiom :vocab:idioms: + ,* Raining :vocab:idioms: :PROPERTIES: :ANKI_DECK: English :ANKI_NOTE_TYPE: Basic (and reversed card) - :ANKI_TAGS: languages european_languages + :ANKI_TAGS: vocab idioms :END: ,** Front (it's) raining cats and dogs @@ -42,35 +42,41 @@ there were any ambiguity or grammatical mistakes ;-)/ it's raining very hard #+END_SRC - - An Anki note is an Org entry with =ANKI_NOTE_TYPE= property - - Anki tags can be set in two ways - 1. With "ANKI_TAGS" property, multiple tags are separated with space - 2. With Org tags [fn:1], this could be turned off if you would like to keep Org tags separated from Anki tags - - Other necessary information (e.g. deck, note type) of a note is - put in the property drawer of the entry - - As the value of =ANKI_DECK= is retrieved with inheritance, you - don't have to set it per note, instead, you could create a deck - entry with this property set and put note entries under it, or - set it per file by ~#+PROPERTY: ANKI_DECK YourDeck~ - - Child entries of a note entry are fields + - Anki deck is provided by ~ANKI_DECK~ property. This property is + retrieved with inheritance, that is to say, it can be put in any + ancestor entries or at top of the file by ~#+PROPERTY: ANKI_DECK + DeckName~. + - ~ANKI_NOTE_TYPE~ property is to specify the Anki note type of a + note and is also required for identifying an Anki note entry. + - Anki tags can be provided in two ways: + 1. With a ~ANKI_TAGS~ property, multiple tags are separated by spaces + 2. With Org tags [fn:1], this could be turned off if you would + like to keep Org tags separated from Anki tags + - Child entries of a note entry are fields. + + Typing all these information by hand could be inefficient and prone + to errors, so this package provides an interactive command + ~anki-editor-insert-note~ to help with this and hooks up + auto-completions for decks, note types and tags etc. ** Commands - | Command | Brief Description | - |------------------------------------+--------------------------------------------------------------------------------------| - | anki-editor-push-notes | Push notes to Anki. Additional arguments can be used to restrict the range of notes. | - | anki-editor-retry-failure-notes | Same as above, except that it only pushes notes that have =ANKI_FAILURE_REASON=. | - | anki-editor-insert-note | Insert a note entry like =M-RET=, interactively. | - | anki-editor-cloze-region | Create a cloze deletion from region. | - | 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. | - -** Functions - - | Name | Description | - |------------------------------+------------------------------------------------------------| - | anki-editor-map-note-entries | Simple wrapper that calls =org-map-entries=. | - | anki-editor-note-at-point | Construct an alist representing a note from current 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. | + | 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-anki-connect-check | Check if correct version of AnkiConnect is running. | + | anki-editor-anki-connect-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 @@ -83,6 +89,7 @@ there were any ambiguity or grammatical mistakes ;-)/ | anki-editor-ignored-org-tags | '("export" "noexport") | A list of Org tags that are ignored when constructing notes form entries. | | anki-editor-org-tags-as-anki-tags | t | If nil, tags of entries wont't be counted as Anki tags. | | anki-editor-protected-tags | '("marked" "leech") | A list of tags that won't be deleted from Anki even though they're absent in Org entries. | + | anki-editor-use-math-jax | nil | Use Anki's built in MathJax support instead of LaTeX. | * Limitations @@ -94,15 +101,14 @@ there were any ambiguity or grammatical mistakes ;-)/ ** Working with Anki add-ons - This package may not work well when you are using certain Anki - add-ons especially those who extend the builtin Anki note editor to + This package might not work well with certain Anki add-ons + especially those who extend the builtin Anki note editor to automatically fill note field content (e.g. ~Add note id~). * Troubleshooting In case of a failed operation and this package doesn't provide much - useful information, especially for note creation, don't be - frustrated, see below for some hints. + useful information, don't be frustrated, see below for some hints. 1. Decks don't exist in Anki. This package by default doesn't create decks for you, when trying out this package with ~examples.org~, @@ -139,5 +145,5 @@ there were any ambiguity or grammatical mistakes ;-)/ [fn:1] It should be noted that Org only allows letters, numbers, =_= -and =@= in a tag but Anki allows more, so you may have to edit you +and ~@~ in a tag but Anki allows more, so you may have to edit you Anki tags before they can be used in Org without any surprise. diff --git a/anki-editor.el b/anki-editor.el index 6783f59..8a5dee0 100644 --- a/anki-editor.el +++ b/anki-editor.el @@ -11,20 +11,18 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;;; Commentary: +;; +;; This package is for users of both Emacs and Anki, who'd like to +;; make Anki cards in Org mode. With this package, Anki cards can be +;; made from an Org buffer like below: (inspired by org-drill) ;; -;; This package is for people who use Anki as SRS but would like to -;; make cards in Org-mode. -;; -;; With this package, you can make cards from something like: -;; (which is inspired by `org-dirll') -;; -;; * Item :emacs:lisp:programming: +;; * Sample :emacs:lisp:programming: ;; :PROPERTIES: ;; :ANKI_DECK: Computing ;; :ANKI_NOTE_TYPE: Basic ;; :END: ;; ** Front -;; How to hello world in elisp ? +;; How to say "hello world" in elisp? ;; ** Back ;; #+BEGIN_SRC emacs-lisp ;; (message "Hello, world!") @@ -32,13 +30,13 @@ ;; ;; This package extends Org-mode's built-in HTML backend to generate ;; HTML for contents of note fields with specific syntax (e.g. latex) -;; translated to Anki style, then save the note to Anki. +;; translated to Anki style. ;; ;; For this package to work, you have to setup these external dependencies: ;; - curl -;; - AnkiConnect, an Anki addon that runs an HTTP server to expose -;; Anki functions as RESTful APIs, see -;; https://github.com/FooSoft/anki-connect#installation +;; - AnkiConnect, an Anki addon that runs an RPC server over HTTP to expose +;; Anki functions as APIs, +;; see https://github.com/FooSoft/anki-connect#installation ;; for installation instructions ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -93,16 +91,19 @@ See https://apps.ankiweb.net/docs/manual.html#latex-conflicts.") (defcustom anki-editor-org-tags-as-anki-tags t - "If nil, tags of entries wont't be counted as Anki tags.") + "If nil, tags of entries won't be counted as Anki tags.") (defcustom anki-editor-protected-tags '("marked" "leech") - "A list of tags that won't be deleted from Anki even though they're absent in Org entries, such as special tags `marked', `leech'." + "A list of tags that won't be deleted from Anki even though +they're absent in Org entries, such as special tags `marked', +`leech'." :type '(repeat string)) (defcustom anki-editor-ignored-org-tags (append org-export-select-tags org-export-exclude-tags) - "A list of Org tags that are ignored when constructing notes form entries." + "A list of Org tags that are ignored when constructing notes +form entries." :type '(repeat string)) (defcustom anki-editor-anki-connect-listening-address @@ -142,31 +143,23 @@ See https://apps.ankiweb.net/docs/manual.html#latex-conflicts.") (request-backend 'curl) (json-array-type 'list) reply err) - - (let ((response (request (format "http://%s:%s" - anki-editor-anki-connect-listening-address - anki-editor-anki-connect-listening-port) - :type "POST" - :parser 'json-read - :data request-body - :success (cl-function (lambda (&key data &allow-other-keys) - (setq reply data))) - :error (cl-function (lambda (&key _ &key error-thrown &allow-other-keys) - (setq err (string-trim (cdr error-thrown))))) - :sync t))) - - ;; HACK: With sync set to t, `request' waits for curl process to - ;; exit, then response data becomes available, but callbacks - ;; might not be called right away but at a later time, that's - ;; why here we manually invoke callbacks to receive the result. - (unless (request-response-done-p response) - (request--curl-callback (get-buffer-process (request-response--buffer response)) "finished\n"))) - + (request (format "http://%s:%s" + anki-editor-anki-connect-listening-address + anki-editor-anki-connect-listening-port) + :type "POST" + :parser 'json-read + :data request-body + :success (cl-function (lambda (&key data &allow-other-keys) + (setq reply data))) + :error (cl-function (lambda (&key _ &key error-thrown &allow-other-keys) + (setq err (string-trim (cdr error-thrown))))) + :sync t) (when err (error "Error communicating with AnkiConnect using cURL: %s" err)) (or reply (error "Got empty reply from AnkiConnect")))) (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-alist (anki-editor--anki-connect-invoke ,@args) (when .error (error .error)) .result)) @@ -380,7 +373,8 @@ The implementation is borrowed and simplified from ox-html." ;;; Core Functions (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." + "Simple wrapper that calls `org-map-entries' with + `&ANKI_NOTE_TYPE<>\"\"' appended to MATCH." ;; disable property inheritance temporarily, or all subheadings of a ;; note heading will be counted as note headings as well (let ((org-use-property-inheritance nil)) @@ -391,7 +385,6 @@ The implementation is borrowed and simplified from ox-html." Where the subtree is created depends on PREFIX." (org-insert-heading prefix) (insert heading) - (unless (save-excursion (org-up-heading-safe) ;; don't insert `ANKI_DECK' if some ancestor already has @@ -399,9 +392,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) - (dolist (field fields) (save-excursion (org-insert-heading-respect-content) @@ -426,12 +417,10 @@ Where the subtree is created depends on PREFIX." (funcall queue 'createDeck `((deck . ,(alist-get 'deck note))))) - (funcall queue 'addNote `((note . ,(anki-editor--anki-connect-map-note note))) #'anki-editor--set-note-id) - (funcall queue))) (defun anki-editor--update-note (note) @@ -440,7 +429,6 @@ Where the subtree is created depends on PREFIX." (funcall queue 'updateNoteFields `((note . ,(anki-editor--anki-connect-map-note note)))) - (funcall queue 'notesInfo `((notes . (,(alist-get 'note-id note)))) @@ -454,19 +442,15 @@ Where the subtree is created depends on PREFIX." (alist-get 'tags note)) anki-editor-protected-tags)) (tag-queue (anki-editor--anki-connect-invoke-queue))) - (when tags-to-add (funcall tag-queue 'addTags `((notes . (,(alist-get 'note-id note))) (tags . ,(mapconcat #'identity tags-to-add " "))))) - (when tags-to-remove (funcall tag-queue 'removeTags `((notes . (,(alist-get 'note-id note))) (tags . ,(mapconcat #'identity tags-to-remove " "))))) - (funcall tag-queue)))) - (funcall queue))) (defun anki-editor--set-failure-reason (reason) @@ -638,7 +622,7 @@ name and the cdr of which is field content." ;;; Commands (defun anki-editor-push-notes (&optional scope match) - "Build notes from headings that can be matched by MATCH within SCOPE and push them to Anki. + "Build notes from headings that match MATCH within SCOPE and push them to Anki. The default search condition `&ANKI_NOTE_TYPE<>\"\"' will always be appended to MATCH. From 5a19baefa93a655c17008240097258fdba08696c Mon Sep 17 00:00:00 2001 From: louie Date: Mon, 23 Sep 2019 17:53:07 +0800 Subject: [PATCH 03/19] Update examples --- examples.org | 63 +++++++++++++++++++++++++++------------------------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/examples.org b/examples.org index 78093e8..8e06c70 100644 --- a/examples.org +++ b/examples.org @@ -1,32 +1,32 @@ #+PROPERTY: ANKI_DECK Default -* Fact +* Deck in file :PROPERTIES: :ANKI_NOTE_TYPE: Cloze :END: ** Text - Cards of this note wil be created in {{c1::Default::which deck ?}} + Cards of this note will be created in {{c1::Default::which deck?}} -* Fact +* Deck in entry :PROPERTIES: - :ANKI_DECK: English + :ANKI_DECK: Languages :ANKI_NOTE_TYPE: Cloze :END: ** Text - Cards of this note wil be created in {{c1::English::which deck ?}} + Cards of this note will be created in {{c1::Languages::which deck?}} -* The English Language +* Languages :PROPERTIES: - :ANKI_DECK: English + :ANKI_DECK: Languages :END: ** Vocabulary -*** Item :vocab:idioms: +*** Raining :vocab:idioms:english: :PROPERTIES: :ANKI_NOTE_TYPE: Basic (and reversed card) :END: @@ -39,35 +39,51 @@ it's raining very hard -** Grammar :grammar: +** Grammar :grammar:english: -*** Item +*** 名词从句 :PROPERTIES: :ANKI_NOTE_TYPE: Basic :END: **** Front - 说出名词从句的形式 + 名词从句有哪些形式? **** Back - 1) that + 一个完整的句子, that无实际意义 + 1) That + 一个完整的句子, that无实际意义 2) 由疑问句改装而成 +** Dialects + +*** Cantonese + :PROPERTIES: + :ANKI_NOTE_TYPE: Basic (and reversed card) + :ANKI_TAGS: dialect cantonese + :END: + +**** Front + + 食咗饭未吖? + +**** Back + + 吃过饭了没? + * Computing :PROPERTIES: :ANKI_DECK: Computing :END: -** Item :lisp:emacs:programming: +** Emacs Lisp :lisp:emacs:programming: :PROPERTIES: :ANKI_NOTE_TYPE: Basic :END: *** Front - How to trap errors in elisp ? + How to trap errors in emacs lisp? *** Back @@ -95,32 +111,19 @@ :ANKI_DECK: Mathematics :END: -** Item1 - :PROPERTIES: - :ANKI_NOTE_TYPE: Cloze - :END: - -*** Text - - The dot product of two vectors is {{c1::$|\alpha| \cdot |\beta| \cos{\varphi}$}} - -*** Extra - -** Item2 +** Dot product :PROPERTIES: :ANKI_NOTE_TYPE: Basic :END: *** Front - Given two vectors: + How to calculate the dot product of two vectors: \begin{equation*} \alpha = \{a_1, a_2, a_3\}, \beta = \{b_1, b_2, b_3\} \end{equation*} - What's the result of $\alpha \cdot \beta$ ? - *** Back - \[a_1b_1 + a_2b_2 + a_3b_3\] + \[\alpha \cdot \beta = a_1b_1 + a_2b_2 + a_3b_3\] From 308cd97a9d107d2aff95e4096e8db9301f8f3676 Mon Sep 17 00:00:00 2001 From: louie Date: Tue, 1 Oct 2019 22:04:43 +0800 Subject: [PATCH 04/19] Add support for exporting field contents literally --- anki-editor.el | 64 ++++++++++++++++++++++++++++++++++---------------- examples.org | 25 ++++++++++++++++++++ 2 files changed, 69 insertions(+), 20 deletions(-) diff --git a/anki-editor.el b/anki-editor.el index 8a5dee0..e3c9df5 100644 --- a/anki-editor.el +++ b/anki-editor.el @@ -68,12 +68,15 @@ (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-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-buffer-html-output "*AnkiEditor HTML Output*") (defconst anki-editor-org-tag-regexp "^\\([[:alnum:]_@#%]+\\)+$") +(defconst anki-editor-exporter-raw "raw") +(defconst anki-editor-exporter-default "default") (defconst anki-editor-ankiconnect-version 5) (defgroup anki-editor nil @@ -466,6 +469,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-match-p (format "%s\\+?" anki-editor-prop-tags))) (anki-editor-all-tags)) (_ nil))) @@ -552,27 +556,47 @@ name and the cdr of which is field content." :raw-value field-heading))) (contents-begin (org-element-property :contents-begin field-heading)) - (contents-end (org-element-property :contents-end field-heading))) - + (contents-end (org-element-property :contents-end field-heading)) + (exporter (or (org-entry-get-with-inheritance anki-editor-prop-exporter) + anki-editor-exporter-default)) + (end-of-header (org-element-property :contents-begin field-heading)) + raw-content + content-elem) + (when (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. + (while (progn + (goto-char end-of-header) + (setq content-elem (org-element-context)) + (memq (car content-elem) '(drawer planning property-drawer))) + (setq end-of-header (org-element-property :end content-elem))) + (setq contents-begin (org-element-property :begin content-elem))) + (setq raw-content (or (and contents-begin + contents-end + (buffer-substring + contents-begin + ;; in case the buffer is narrowed, + ;; e.g. by `org-map-entries' when + ;; scope is `tree' + (min (point-max) contents-end))) + "")) (push (cons field-name - (cond - ((and contents-begin contents-end) (or (org-export-string-as - (buffer-substring - contents-begin - ;; in case the buffer is narrowed, - ;; e.g. by `org-map-entries' when - ;; scope is `tree' - (min (point-max) contents-end)) - 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 ""))) + (pcase exporter + ((pred (string= anki-editor-exporter-raw)) + raw-content) + ((pred (string= anki-editor-exporter-default)) + (or (org-export-string-as + raw-content + 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 + "")) + (_ (error "Invalid exporter: %s" exporter)))) fields) (org-forward-heading-same-level nil t)))) (reverse fields)))) diff --git a/examples.org b/examples.org index 8e06c70..58e846c 100644 --- a/examples.org +++ b/examples.org @@ -19,6 +19,31 @@ Cards of this note will be created in {{c1::Languages::which deck?}} +* Raw fields + :PROPERTIES: + :ANKI_NOTE_TYPE: Basic + :END: + +** Front + + How to send the content of a field or fields to Anki as is? + +** Back + :PROPERTIES: + :ANKI_EXPORTER: raw + :END: + + With property :ANKI_EXPORTER: raw, 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. + * Languages :PROPERTIES: :ANKI_DECK: Languages From 3c5267513404e397e2285a65b0ebbe3f671c6e46 Mon Sep 17 00:00:00 2001 From: louie Date: Sun, 3 Nov 2019 00:20:51 +0800 Subject: [PATCH 05/19] Compute SHA1 against file content instead of file name --- anki-editor.el | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/anki-editor.el b/anki-editor.el index e3c9df5..fe6e6e7 100644 --- a/anki-editor.el +++ b/anki-editor.el @@ -193,7 +193,10 @@ or raise an error." (defun anki-editor--anki-connect-store-media-file (path) "Store media file for PATH, which is an absolute file name. The result is the path to the newly stored media file." - (let* ((hash (secure-hash 'sha1 path)) + (let* ((bytes (with-temp-buffer + (insert-file-contents-literally path) + (buffer-string))) + (hash (secure-hash 'sha1 bytes)) (media-file-name (format "%s-%s%s" (file-name-base path) hash @@ -203,10 +206,7 @@ The result is the path to the newly stored media file." "retrieveMediaFile" `((filename . ,media-file-name)))) (message "Storing media file %s to Anki, this might take a while" path) - (setq content (base64-encode-string - (with-temp-buffer - (insert-file-contents path) - (buffer-string)))) + (setq content (base64-encode-string bytes)) (anki-editor--anki-connect-invoke-result "storeMediaFile" `((filename . ,media-file-name) From 375d6d66d2d23f864b4513bc6b8bf7613d489d16 Mon Sep 17 00:00:00 2001 From: louie Date: Sun, 3 Nov 2019 00:22:27 +0800 Subject: [PATCH 06/19] Refactor api functions --- anki-editor.el | 104 +++++++++++++++++++++++-------------------------- 1 file changed, 48 insertions(+), 56 deletions(-) diff --git a/anki-editor.el b/anki-editor.el index fe6e6e7..16c80ac 100644 --- a/anki-editor.el +++ b/anki-editor.el @@ -77,7 +77,7 @@ (defconst anki-editor-org-tag-regexp "^\\([[:alnum:]_@#%]+\\)+$") (defconst anki-editor-exporter-raw "raw") (defconst anki-editor-exporter-default "default") -(defconst anki-editor-ankiconnect-version 5) +(defconst anki-editor-api-version 5) (defgroup anki-editor nil "Customizations for anki-editor." @@ -109,11 +109,11 @@ they're absent in Org entries, such as special tags `marked', form entries." :type '(repeat string)) -(defcustom anki-editor-anki-connect-listening-address +(defcustom anki-editor-api-host "127.0.0.1" "The network address AnkiConnect is listening.") -(defcustom anki-editor-anki-connect-listening-port +(defcustom anki-editor-api-port "8765" "The port number AnkiConnect is listening.") @@ -122,14 +122,6 @@ form entries." ;;; AnkiConnect -(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) @@ -139,19 +131,22 @@ form entries." (apply #'anki-editor--anki-connect-invoke-multi (nreverse action-queue)) (setq action-queue nil)))))) -(defun anki-editor--anki-connect-invoke (action &optional params) +(defun anki-editor-api-call (action &rest params) "Invoke AnkiConnect with ACTION and PARAMS." - (let ((request-body (json-encode - (anki-editor--anki-connect-action action params anki-editor-ankiconnect-version))) + (let ((payload (list :action action :version anki-editor-api-version)) (request-backend 'curl) (json-array-type 'list) reply err) + + (when params + (plist-put payload :params params)) + (request (format "http://%s:%s" - anki-editor-anki-connect-listening-address - anki-editor-anki-connect-listening-port) + anki-editor-api-host + anki-editor-api-port) :type "POST" :parser 'json-read - :data request-body + :data (json-encode payload) :success (cl-function (lambda (&key data &allow-other-keys) (setq reply data))) :error (cl-function (lambda (&key _ &key error-thrown &allow-other-keys) @@ -160,12 +155,12 @@ form entries." (when err (error "Error communicating with AnkiConnect using cURL: %s" err)) (or reply (error "Got empty reply from AnkiConnect")))) -(defmacro anki-editor--anki-connect-invoke-result (&rest args) +(defun anki-editor-api-call-result (&rest args) "Invoke AnkiConnect with ARGS, return the result from response or raise an error." - `(let-alist (anki-editor--anki-connect-invoke ,@args) - (when .error (error .error)) - .result)) + (let-alist (apply #'anki-editor-api-call args) + (when .error (error .error)) + .result)) (defun anki-editor--anki-connect-invoke-multi (&rest actions) "Invoke AnkiConnect with ACTIONS, a list of (action . result-handler) pairs." @@ -190,7 +185,7 @@ or raise an error." ;; to be type of list. (cons "tags" (vconcat .tags))))) -(defun anki-editor--anki-connect-store-media-file (path) +(defun anki-editor-api--store-media-file (path) "Store media file for PATH, which is an absolute file name. The result is the path to the newly stored media file." (let* ((bytes (with-temp-buffer @@ -200,17 +195,14 @@ The result is the path to the newly stored media file." (media-file-name (format "%s-%s%s" (file-name-base path) hash - (file-name-extension path t))) - content) - (when (equal :json-false (anki-editor--anki-connect-invoke-result - "retrieveMediaFile" - `((filename . ,media-file-name)))) + (file-name-extension path t)))) + (when (eq :json-false + (anki-editor-api-call-result 'retrieveMediaFile + :filename media-file-name)) (message "Storing media file %s to Anki, this might take a while" path) - (setq content (base64-encode-string bytes)) - (anki-editor--anki-connect-invoke-result - "storeMediaFile" - `((filename . ,media-file-name) - (data . ,content)))) + (anki-editor-api-call-result 'storeMediaFile + :filename media-file-name + :data (base64-encode-string bytes))) media-file-name)) @@ -331,7 +323,7 @@ The implementation is borrowed and simplified from ox-html." (file-name-absolute-p raw-path)) (setq raw-path (concat (file-name-as-directory home) raw-path))) ;; storing file to Anki and return the modified path - (anki-editor--anki-connect-store-media-file (expand-file-name (url-unhex-string raw-path))))) + (anki-editor-api--store-media-file (expand-file-name (url-unhex-string raw-path))))) (t (throw 'giveup nil)))) (attributes-plist (let* ((parent (org-export-get-parent-element link)) @@ -354,7 +346,7 @@ The implementation is borrowed and simplified from ox-html." ;; Audio file. ((string-suffix-p ".mp3" path t) - (format "[sound:%s]" path)) + (format "[sound:%s]" path)) ;; External link with a description part. ((and path desc) (format "%s" @@ -479,11 +471,11 @@ Where the subtree is created depends on PREFIX." (defun anki-editor-all-tags () "Get all tags from Anki." - (anki-editor--anki-connect-invoke-result "getTags")) + (anki-editor-api-call-result 'getTags)) (defun anki-editor-deck-names () "Get all decks names from Anki." - (anki-editor--anki-connect-invoke-result "deckNames")) + (anki-editor-api-call-result 'deckNames)) (defun anki-editor--enable-tag-completion () (and anki-editor-mode anki-editor-org-tags-as-anki-tags)) @@ -504,7 +496,7 @@ Where the subtree is created depends on PREFIX." (defun anki-editor-note-types () "Get note types from Anki." - (anki-editor--anki-connect-invoke-result "modelNames")) + (anki-editor-api-call-result 'modelNames)) (defun anki-editor-note-at-point () "Construct an alist representing a note from current entry." @@ -632,7 +624,7 @@ name and the cdr of which is field content." (defun anki-editor-setup-minor-mode () "Set up this minor mode." - (anki-editor-anki-connect-check) + (anki-editor-api-check) (add-hook 'org-property-allowed-value-functions #'anki-editor--get-allowed-values-for-property nil t) (advice-add 'org-set-tags :before #'anki-editor--before-set-tags) (advice-add 'org-get-buffer-tags :around #'anki-editor--get-buffer-tags) @@ -715,9 +707,8 @@ matching non-empty `ANKI_FAILURE_REASON' properties." (interactive (list (list (org-entry-get nil anki-editor-prop-note-id)))) (when (or (not (called-interactively-p 'interactive)) (yes-or-no-p (format "Do you really want to delete note %s? The deletion can't be undone. " (nth 0 noteids)))) - (anki-editor--anki-connect-invoke-result - "deleteNotes" - `((notes . ,noteids))) + (anki-editor-api-call-result 'deleteNotes + :notes noteids) (org-entry-delete nil anki-editor-prop-note-id) (when (called-interactively-p 'interactive) (message "Deleted note %s" (nth 0 noteids))))) @@ -738,7 +729,8 @@ same as how it is used by `M-RET'(org-insert-heading)." (sort (anki-editor-note-types) #'string-lessp))) (fields (progn (message "Fetching note fields...") - (anki-editor--anki-connect-invoke-result "modelFieldNames" `((modelName . ,note-type))))) + (anki-editor-api-call-result 'modelFieldNames + :modelName note-type))) (note-heading (read-from-minibuffer "Enter the note heading (optional): "))) (anki-editor--insert-note-skeleton prefix @@ -789,17 +781,17 @@ same as how it is used by `M-RET'(org-insert-heading)." ;;; More utilities -(defun anki-editor-anki-connect-check () +(defun anki-editor-api-check () "Check if correct version of AnkiConnect is serving." (interactive) - (let ((ver (anki-editor--anki-connect-invoke-result "version"))) - (if (<= anki-editor-ankiconnect-version ver) + (let ((ver (anki-editor-api-call-result 'version))) + (if (<= anki-editor-api-version ver) (when (called-interactively-p 'interactive) (message "AnkiConnect v.%d is running" ver)) (error "anki-editor requires minimal version %d of AnkiConnect installed" - anki-editor-ankiconnect-version)))) + anki-editor-api-version)))) -(defun anki-editor-anki-connect-upgrade () +(defun anki-editor-api-upgrade () "Upgrade AnkiConnect to the latest version. This will display a confirmation dialog box in Anki asking if you @@ -810,14 +802,14 @@ This is useful when new version of this package depends on the bugfixes or new features of AnkiConnect." (interactive) (when (yes-or-no-p "This is going to download the latest AnkiConnect from the Internet to your computer, do you want to continue? ") - (let ((result (anki-editor--anki-connect-invoke-result "upgrade"))) + (let ((result (anki-editor-api-call-result 'upgrade))) (when (and (booleanp result) result) (message "AnkiConnect has been upgraded, you might have to restart Anki for the changes to take effect."))))) (defun anki-editor-sync-collections () "Synchronizes the local anki collections with ankiweb." (interactive) - (anki-editor--anki-connect-invoke "sync")) + (anki-editor-api-call-result 'sync)) (defun anki-editor-gui-browse (&optional query) "Open Anki Browser with QUERY. @@ -828,22 +820,22 @@ note or deck." (_ (format "deck:%s" (or (org-entry-get-with-inheritance anki-editor-prop-deck) "current")))))) - (anki-editor--anki-connect-invoke "guiBrowse" `((query . ,(or query ""))))) + (anki-editor-api-call 'guiBrowse :query (or query ""))) (defun anki-editor-gui-add-cards () "Open Anki Add Cards dialog with presets from current note entry." (interactive) - (anki-editor--anki-connect-invoke-result - "guiAddCards" - `((note . ,(cons '(options . ((closeAfterAdding . t))) - (anki-editor--anki-connect-map-note - (anki-editor-note-at-point))))))) + (anki-editor-api-call-result 'guiAddCards + :note `((:options (:closeAfterAdding t)) + ,(anki-editor--api-note + (anki-editor-note-at-point))))) (defun anki-editor-find-notes (&optional query) "Find notes with QUERY." (interactive "sQuery: ") - (let ((nids (anki-editor--anki-connect-invoke-result "findNotes" `((query . ,query))))) + (let ((nids (anki-editor-api-call-result 'findNotes + :query query))) (if (called-interactively-p 'interactive) (message "%S" nids) nids))) From 0d219e4118f6067ffc0ad77a8bdcba3acced3cb2 Mon Sep 17 00:00:00 2001 From: louie Date: Fri, 1 Nov 2019 22:59:15 +0800 Subject: [PATCH 07/19] Refactor multi api call --- anki-editor.el | 189 ++++++++++++++++++++++++++----------------------- 1 file changed, 99 insertions(+), 90 deletions(-) diff --git a/anki-editor.el b/anki-editor.el index 16c80ac..eddf7df 100644 --- a/anki-editor.el +++ b/anki-editor.el @@ -5,13 +5,13 @@ ;; Description: Make Anki Cards in Org-mode ;; Author: Lei Tan ;; Version: 0.3.3 -;; Package-Requires: ((emacs "25") (request "0.3.0") (dash "2.12.0")) +;; Package-Requires: ((emacs "25.1") (request "0.3.0")) ;; URL: https://github.com/louietan/anki-editor ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;;; Commentary: -;; +;; ;; This package is for users of both Emacs and Anki, who'd like to ;; make Anki cards in Org mode. With this package, Anki cards can be ;; made from an Org buffer like below: (inspired by org-drill) @@ -59,7 +59,6 @@ ;;; Code: (require 'cl-lib) -(require 'dash) (require 'json) (require 'org-element) (require 'ox) @@ -88,10 +87,6 @@ "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.") -(defcustom anki-editor-create-decks - nil - "If non-nil, creates deck before creating a note.") - (defcustom anki-editor-org-tags-as-anki-tags t "If nil, tags of entries won't be counted as Anki tags.") @@ -120,16 +115,8 @@ form entries." (defcustom anki-editor-use-math-jax nil "Use Anki's built in MathJax support instead of LaTeX.") -;;; AnkiConnect -(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)))))) +;;; AnkiConnect (defun anki-editor-api-call (action &rest params) "Invoke AnkiConnect with ACTION and PARAMS." @@ -162,28 +149,48 @@ or raise an error." (when .error (error .error)) .result)) -(defun anki-editor--anki-connect-invoke-multi (&rest actions) - "Invoke AnkiConnect with ACTIONS, a list of (action . result-handler) pairs." - (-zip-with (lambda (result handler) - (when-let ((_ (listp result)) - (err (alist-get 'error result))) - (error err)) - (and handler (funcall handler result))) - (anki-editor--anki-connect-invoke-result - "multi" `((actions . ,(mapcar #'car actions)))) - (mapcar #'cdr actions))) +(defmacro anki-editor-api-with-multi (&rest body) + "Use in combination with `anki-editor-api-enqueue' to combine +multiple api calls into a single 'multi' call, return the results +of these calls in the same order." + `(let (--anki-editor-var-multi-actions-- + --anki-editor-var-multi-results--) + ,@body + (setq --anki-editor-var-multi-results-- + (anki-editor-api-call-result + 'multi + :actions (nreverse + ;; Here we make a vector from the action list, + ;; or `json-encode' will consider it as an association list. + (vconcat + --anki-editor-var-multi-actions--)))) + (cl-loop for result in --anki-editor-var-multi-results-- + do (when-let ((_ (listp result)) + (err (alist-get 'error result))) + (error err)) + collect result))) -(defun anki-editor--anki-connect-map-note (note) +(defmacro anki-editor-api-enqueue (action &rest params) + "Like `anki-editor-api-call', but is only used in combination +with `anki-editor-api-with-multi'. Instead of sending the +request directly, it simply queues the request." + `(let ((action (list :action ,action)) + (params (list ,@params))) + (when params + (plist-put action :params params)) + (push action --anki-editor-var-multi-actions--))) + +(defun anki-editor-api--note (note) "Convert NOTE to the form that AnkiConnect accepts." - (let-alist note - (list (cons "id" .note-id) - (cons "deckName" .deck) - (cons "modelName" .note-type) - (cons "fields" .fields) - ;; Convert tags to a vector since empty list is identical to nil - ;; which will become None in Python, but AnkiConnect requires it - ;; to be type of list. - (cons "tags" (vconcat .tags))))) + (list + :id (string-to-number (or (anki-editor-note-id note) "0")) + :deckName (anki-editor-note-deck note) + :modelName (anki-editor-note-model note) + :fields (anki-editor-note-fields note) + ;; Convert tags to a vector since empty list is identical to nil + ;; which will become None in Python, but AnkiConnect requires it + ;; to be type of list. + :tags (vconcat (anki-editor-note-tags note)))) (defun anki-editor-api--store-media-file (path) "Store media file for PATH, which is an absolute file name. @@ -367,6 +374,9 @@ The implementation is borrowed and simplified from ox-html." ;;; Core Functions +(cl-defstruct anki-editor-note + id model deck fields tags) + (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." @@ -396,9 +406,11 @@ Where the subtree is created depends on PREFIX." (defun anki-editor--push-note (note) "Request AnkiConnect for updating or creating NOTE." - (if (= (alist-get 'note-id note) -1) - (anki-editor--create-note note) - (anki-editor--update-note note))) + (cond + ((null (anki-editor-note-id note)) + (anki-editor--create-note note)) + (t + (anki-editor--update-note note)))) (defun anki-editor--set-note-id (id) (unless id @@ -407,46 +419,37 @@ Where the subtree is created depends on PREFIX." (defun anki-editor--create-note (note) "Request AnkiConnect for creating NOTE." - (let ((queue (anki-editor--anki-connect-invoke-queue))) - (when anki-editor-create-decks - (funcall queue - 'createDeck - `((deck . ,(alist-get 'deck note))))) - (funcall queue - 'addNote - `((note . ,(anki-editor--anki-connect-map-note note))) - #'anki-editor--set-note-id) - (funcall queue))) + (thread-last + (anki-editor-api-with-multi + (anki-editor-api-enqueue 'createDeck + :deck (anki-editor-note-deck note)) + (anki-editor-api-enqueue 'addNote + :note (anki-editor-api--note note))) + (nth 1) + (anki-editor--set-note-id))) (defun anki-editor--update-note (note) "Request AnkiConnect for updating fields and tags of 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 - (let* ((existing-note (car result)) - (tags-to-add (-difference (-difference (alist-get 'tags note) - (alist-get 'tags existing-note)) - anki-editor-ignored-org-tags)) - (tags-to-remove (-difference (-difference (alist-get 'tags existing-note) - (alist-get 'tags note)) - anki-editor-protected-tags)) - (tag-queue (anki-editor--anki-connect-invoke-queue))) - (when tags-to-add - (funcall tag-queue - 'addTags `((notes . (,(alist-get 'note-id note))) - (tags . ,(mapconcat #'identity tags-to-add " "))))) - (when tags-to-remove - (funcall tag-queue - 'removeTags `((notes . (,(alist-get 'note-id note))) - (tags . ,(mapconcat #'identity tags-to-remove " "))))) - (funcall tag-queue)))) - (funcall queue))) + (let* ((oldnote (caar (anki-editor-api-with-multi + (anki-editor-api-enqueue 'notesInfo + :notes (list (anki-editor-note-id note))) + (anki-editor-api-enqueue 'updateNoteFields + :note (anki-editor-api--note note))))) + (tagsadd (cl-set-difference (anki-editor-note-tags note) + (alist-get 'tags oldnote) + :test 'string=)) + (tagsdel (thread-first (alist-get 'tags oldnote) + (cl-set-difference (anki-editor-note-tags note) :test 'string=) + (cl-set-difference anki-editor-protected-tags :test 'string=)))) + (anki-editor-api-with-multi + (when tagsadd + (anki-editor-api-enqueue 'addTags + :notes (list (anki-editor-note-id note)) + :tags (mapconcat #'identity tagsadd " "))) + (when tagsdel + (anki-editor-api-enqueue 'removeTags + :notes (list (anki-editor-note-id note)) + :tags (mapconcat #'identity tagsdel " ")))))) (defun anki-editor--set-failure-reason (reason) "Set failure reason to REASON in property drawer at point." @@ -485,7 +488,7 @@ Where the subtree is created depends on PREFIX." (when (and (anki-editor--enable-tag-completion) (not just-align)) (setq anki-editor--anki-tags-cache (anki-editor-all-tags)) - (unless (-all? #'anki-editor-is-valid-org-tag anki-editor--anki-tags-cache) + (when (cl-notevery #'anki-editor-is-valid-org-tag anki-editor--anki-tags-cache) (warn "Some tags from Anki contain characters that are not valid in Org tags.")))) (defun anki-editor--get-buffer-tags (oldfun) @@ -499,30 +502,32 @@ Where the subtree is created depends on PREFIX." (anki-editor-api-call-result 'modelNames)) (defun anki-editor-note-at-point () - "Construct an alist representing a note from current entry." + "Make a note struct from current entry." (let ((org-trust-scanner-tags t) (deck (org-entry-get-with-inheritance anki-editor-prop-deck)) (note-id (org-entry-get nil anki-editor-prop-note-id)) (note-type (org-entry-get nil anki-editor-prop-note-type)) - (tags (anki-editor--get-tags)) + (tags (cl-set-difference (anki-editor--get-tags) + anki-editor-ignored-org-tags + :test 'string=)) (fields (anki-editor--build-fields))) (unless deck (error "No deck specified")) (unless note-type (error "Missing note type")) (unless fields (error "Missing fields")) - `((deck . ,deck) - (note-id . ,(string-to-number (or note-id "-1"))) - (note-type . ,note-type) - (tags . ,tags) - (fields . ,fields)))) + (make-anki-editor-note :id note-id + :model note-type + :deck deck + :tags tags + :fields fields))) (defun anki-editor--get-tags () (let ((tags (anki-editor--entry-get-multivalued-property-with-inheritance nil anki-editor-prop-tags))) (if anki-editor-org-tags-as-anki-tags - (append tags (org-get-tags-at)) + (append tags (org-get-tags)) tags))) (defun anki-editor--entry-get-multivalued-property-with-inheritance (pom property) @@ -601,7 +606,10 @@ name and the cdr of which is field content." (defun anki-editor--concat-multivalued-property-value (prop value) (let ((old-values (org-entry-get-multivalued-property nil prop))) (unless (string-suffix-p prop "+") - (setq old-values (-difference old-values (org-entry-get-multivalued-property nil (concat prop "+"))))) + (setq old-values (cl-set-difference old-values + (org-entry-get-multivalued-property + nil (concat prop "+")) + :test 'string=))) (mapconcat #'org-entry-protect-space (append old-values (list value)) " "))) @@ -827,15 +835,16 @@ note or deck." entry." (interactive) (anki-editor-api-call-result 'guiAddCards - :note `((:options (:closeAfterAdding t)) - ,(anki-editor--api-note - (anki-editor-note-at-point))))) + :note (append + (anki-editor-api--note + (anki-editor-note-at-point)) + (list :options '(:closeAfterAdding t))))) (defun anki-editor-find-notes (&optional query) "Find notes with QUERY." (interactive "sQuery: ") (let ((nids (anki-editor-api-call-result 'findNotes - :query query))) + :query (or query "")))) (if (called-interactively-p 'interactive) (message "%S" nids) nids))) From df2a90c9b8b21330def0f62526d7ea20fc089413 Mon Sep 17 00:00:00 2001 From: louie Date: Mon, 10 Feb 2020 19:51:02 +0800 Subject: [PATCH 08/19] Remove the dependency on request.el --- anki-editor.el | 59 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/anki-editor.el b/anki-editor.el index eddf7df..8035c9b 100644 --- a/anki-editor.el +++ b/anki-editor.el @@ -5,7 +5,7 @@ ;; Description: Make Anki Cards in Org-mode ;; Author: Lei Tan ;; Version: 0.3.3 -;; Package-Requires: ((emacs "25.1") (request "0.3.0")) +;; Package-Requires: ((emacs "25.1")) ;; URL: https://github.com/louietan/anki-editor ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -63,7 +63,6 @@ (require 'org-element) (require 'ox) (require 'ox-html) -(require 'request) (defconst anki-editor-prop-note-type "ANKI_NOTE_TYPE") (defconst anki-editor-prop-note-id "ANKI_NOTE_ID") @@ -118,6 +117,40 @@ form entries." ;;; AnkiConnect +(cl-defun anki-editor--fetch (url + &rest settings + &key type data success error parser + &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 +request.el's sync mode calls cURL asynchronously under the hood, +which doesn't work on some machines (like mine) where the process +sentinel never gets called. After some debugging of Emacs, it +seems that in 'process.c' the pselect syscall to the file +descriptor of inotify used by 'autorevert' always returns a +nonzero value and causes 'status_notify' never being called. To +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)) + (unwind-protect + (with-current-buffer responsebuf + (apply #'call-process "curl" nil t nil (list + url + "--silent" + "-X" type + "--data-binary" + (concat "@" tempfile))) + + (goto-char (point-min)) + (apply success (list :data (funcall parser)))) + (kill-buffer responsebuf) + (delete-file tempfile)))) + (defun anki-editor-api-call (action &rest params) "Invoke AnkiConnect with ACTION and PARAMS." (let ((payload (list :action action :version anki-editor-api-version)) @@ -128,17 +161,17 @@ form entries." (when params (plist-put payload :params params)) - (request (format "http://%s:%s" - anki-editor-api-host - anki-editor-api-port) - :type "POST" - :parser 'json-read - :data (json-encode payload) - :success (cl-function (lambda (&key data &allow-other-keys) - (setq reply data))) - :error (cl-function (lambda (&key _ &key error-thrown &allow-other-keys) - (setq err (string-trim (cdr error-thrown))))) - :sync t) + (anki-editor--fetch (format "http://%s:%s" + anki-editor-api-host + anki-editor-api-port) + :type "POST" + :parser 'json-read + :data (json-encode payload) + :success (cl-function (lambda (&key data &allow-other-keys) + (setq reply data))) + :error (cl-function (lambda (&key error-thrown &allow-other-keys) + (setq err (string-trim (cdr error-thrown))))) + :sync t) (when err (error "Error communicating with AnkiConnect using cURL: %s" err)) (or reply (error "Got empty reply from AnkiConnect")))) From 1feb3becc3e304f838e004f6376517bad4f52aea Mon Sep 17 00:00:00 2001 From: louie Date: Sun, 3 Nov 2019 17:24:00 +0800 Subject: [PATCH 09/19] Reduce duplication in latex functions --- anki-editor.el | 152 ++++++++++++++++++++----------------------------- 1 file changed, 62 insertions(+), 90 deletions(-) diff --git a/anki-editor.el b/anki-editor.el index 8035c9b..a5546e2 100644 --- a/anki-editor.el +++ b/anki-editor.el @@ -64,19 +64,6 @@ (require 'ox) (require 'ox-html) -(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-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-buffer-html-output "*AnkiEditor HTML Output*") -(defconst anki-editor-org-tag-regexp "^\\([[:alnum:]_@#%]+\\)+$") -(defconst anki-editor-exporter-raw "raw") -(defconst anki-editor-exporter-default "default") -(defconst anki-editor-api-version 5) - (defgroup anki-editor nil "Customizations for anki-editor." :group 'org) @@ -117,6 +104,8 @@ form entries." ;;; AnkiConnect +(defconst anki-editor-api-version 5) + (cl-defun anki-editor--fetch (url &rest settings &key type data success error parser @@ -246,95 +235,68 @@ The result is the path to the newly stored media file." media-file-name)) -;;; Org Export Backend +;;; Org export backend (defconst anki-editor--ox-anki-html-backend - (if anki-editor-use-math-jax - (org-export-create-backend - :parent 'html - :transcoders '((latex-fragment . anki-editor--ox-latex-for-mathjax) - (latex-environment . anki-editor--ox-latex-for-mathjax))) - (org-export-create-backend - :parent 'html - :transcoders '((latex-fragment . anki-editor--ox-latex) - (latex-environment . anki-editor--ox-latex))))) + (org-export-create-backend + :parent 'html + :transcoders '((latex-fragment . anki-editor--ox-latex) + (latex-environment . anki-editor--ox-latex)))) (defconst anki-editor--ox-export-ext-plist '(:with-toc nil :anki-editor-mode t)) -(defun anki-editor--translate-latex-delimiters (latex-code) - (catch 'done - (let ((delimiter-map (list (list (cons (format "^%s" (regexp-quote "$$")) "[$$]") - (cons (format "%s$" (regexp-quote "$$")) "[/$$]")) - (list (cons (format "^%s" (regexp-quote "$")) "[$]") - (cons (format "%s$" (regexp-quote "$")) "[/$]")) - (list (cons (format "^%s" (regexp-quote "\\(")) "[$]") - (cons (format "%s$" (regexp-quote "\\)")) "[/$]")) - (list (cons (format "^%s" (regexp-quote "\\[")) "[$$]") - (cons (format "%s$" (regexp-quote "\\]")) "[/$$]")))) - (matched nil)) - (save-match-data - (dolist (pair delimiter-map) - (dolist (delimiter pair) - (when (setq matched (string-match (car delimiter) latex-code)) - (setq latex-code (replace-match (cdr delimiter) t t latex-code)))) - (when matched (throw 'done latex-code))))) - latex-code)) +(cl-macrolet ((with-table (table) + `(cl-loop for delims in ,table + collect + (list (concat "^" (regexp-quote (cl-first delims))) + (cl-second delims) + (concat (regexp-quote (cl-third delims)) "$") + (cl-fourth delims))))) -(defun anki-editor--translate-latex-delimiters-to-anki-mathjax-delimiters (latex-code) - (catch 'done - (let ((delimiter-map (list (list (cons (format "^%s" (regexp-quote "$$")) "\\[") - (cons (format "%s$" (regexp-quote "$$")) "\\]")) - (list (cons (format "^%s" (regexp-quote "$")) "\\(") - (cons (format "%s$" (regexp-quote "$")) "\\)")))) - (matched nil)) - (save-match-data - (dolist (pair delimiter-map) - (dolist (delimiter pair) - (when (setq matched (string-match (car delimiter) latex-code)) - (setq latex-code (replace-match (cdr delimiter) t t latex-code)))) - (when matched (throw 'done latex-code))))) - latex-code)) + (defconst anki-editor--native-latex-delimiters + (with-table '(("$$" "[$$]" + "$$" "[/$$]") + ("$" "[$]" + "$" "[/$]") + ("\\(" "[$]" + "\\)" "[/$]") + ("\\[" "[$$]" + "\\]" "[/$$]")))) -(defun anki-editor--wrap-latex (content) - "Wrap CONTENT with Anki-style latex markers." - (format "

[latex]
%s
[/latex]

" content)) + (defconst anki-editor--mathjax-delimiters + (with-table '(("$$" "\\[" + "$$" "\\]") + ("$" "\\(" + "$" "\\)"))))) -(defun anki-editor--wrap-latex-for-mathjax (content) - "Wrap CONTENT for Anki's native MathJax support." - (format "

%s

" content)) +(defun anki-editor--translate-latex-fragment (latex-code) + (let ((table (if anki-editor-use-math-jax + anki-editor--mathjax-delimiters + anki-editor--native-latex-delimiters))) + (cl-loop for delims in table + for matches = (string-match (cl-first delims) latex-code) + when matches + do + (setq latex-code (replace-match (cl-second delims) t t latex-code)) + (string-match (cl-third delims) latex-code) + (setq latex-code (replace-match (cl-fourth delims) t t latex-code)) + until matches + finally return latex-code))) -(defun anki-editor--wrap-div (content) - (format "
%s
" content)) +(defun anki-editor--translate-latex-env (latex-code) + (setq latex-code (replace-regexp-in-string "\n" "
" (org-html-encode-plain-text latex-code))) + (if anki-editor-use-math-jax + (concat "\\[
" latex-code "\\]") + (concat "[latex]
" latex-code "[/latex]"))) (defun anki-editor--ox-latex (latex _contents _info) "Transcode LATEX from Org to HTML. CONTENTS is nil. INFO is a plist holding contextual information." (let ((code (org-remove-indentation (org-element-property :value latex)))) - (setq code - (pcase (org-element-type latex) - ('latex-fragment (anki-editor--translate-latex-delimiters code)) - ('latex-environment (anki-editor--wrap-latex - (mapconcat #'anki-editor--wrap-div - (split-string (org-html-encode-plain-text code) "\n") - ""))))) - - (if anki-editor-break-consecutive-braces-in-latex - (replace-regexp-in-string "}}" "} } " code) - code))) - -(defun anki-editor--ox-latex-for-mathjax (latex _contents _info) - "Transcode LATEX from Org to HTML. -CONTENTS is nil. INFO is a plist holding contextual information." - (let ((code (org-remove-indentation (org-element-property :value latex)))) - (setq code - (pcase (org-element-type latex) - ('latex-fragment (anki-editor--translate-latex-delimiters-to-anki-mathjax-delimiters code)) - ('latex-environment (anki-editor--wrap-latex-for-mathjax - (mapconcat #'anki-editor--wrap-div - (split-string (org-html-encode-plain-text code) "\n") - ""))))) - + (setq code (cl-ecase (org-element-type latex) + (latex-fragment (anki-editor--translate-latex-fragment code)) + (latex-environment (anki-editor--translate-latex-env code)))) (if anki-editor-break-consecutive-braces-in-latex (replace-regexp-in-string "}}" "} } " code) code))) @@ -342,7 +304,6 @@ CONTENTS is nil. INFO is a plist holding contextual information." (defun anki-editor--ox-html-link (oldfun link desc info) "When LINK is a link to local file, transcodes it to html and stores the target file to Anki, otherwise calls OLDFUN for help. The implementation is borrowed and simplified from ox-html." - (or (catch 'giveup (unless (plist-get info :anki-editor-mode) (throw 'giveup nil)) @@ -405,7 +366,18 @@ The implementation is borrowed and simplified from ox-html." (funcall oldfun link desc info))) -;;; Core Functions +;;; 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-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) @@ -812,7 +784,7 @@ same as how it is used by `M-RET'(org-insert-heading)." (interactive) (org-export-to-buffer anki-editor--ox-anki-html-backend - anki-editor-buffer-html-output nil t nil t anki-editor--ox-export-ext-plist #'html-mode)) + "*AnkiEditor HTML Output*" nil t nil t anki-editor--ox-export-ext-plist #'html-mode)) (defun anki-editor-convert-region-to-html () "Convert and replace region to HTML." From 36b8884b157b3a2536b8686e07031f2066bef40b Mon Sep 17 00:00:00 2001 From: louie Date: Sun, 3 Nov 2019 21:41:56 +0800 Subject: [PATCH 10/19] Update documentation --- Changelog.org | 15 ++++++++ README.org | 94 ++++++++++++++++++++++++++++++--------------------- 2 files changed, 70 insertions(+), 39 deletions(-) diff --git a/Changelog.org b/Changelog.org index 2c803d6..684e7b5 100644 --- a/Changelog.org +++ b/Changelog.org @@ -2,6 +2,21 @@ * Changelog +** Unreleased + + 1. Removed variable ~anki-editor-create-decks~, now decks are + created automatically + 2. Identifiers prefixed with ~anki-editor-anki-connect-~ were + renamed to ~anki-editor-api-~ + 3. Added two macros ~anki-editor-api-with-multi~ and + ~anki-editor-api-enqueue~ to make working with multi api calls + easier + 4. Fixed computing hash of media files against file names instead + of file content + 5. Supports literal note fields + 6. Added more utility commands/functions + 7. Minor internal code refactoring + ** v0.3.3 - Reenable support for setting tags with =ANKI_TAGS= property diff --git a/README.org b/README.org index 1a0af29..c253bf8 100644 --- a/README.org +++ b/README.org @@ -72,8 +72,8 @@ there are any ambiguity or grammatical mistakes ;-)/ | 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-anki-connect-check | Check if correct version of AnkiConnect is running. | - | anki-editor-anki-connect-upgrade | Upgrade AnkiConnect. | + | 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. | @@ -82,15 +82,65 @@ there are any ambiguity or grammatical mistakes ;-)/ | Name | Default Value | Description | |-----------------------------------------------+------------------------+----------------------------------------------------------------------------------------------------------| - | anki-editor-anki-connect-listening-address | "127.0.0.1" | The network address AnkiConnect is listening. | - | anki-editor-anki-connect-listening-port | "8765" | The port number AnkiConnect is listening. | + | anki-editor-api-host | "127.0.0.1" | The network address AnkiConnect is listening. | + | anki-editor-api-port | "8765" | The port number AnkiConnect is listening. | | anki-editor-break-consecutive-braces-in-latex | nil | If non-nil, consecutive `}' will be automatically separated by spaces to prevent early-closing of cloze. | - | anki-editor-create-decks | nil | If non-nil, creates deck before creating a note. | | anki-editor-ignored-org-tags | '("export" "noexport") | A list of Org tags that are ignored when constructing notes form entries. | | anki-editor-org-tags-as-anki-tags | t | If nil, tags of entries wont't be counted as Anki tags. | | anki-editor-protected-tags | '("marked" "leech") | A list of tags that won't be deleted from Anki even though they're absent in Org entries. | | anki-editor-use-math-jax | nil | Use Anki's built in MathJax support instead of LaTeX. | +** Functions and Macros + +*** anki-editor-map-note-entries + + Simple wrapper that calls ~org-map-entries~ with + ~&ANKI_NOTE_TYPE<>\"\"~ appended to MATCH. + +*** anki-editor-api-call + + Invoke AnkiConnect with ACTION and PARAMS. + +*** anki-editor-api-call-result + + Calls above, returns result field or raise an error. + +*** anki-editor-api-with-multi + + Used in combination with ~anki-editor-api-enqueue~ to queue + multiple api calls and combine them into one 'multi' call at the + end, return the results of these calls in the same order. + + Usage: + #+begin_src elisp + (cl-destructuring-bind (decks models tags notes) + (anki-editor-api-with-multi + ;; The following api calls will be combined into one 'multi' call. + (anki-editor-api-enqueue 'deckNames) + (anki-editor-api-enqueue 'modelNames) + (anki-editor-api-enqueue 'getTags) + (anki-editor-api-enqueue 'findNotes :query "deck:Default")) + (message (concat "decks: %S\n" + "models: %S\n" + "tags: %S\n" + "notes: %S") + decks models tags notes)) + #+end_src + +*** anki-editor-api-enqueue + + Like ~anki-editor-api-call~, but is only used in combination with + ~anki-editor-api-with-multi~. Instead of sending the request + directly, it simply queues the request. + +*** anki-editor-note-at-point + + Make a note struct from current entry. + +*** anki-editor-find-notes + + Find notes with QUERY. + * Limitations ** Tags between Anki and Org @@ -105,40 +155,6 @@ there are any ambiguity or grammatical mistakes ;-)/ especially those who extend the builtin Anki note editor to automatically fill note field content (e.g. ~Add note id~). -* Troubleshooting - - In case of a failed operation and this package doesn't provide much - useful information, don't be frustrated, see below for some hints. - - 1. Decks don't exist in Anki. This package by default doesn't create - decks for you, when trying out this package with ~examples.org~, - you might find that every single note creation fails, simply - because they're fake decks that might not be in your Anki - collection. If you'd like it to automatically create missing - decks, set ~anki-editor-create-decks~ to ~t~. - - 2. Note is counted as a duplicate. From [[https://apps.ankiweb.net/docs/manual.html#adding-cards-and-notes][Anki docs]] - #+BEGIN_QUOTE - Anki checks the first field for uniqueness, so it will warn you - if you enter two cards with a Front field of “apple” (for - example). The uniqueness check is limited to the current note - type, so if you’re studying multiple languages, two cards with - the same Front would not be listed as duplicates as long as you - had a different note type for each language. - #+END_QUOTE - - If all the above don't help, then we have to go deeper to find out - what goes wrong. Here are some methods: - - - Turn on logging in ~request.el~. Customize ~request-log-level~ to - ~debug~, retry failed actions and switch to buffer - ~ *request-log*~ (there's a leading space, see [[https://www.emacswiki.org/emacs/InvisibleBuffers][invisible buffer]]) - to get logs from ~request.el~. This way we can't inspect the - request payload, since it's dumped into a temp file that's deleted - when request finishes. - - Use a traffic sniffer to inspect communications between Emacs and - Anki. - * Demo [[./demo.gif]] From e665f2c28fd1f461992c0fba809592e54c70ba4f Mon Sep 17 00:00:00 2001 From: louie Date: Mon, 11 Nov 2019 23:03:55 +0800 Subject: [PATCH 11/19] Make latex flavor a list of choices --- Changelog.org | 4 +++- README.org | 2 +- anki-editor.el | 35 ++++++++++++++++++----------------- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/Changelog.org b/Changelog.org index 684e7b5..287a724 100644 --- a/Changelog.org +++ b/Changelog.org @@ -15,7 +15,9 @@ of file content 5. Supports literal note fields 6. Added more utility commands/functions - 7. Minor internal code refactoring + 7. ~anki-editor-use-math-jax~ was replaced with + ~anki-editor-latex-style~ + 8. Minor internal code refactoring ** v0.3.3 diff --git a/README.org b/README.org index c253bf8..45095ce 100644 --- a/README.org +++ b/README.org @@ -88,7 +88,7 @@ there are any ambiguity or grammatical mistakes ;-)/ | anki-editor-ignored-org-tags | '("export" "noexport") | A list of Org tags that are ignored when constructing notes form entries. | | anki-editor-org-tags-as-anki-tags | t | If nil, tags of entries wont't be counted as Anki tags. | | anki-editor-protected-tags | '("marked" "leech") | A list of tags that won't be deleted from Anki even though they're absent in Org entries. | - | anki-editor-use-math-jax | nil | Use Anki's built in MathJax support instead of LaTeX. | + | anki-editor-latex-style | builtin | The style of latex to translate into. | ** Functions and Macros diff --git a/anki-editor.el b/anki-editor.el index a5546e2..c76ec1e 100644 --- a/anki-editor.el +++ b/anki-editor.el @@ -98,8 +98,10 @@ form entries." "8765" "The port number AnkiConnect is listening.") -(defcustom anki-editor-use-math-jax nil - "Use Anki's built in MathJax support instead of LaTeX.") +(defcustom anki-editor-latex-style 'builtin + "The style of latex to translate into." + :type '(radio (const :tag "Built-in" builtin) + (const :tag "MathJax" mathjax))) ;;; AnkiConnect @@ -271,24 +273,23 @@ The result is the path to the newly stored media file." "$" "\\)"))))) (defun anki-editor--translate-latex-fragment (latex-code) - (let ((table (if anki-editor-use-math-jax - anki-editor--mathjax-delimiters - anki-editor--native-latex-delimiters))) - (cl-loop for delims in table - for matches = (string-match (cl-first delims) latex-code) - when matches - do - (setq latex-code (replace-match (cl-second delims) t t latex-code)) - (string-match (cl-third delims) latex-code) - (setq latex-code (replace-match (cl-fourth delims) t t latex-code)) - until matches - finally return latex-code))) + (cl-loop for delims in (cl-ecase anki-editor-latex-style + (builtin anki-editor--native-latex-delimiters) + (mathjax anki-editor--mathjax-delimiters)) + for matches = (string-match (cl-first delims) latex-code) + when matches + do + (setq latex-code (replace-match (cl-second delims) t t latex-code)) + (string-match (cl-third delims) latex-code) + (setq latex-code (replace-match (cl-fourth delims) t t latex-code)) + until matches + finally return latex-code)) (defun anki-editor--translate-latex-env (latex-code) (setq latex-code (replace-regexp-in-string "\n" "
" (org-html-encode-plain-text latex-code))) - (if anki-editor-use-math-jax - (concat "\\[
" latex-code "\\]") - (concat "[latex]
" latex-code "[/latex]"))) + (cl-ecase anki-editor-latex-style + (builtin (concat "[latex]
" latex-code "[/latex]")) + (mathjax (concat "\\[
" latex-code "\\]")))) (defun anki-editor--ox-latex (latex _contents _info) "Transcode LATEX from Org to HTML. From b2cbf48911556a33809cc4211bfc5f9a89cd797e Mon Sep 17 00:00:00 2001 From: louie Date: Mon, 10 Feb 2020 23:23:44 +0800 Subject: [PATCH 12/19] Refactor anki-editor--build-fields --- anki-editor.el | 115 ++++++++++++++++++++++++------------------------- 1 file changed, 56 insertions(+), 59 deletions(-) diff --git a/anki-editor.el b/anki-editor.el index c76ec1e..2e4c0e3 100644 --- a/anki-editor.el +++ b/anki-editor.el @@ -543,66 +543,63 @@ Where the subtree is created depends on PREFIX." (mapcar #'org-entry-restore-space values))) (defun anki-editor--build-fields () - "Build a list of fields from subheadings of current heading, -each element of which is a cons cell, the car of which is field -name and the cdr of which is field content." + "Build a list of fields from subheadings of current heading. + +Return a list of cons of (FIELD-NAME . FIELD-CONTENT)." (save-excursion - (let (fields - (point-of-last-child (point))) - (when (org-goto-first-child) - (while (/= point-of-last-child (point)) - (setq point-of-last-child (point)) - (let* ((inhibit-message t) ;; suppress echo message from `org-babel-exp-src-block' - (field-heading (org-element-at-point)) - (field-name (substring-no-properties - (org-element-property - :raw-value - field-heading))) - (contents-begin (org-element-property :contents-begin field-heading)) - (contents-end (org-element-property :contents-end field-heading)) - (exporter (or (org-entry-get-with-inheritance anki-editor-prop-exporter) - anki-editor-exporter-default)) - (end-of-header (org-element-property :contents-begin field-heading)) - raw-content - content-elem) - (when (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. - (while (progn - (goto-char end-of-header) - (setq content-elem (org-element-context)) - (memq (car content-elem) '(drawer planning property-drawer))) - (setq end-of-header (org-element-property :end content-elem))) - (setq contents-begin (org-element-property :begin content-elem))) - (setq raw-content (or (and contents-begin - contents-end - (buffer-substring - contents-begin - ;; in case the buffer is narrowed, - ;; e.g. by `org-map-entries' when - ;; scope is `tree' - (min (point-max) contents-end))) - "")) - (push (cons field-name - (pcase exporter - ((pred (string= anki-editor-exporter-raw)) - raw-content) - ((pred (string= anki-editor-exporter-default)) - (or (org-export-string-as - raw-content - 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 - "")) - (_ (error "Invalid exporter: %s" exporter)))) - fields) - (org-forward-heading-same-level nil t)))) - (reverse fields)))) + (cl-loop with inhibit-message = t ; suppress echo message from `org-babel-exp-src-block' + initially (unless (org-goto-first-child) + (cl-return)) + for last-pt = (point) + 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 end = (org-element-property :contents-end element) + for raw = (or (and begin + end + (buffer-substring-no-properties + begin + ;; in case the buffer is narrowed, + ;; e.g. by `org-map-entries' when + ;; 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))) + collect (cons heading content) + ;; proceed to next field entry and check last-pt to + ;; see if it's already the last entry + do (org-forward-heading-same-level nil t) + until (= last-pt (point))))) ;;; Minor mode From 3fba9ea13b515c8a8738f4d1dfa43b548cab4033 Mon Sep 17 00:00:00 2001 From: louie Date: Sun, 23 Feb 2020 15:37:40 +0800 Subject: [PATCH 13/19] Fix byte-compilation warnings --- anki-editor.el | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/anki-editor.el b/anki-editor.el index 2e4c0e3..8fbc91c 100644 --- a/anki-editor.el +++ b/anki-editor.el @@ -71,11 +71,13 @@ (defcustom anki-editor-break-consecutive-braces-in-latex nil "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." + :type 'boolean) (defcustom anki-editor-org-tags-as-anki-tags t - "If nil, tags of entries won't be counted as Anki tags.") + "If nil, tags of entries won't be counted as Anki tags." + :type 'boolean) (defcustom anki-editor-protected-tags '("marked" "leech") @@ -92,11 +94,13 @@ form entries." (defcustom anki-editor-api-host "127.0.0.1" - "The network address AnkiConnect is listening.") + "The network address AnkiConnect is listening." + :type 'string) (defcustom anki-editor-api-port "8765" - "The port number AnkiConnect is listening.") + "The port number AnkiConnect is listening." + :type 'string) (defcustom anki-editor-latex-style 'builtin "The style of latex to translate into." @@ -110,7 +114,7 @@ form entries." (cl-defun anki-editor--fetch (url &rest settings - &key type data success error parser + &key type data success _error parser &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 @@ -145,7 +149,7 @@ more digging." (defun anki-editor-api-call (action &rest params) "Invoke AnkiConnect with ACTION and PARAMS." (let ((payload (list :action action :version anki-editor-api-version)) - (request-backend 'curl) + (_request-backend 'curl) (json-array-type 'list) reply err) @@ -189,7 +193,7 @@ of these calls in the same order." (vconcat --anki-editor-var-multi-actions--)))) (cl-loop for result in --anki-editor-var-multi-results-- - do (when-let ((_ (listp result)) + do (when-let ((pred (listp result)) (err (alist-get 'error result))) (error err)) collect result))) From 7dcae54caa5a3dbb864b3ea70a211f3fee16b5f4 Mon Sep 17 00:00:00 2001 From: louie Date: Mon, 24 Feb 2020 13:39:31 +0800 Subject: [PATCH 14/19] 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 From c47dfade3aec07e2bc701b56e66e880b8c2972e9 Mon Sep 17 00:00:00 2001 From: louie Date: Mon, 24 Feb 2020 17:16:20 +0800 Subject: [PATCH 15/19] Collect notes in one pass --- anki-editor.el | 75 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 49 insertions(+), 26 deletions(-) diff --git a/anki-editor.el b/anki-editor.el index b90aef2..3684873 100644 --- a/anki-editor.el +++ b/anki-editor.el @@ -713,6 +713,13 @@ Return a list of cons of (FIELD-NAME . FIELD-CONTENT)." ;;; Commands +(defvar anki-editor--note-markers nil) + +(defun anki-editor--collect-note-marker () + (message "Scanning notes %d (%s@%d), wait a moment..." + (length anki-editor--note-markers) (buffer-name) (point)) + (push (point-marker) anki-editor--note-markers)) + (defun anki-editor-push-notes (&optional scope match) "Build notes from headings that match MATCH within SCOPE and push them to Anki. @@ -741,32 +748,48 @@ of that heading." ((equal current-prefix-arg '(16)) 'file) ((equal current-prefix-arg '(64)) 'agenda) (t nil)))) - - (let ((total (progn - (message "Counting notes...") - (length (anki-editor-map-note-entries t match scope)))) - (acc 0) - (failed 0)) - - (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 - ((zerop total) "Nothing to push") - ((zerop failed) (format "Pushed %d notes to Anki successfully" acc)) - (t (format "Pushed %d notes in total, among which %d were failed. Check property drawers for failure reasons. -When the issues are resolved, you could repush the failed ones with `anki-editor-retry-failed-notes'." - acc failed)))))) + (unwind-protect + (progn + (anki-editor-map-note-entries #'anki-editor--collect-note-marker match scope) + (setq anki-editor--note-markers (reverse anki-editor--note-markers)) + (let ((count 0) + (failed 0)) + (save-excursion + (anki-editor--with-collection-data-updated + (cl-loop with bar-width = 30 + for marker in anki-editor--note-markers + for progress = (/ (float (cl-incf count)) (length anki-editor--note-markers)) + do + (goto-char marker) + (message "Uploading notes in buffer %s%s [%s%s] %d/%d (%.2f%%)" + (marker-buffer marker) + (if (zerop failed) + "" + (propertize (format " %d failed" failed) + 'face `(:foreground "red"))) + (make-string (truncate (* bar-width progress)) ?#) + (make-string (- bar-width (truncate (* bar-width progress))) ?.) + count + (length anki-editor--note-markers) + (* 100 progress)) + (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)))) + ;; free marker + (set-marker marker nil)))) + (message + (cond + ((zerop (length anki-editor--note-markers)) "Nothing to push") + ((zerop failed) (format "Successfully pushed %d notes to Anki" count)) + (t (format "Pushed %d notes to Anki, with %d failed. Check property drawers for details. +When you have fixed those issues, try re-push the failed ones with `anki-editor-retry-failed-notes'." + count failed)))))) + ;; clean up markers + (cl-loop for m in anki-editor--note-markers + do (set-marker m nil) + finally do (setq anki-editor--note-markers nil)))) (defun anki-editor-push-new-notes (&optional scope) "Push note entries without ANKI_NOTE_ID in SCOPE to Anki." From c76166e31836276bd7ce47eeff05a4b5d3d2ee9b Mon Sep 17 00:00:00 2001 From: louie Date: Mon, 24 Feb 2020 17:40:41 +0800 Subject: [PATCH 16/19] Capture connection error --- anki-editor.el | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/anki-editor.el b/anki-editor.el index 3684873..91fa3d2 100644 --- a/anki-editor.el +++ b/anki-editor.el @@ -878,7 +878,9 @@ When note heading is not provided, it is used as the first field." (defun anki-editor-api-check () "Check if correct version of AnkiConnect is serving." (interactive) - (let ((ver (anki-editor-api-call-result 'version))) + (let ((ver (condition-case err + (anki-editor-api-call-result 'version) + (error (error "Failed to connect to Anki: %s" (error-message-string err)))))) (if (<= anki-editor-api-version ver) (when (called-interactively-p 'interactive) (message "AnkiConnect v.%d is running" ver)) From 58cad3a28747ec358c58b91fcd0f088644f7e7ac Mon Sep 17 00:00:00 2001 From: louie Date: Mon, 2 Mar 2020 00:10:53 +0800 Subject: [PATCH 17/19] Add styling utilities --- Changelog.org | 7 ++++- README.org | 12 +++++++- anki-editor.el | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 2 deletions(-) diff --git a/Changelog.org b/Changelog.org index 587de8f..a316797 100644 --- a/Changelog.org +++ b/Changelog.org @@ -17,8 +17,13 @@ 6. Added more utility commands/functions 7. ~anki-editor-use-math-jax~ was replaced with ~anki-editor-latex-style~ - 8. Minor internal code refactoring + 8. Code refactoring 9. Allows to put one field in heading + 10. Added options to copy custom head tags to card stylings + 1. Variable ~anki-editor-include-default-style~ + 2. Variable ~anki-editor-html-head~ + 3. Command ~anki-editor-copy-styles~ + 4. Command ~anki-editor-remove-styles~ ** v0.3.3 diff --git a/README.org b/README.org index 0094c3c..eed286b 100644 --- a/README.org +++ b/README.org @@ -84,11 +84,13 @@ there are any ambiguity or grammatical mistakes ;-)/ |-----------------------------------------------+------------------------+----------------------------------------------------------------------------------------------------------| | anki-editor-api-host | "127.0.0.1" | The network address AnkiConnect is listening. | | anki-editor-api-port | "8765" | The port number AnkiConnect is listening. | - | anki-editor-break-consecutive-braces-in-latex | nil | If non-nil, consecutive `}' will be automatically separated by spaces to prevent early-closing of cloze. | + | anki-editor-break-consecutive-braces-in-latex | nil | If non-nil, consecutive "}" will be automatically separated by spaces to prevent early-closing of cloze. | | anki-editor-ignored-org-tags | '("export" "noexport") | A list of Org tags that are ignored when constructing notes form entries. | | anki-editor-org-tags-as-anki-tags | t | If nil, tags of entries wont't be counted as Anki tags. | | anki-editor-protected-tags | '("marked" "leech") | A list of tags that won't be deleted from Anki even though they're absent in Org entries. | | anki-editor-latex-style | builtin | The style of latex to translate into. | + | anki-editor-include-default-style | t | Wheter or not to include `org-html-style-default' when using `anki-editor-copy-styles'. | + | anki-editor-html-head | nil | Additional html tags to append to card stylings when using `anki-editor-copy-styles'. | ** Functions and Macros @@ -141,6 +143,14 @@ there are any ambiguity or grammatical mistakes ;-)/ Find notes with QUERY. +*** anki-editor-copy-styles + + Copy `org-html-style-default' and `anki-editor-html-head' to Anki card stylings. + +*** anki-editor-remove-styles + + Remove from card stylings html tags generated by this mode. + * Limitations ** Tags between Anki and Org diff --git a/anki-editor.el b/anki-editor.el index 91fa3d2..d2c7b4e 100644 --- a/anki-editor.el +++ b/anki-editor.el @@ -107,6 +107,17 @@ form entries." :type '(radio (const :tag "Built-in" builtin) (const :tag "MathJax" mathjax))) +(defcustom anki-editor-include-default-style t + "Wheter or not to include `org-html-style-default' when using `anki-editor-copy-styles'. +For example, you might want to turn this off when you are going to +provide your custom styles in `anki-editor-html-head'." + :type 'boolean) + +(defcustom anki-editor-html-head nil + "Additional html tags to append to card stylings when using `anki-editor-copy-styles'. +For example, you can put custom styles or scripts in this variable." + :type 'string) + ;;; AnkiConnect @@ -937,6 +948,70 @@ entry." (message "%S" nids) nids))) +(defvar anki-editor--style-start "\n") +(defvar anki-editor--style-end "\n