Initial commit
This commit is contained in:
commit
23d022a0a1
4 changed files with 313 additions and 0 deletions
23
README.org
Normal file
23
README.org
Normal file
|
@ -0,0 +1,23 @@
|
|||
anki-editor -- an Emacs package that helps you create Anki cards in Org-mode
|
||||
|
||||
* Requirements
|
||||
- [[https://github.com/FooSoft/anki-connect#installation][anki-connect]],
|
||||
an Anki add-on which is required for this package to interact with
|
||||
Anki.
|
||||
- curl
|
||||
|
||||
* Usage
|
||||
1. Download it and put it into your Emacs' =load-path=
|
||||
2. =(require 'anki-editor)=
|
||||
3. Write notes in org syntax, e.g. [[./examples.org][examples.org]]
|
||||
- Headings of deck are tagged with =deck=
|
||||
- Headings of note are tagged with =note=
|
||||
- Custom properties of a note heading can be used to specify note type and tags
|
||||
- Subheadings of a note heading are field names
|
||||
- The content of a field heading is the text of this field
|
||||
4. Have a look at the commands listed below, they might be helpful on note creation/editing
|
||||
- =anki-editor-submit= :: Send notes in current buffer to Anki
|
||||
- =anki-editor-setup-default-keybindings= :: Setup default keybindings (all keys are prefixed with =C-c a=)
|
||||
|
||||
** Demo
|
||||
[[./demo.gif]]
|
228
anki-editor.el
Normal file
228
anki-editor.el
Normal file
|
@ -0,0 +1,228 @@
|
|||
;;; anki-editor.el --- Create Anki cards in Org-mode -*- lexical-binding: t; -*-
|
||||
|
||||
;; Copyright (C) 2018 Louie Tan
|
||||
|
||||
;; Author: Louie Tan <louietanlei@gmail.com>
|
||||
|
||||
;; This file is not part of GNU Emacs.
|
||||
|
||||
;; This program is free software; you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
;;
|
||||
;; This program is distaributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
;;
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
(require 'json)
|
||||
(require 'org-element)
|
||||
|
||||
|
||||
(defconst anki-editor-note-tag "note")
|
||||
(defconst anki-editor-deck-tag "deck")
|
||||
(defconst anki-editor-note-type-prop :ANKI_NOTE_TYPE)
|
||||
(defconst anki-editor-note-tags-prop :ANKI_TAGS)
|
||||
(defconst anki-editor-anki-connect-listening-address "127.0.0.1")
|
||||
(defconst anki-editor-anki-connect-listening-port "8765")
|
||||
|
||||
;; Commands
|
||||
|
||||
(defun anki-editor-submit ()
|
||||
(interactive)
|
||||
(let* ((tree (org-element-parse-buffer))
|
||||
(note-headings (anki-editor--get-note-headings tree))
|
||||
(total (length note-headings)))
|
||||
|
||||
(if (null note-headings)
|
||||
(message "No notes found in current buffer")
|
||||
|
||||
(message "Submitting %d notes to Anki..." total)
|
||||
(anki-editor--anki-connect-invoke
|
||||
"addNotes" 5
|
||||
`(("notes" . ,(mapcar #'anki-editor--anki-connect-heading-to-note
|
||||
note-headings)))
|
||||
(lambda (result)
|
||||
(let ((failed (seq-count #'null result)))
|
||||
(message (format "Submitted %d notes, %d successful, %d failed." total (- total failed) failed))))))))
|
||||
|
||||
|
||||
(setq anki-editor--key-map `((,(kbd "C-c a s") . ,#'anki-editor-submit)))
|
||||
|
||||
(defun anki-editor-setup-default-keybindings ()
|
||||
(interactive)
|
||||
(dolist (map anki-editor--key-map)
|
||||
(local-set-key (car map) (cdr map)))
|
||||
(message "anki-editor default keybindings have been set"))
|
||||
|
||||
|
||||
;; Core Functions
|
||||
|
||||
(defun anki-editor--get-note-headings (data &optional test)
|
||||
(unless test (setq test 'identity))
|
||||
(org-element-map data 'headline
|
||||
(lambda (element)
|
||||
(let ((tags (org-element-property :tags element)))
|
||||
(when (and (member anki-editor-note-tag tags) (funcall test element))
|
||||
element)))))
|
||||
|
||||
(defun anki-editor--heading-to-note (heading)
|
||||
(let (deck note-type tags fields)
|
||||
(setq deck (anki-editor--get-deck-name heading)
|
||||
note-type (org-element-property anki-editor-note-type-prop heading)
|
||||
tags (org-element-property anki-editor-note-tags-prop heading)
|
||||
fields (mapcar #'anki-editor--heading-to-note-field (anki-editor--get-subheadings heading)))
|
||||
|
||||
(unless deck (error "Please specify a deck !"))
|
||||
(unless note-type (error "Please specify a note type !"))
|
||||
(unless fields (error "Please specify fields !"))
|
||||
|
||||
`((deck . ,deck)
|
||||
(note-type . ,note-type)
|
||||
(tags . ,(and tags (split-string tags " ")))
|
||||
(fields . ,fields))))
|
||||
|
||||
(defun anki-editor--get-deck-name (element)
|
||||
(let ((ancestor (anki-editor--find-ancestor
|
||||
element
|
||||
(lambda (it)
|
||||
(member anki-editor-deck-tag (org-element-property :tags it))))))
|
||||
(and ancestor
|
||||
(substring-no-properties (org-element-property :raw-value ancestor)))))
|
||||
|
||||
(defun anki-editor--get-subheadings (heading)
|
||||
(org-element-map (org-element-contents heading)
|
||||
'headline 'identity nil nil 'headline))
|
||||
|
||||
(defun anki-editor--heading-to-note-field (heading)
|
||||
(let ((field-name (substring-no-properties
|
||||
(org-element-property
|
||||
:raw-value
|
||||
heading)))
|
||||
(contents (org-element-contents heading)))
|
||||
`(,field-name . ,(anki-editor--generate-html
|
||||
(org-element-interpret-data contents)))))
|
||||
|
||||
(defun anki-editor--generate-html (org-content)
|
||||
(with-temp-buffer
|
||||
(insert org-content)
|
||||
(setq anki-editor--replacement-records nil)
|
||||
(anki-editor--replace-latex)
|
||||
(anki-editor--buffer-to-html)
|
||||
(anki-editor--translate-latex)
|
||||
(buffer-substring-no-properties (point-min) (point-max))))
|
||||
|
||||
;; Transformers
|
||||
|
||||
(defun anki-editor--buffer-to-html ()
|
||||
(when (> (buffer-size) 0)
|
||||
(save-mark-and-excursion
|
||||
(mark-whole-buffer)
|
||||
(org-html-convert-region-to-html))))
|
||||
|
||||
(defun anki-editor--replace-latex ()
|
||||
(let (object)
|
||||
(while (setq object (org-element-map
|
||||
(org-element-parse-buffer)
|
||||
'latex-fragment 'identity nil t))
|
||||
(let (begin end latex hash)
|
||||
(setq begin (org-element-property :begin object)
|
||||
end (- (org-element-property :end object) (org-element-property :post-blank object))
|
||||
latex (delete-and-extract-region begin end))
|
||||
(goto-char begin)
|
||||
(insert (setq hash (anki-editor--hash 'latex-fragment latex)))
|
||||
(add-to-list 'anki-editor--replacement-records
|
||||
`(,hash . ((type . latex-fragment)
|
||||
(original . ,latex))))))))
|
||||
|
||||
(setq anki-editor--anki-latex-syntax-map
|
||||
`((,(format "^%s" (regexp-quote "$$")) . "[$$]")
|
||||
(,(format "%s$" (regexp-quote "$$")) . "[/$$]")
|
||||
(,(format "^%s" (regexp-quote "$")) . "[$]")
|
||||
(,(format "%s$" (regexp-quote "$")) . "[/$]")
|
||||
(,(format "^%s" (regexp-quote "\\(")) . "[$]")
|
||||
(,(format "%s$" (regexp-quote "\\)")) . "[/$]")
|
||||
(,(format "^%s" (regexp-quote "\\[")) . "[$$]")
|
||||
(,(format "%s$" (regexp-quote "\\]")) . "[/$$]")))
|
||||
|
||||
(defun anki-editor--translate-latex-to-anki-syntax (latex)
|
||||
(dolist (map anki-editor--anki-latex-syntax-map)
|
||||
(setq latex (replace-regexp-in-string (car map) (cdr map) latex t t)))
|
||||
latex)
|
||||
|
||||
(defun anki-editor--translate-latex ()
|
||||
(dolist (stash anki-editor--replacement-records)
|
||||
(goto-char (point-min))
|
||||
(let ((hash (car stash))
|
||||
(value (cdr stash)))
|
||||
(when (eq 'latex-fragment (alist-get 'type value))
|
||||
(when (search-forward hash nil t)
|
||||
(replace-match (anki-editor--translate-latex-to-anki-syntax
|
||||
(alist-get 'original value))
|
||||
t t))))))
|
||||
|
||||
;; Utilities
|
||||
|
||||
(defun anki-editor--hash (type text)
|
||||
(format "%s-%s" (symbol-name type) (sha1 text)))
|
||||
|
||||
(defun anki-editor--find-ancestor (element test)
|
||||
(let ((parent (org-element-property :parent element)))
|
||||
(and parent
|
||||
(if (funcall test parent)
|
||||
parent
|
||||
(anki-editor--find-ancestor parent test)))))
|
||||
|
||||
;; anki-connect
|
||||
|
||||
(defun anki-editor--anki-connect-invoke (action version &optional params success)
|
||||
(let* ((data `(("action" . ,action)
|
||||
("version" . ,version)))
|
||||
(request-body (json-encode
|
||||
(if params
|
||||
(add-to-list 'data `("params" . ,params))
|
||||
data)))
|
||||
(request-tempfile (make-temp-file "emacs-anki-editor")))
|
||||
|
||||
(with-temp-file request-tempfile
|
||||
(setq buffer-file-coding-system 'utf-8)
|
||||
(set-buffer-multibyte t)
|
||||
(insert request-body))
|
||||
|
||||
(let* ((response (shell-command-to-string
|
||||
(format "curl %s:%s --silent -X POST --data-binary @%s"
|
||||
anki-editor-anki-connect-listening-address
|
||||
anki-editor-anki-connect-listening-port
|
||||
request-tempfile)))
|
||||
anki-error)
|
||||
(when (file-exists-p request-tempfile) (delete-file request-tempfile))
|
||||
(condition-case err
|
||||
(progn
|
||||
(setq response (json-read-from-string response)
|
||||
anki-error (alist-get 'error response))
|
||||
(when anki-error (error "anki-connect responded with error: %s" anki-error))
|
||||
(when success (funcall success (alist-get 'result response))))
|
||||
(error (message "%s" (error-message-string err)))))))
|
||||
|
||||
(defun anki-editor--anki-connect-map-note (note)
|
||||
`(("deckName" . ,(alist-get 'deck note))
|
||||
("modelName" . ,(alist-get 'note-type note))
|
||||
("fields" . ,(alist-get 'fields note))
|
||||
;; Convert tags to a vector since empty list is identical to nil
|
||||
;; which will become None in Python, but anki-connect requires it
|
||||
;; to be type of list.
|
||||
("tags" . ,(vconcat (alist-get 'tags note)))))
|
||||
|
||||
(defun anki-editor--anki-connect-heading-to-note (heading)
|
||||
(anki-editor--anki-connect-map-note
|
||||
(anki-editor--heading-to-note heading)))
|
||||
|
||||
|
||||
(provide 'anki-editor)
|
||||
|
||||
;;; anki-editor.el ends here
|
BIN
demo.gif
Normal file
BIN
demo.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 MiB |
62
examples.org
Normal file
62
examples.org
Normal file
|
@ -0,0 +1,62 @@
|
|||
* English :deck:
|
||||
** Vocabulary
|
||||
*** Item :note:
|
||||
:PROPERTIES:
|
||||
:ANKI_NOTE_TYPE: Basic (and reversed card)
|
||||
:ANKI_TAGS: vocab idioms
|
||||
:END:
|
||||
**** Front
|
||||
(it's) raining cats and dogs
|
||||
**** Back
|
||||
it's raining very hard
|
||||
** Grammar
|
||||
*** Item :note:
|
||||
:PROPERTIES:
|
||||
:ANKI_NOTE_TYPE: Basic
|
||||
:ANKI_TAGS: grammar
|
||||
:END:
|
||||
**** Front
|
||||
列举最基本的句型
|
||||
**** Back
|
||||
#+BEGIN_EXPORT html
|
||||
<div align="left">
|
||||
#+END_EXPORT
|
||||
- S + V
|
||||
- S + V + O
|
||||
- S + V + C
|
||||
- S + V + O + O
|
||||
- S + V + O + C
|
||||
#+BEGIN_EXPORT html
|
||||
</div>
|
||||
#+END_EXPORT
|
||||
* Math :deck:
|
||||
** Fact :note:
|
||||
:PROPERTIES:
|
||||
:ANKI_NOTE_TYPE: Cloze
|
||||
:END:
|
||||
*** Text
|
||||
The square function is {{c1::$f(x) = x^2$}}
|
||||
* Computer Science :deck:
|
||||
** Item :note:
|
||||
:PROPERTIES:
|
||||
:ANKI_NOTE_TYPE: Basic
|
||||
:ANKI_TAGS: lisp emacs programming
|
||||
:END:
|
||||
*** Front
|
||||
How to trap errors in elisp ?
|
||||
*** Back
|
||||
#+BEGIN_EXPORT html
|
||||
<div align="left">
|
||||
#+END_EXPORT
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(condition-case the-error
|
||||
;; protected form
|
||||
(progn
|
||||
(do-something-dangerous))
|
||||
;; error handlers
|
||||
(error-symbol-1 (handler1 err))
|
||||
((error-symbol-2 error-symbol-3) (handler2 err)))
|
||||
#+END_SRC
|
||||
#+BEGIN_EXPORT html
|
||||
</div>
|
||||
#+END_EXPORT
|
Loading…
Reference in a new issue