commit 23d022a0a16a127842f37c2c0a823c969a9cec9e Author: louietan Date: Thu Dec 28 00:20:01 2017 +0800 Initial commit diff --git a/README.org b/README.org new file mode 100644 index 0000000..29a14d1 --- /dev/null +++ b/README.org @@ -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]] diff --git a/anki-editor.el b/anki-editor.el new file mode 100644 index 0000000..e6310df --- /dev/null +++ b/anki-editor.el @@ -0,0 +1,228 @@ +;;; anki-editor.el --- Create Anki cards in Org-mode -*- lexical-binding: t; -*- + +;; Copyright (C) 2018 Louie Tan + +;; Author: Louie Tan + +;; 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 . + + +(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 diff --git a/demo.gif b/demo.gif new file mode 100644 index 0000000..d70cb29 Binary files /dev/null and b/demo.gif differ diff --git a/examples.org b/examples.org new file mode 100644 index 0000000..d831a0e --- /dev/null +++ b/examples.org @@ -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 +
+#+END_EXPORT +- S + V +- S + V + O +- S + V + C +- S + V + O + O +- S + V + O + C +#+BEGIN_EXPORT html +
+#+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 +
+#+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 +
+#+END_EXPORT