Merge branch 'develop'

This commit is contained in:
louie 2018-02-04 12:05:47 +08:00
commit fd4085666b
2 changed files with 103 additions and 143 deletions

View file

@ -54,10 +54,13 @@ anki-editor -- Make Anki Cards in Org-mode
| =anki-editor-submit= | Send notes in current buffer to Anki. | | =anki-editor-submit= | Send notes in current buffer to Anki. |
| =anki-editor-insert-deck= | Insert a deck heading. | | =anki-editor-insert-deck= | Insert a deck heading. |
| =anki-editor-insert-note= | Insert the skeleton of a note. | | =anki-editor-insert-note= | Insert the skeleton of a note. |
| =anki-editor-insert-tags= | Insert a tag at point with autocompletion. | | =anki-editor-insert-tags= | Insert tags at point with autocompletion. |
| =anki-editor-cloze-region= | Cloze region. |
| =anki-editor-export-heading-contents-to-html= | Export the contents of the heading at point to HTML. | | =anki-editor-export-heading-contents-to-html= | Export the contents of the heading at point to HTML. |
| =anki-editor-convert-region-to-html= | Convert and replace region to HTML. | | =anki-editor-convert-region-to-html= | Convert and replace region to HTML. |
Note that =anki-editor-submit= will fail if the deck does not exist
yet. New decks must be created in Anki first.
*Since I'm not a native English speaker, let me know if there's any ambiguity or grammatical mistakes.* *Since I'm not a native English speaker, let me know if there's any ambiguity or grammatical mistakes.*
@ -67,6 +70,12 @@ anki-editor -- Make Anki Cards in Org-mode
* Change Log * Change Log
*0.1.2* *v0.2.0*
- Make deck/note insertion commands smarter on choosing insertion point - Fix =org-element= not functioning correctly in temp buffer.
- Bug fixes - Add a command to cloze region.
- Refactor the code to do the translation with Org's exporting framework.
- Add a customization variable to break consecutive braces in latex.
*v0.1.2*
- Make deck/note insertion commands smarter on choosing insertion point.
- Fix latex environments being joined with the elements following it.

View file

@ -5,7 +5,7 @@
;; Filename: anki-editor.el ;; Filename: anki-editor.el
;; Description: Make Anki Cards in Org-mode ;; Description: Make Anki Cards in Org-mode
;; Author: Louie Tan ;; Author: Louie Tan
;; Version: 0.1.2 ;; Version: 0.2.0
;; Package-Requires: ((emacs "25")) ;; Package-Requires: ((emacs "25"))
;; URL: https://github.com/louietan/anki-editor ;; URL: https://github.com/louietan/anki-editor
;; ;;
@ -61,13 +61,13 @@
(require 'json) (require 'json)
(require 'org-element) (require 'org-element)
(require 'ox)
(defconst anki-editor-prop-note-type :ANKI_NOTE_TYPE) (defconst anki-editor-prop-note-type :ANKI_NOTE_TYPE)
(defconst anki-editor-prop-note-tags :ANKI_TAGS) (defconst anki-editor-prop-note-tags :ANKI_TAGS)
(defconst anki-editor-prop-note-id :ANKI_NOTE_ID) (defconst anki-editor-prop-note-id :ANKI_NOTE_ID)
(defconst anki-editor-prop-failure-reason :ANKI_FAILURE_REASON) (defconst anki-editor-prop-failure-reason :ANKI_FAILURE_REASON)
(defconst anki-editor-buffer-html-output "*anki-editor HTML Output*") (defconst anki-editor-buffer-html-output "*AnkiEditor HTML Output*")
(defgroup anki-editor nil (defgroup anki-editor nil
"Customizations for anki-editor." "Customizations for anki-editor."
@ -81,19 +81,24 @@
"deck" "deck"
"Headings with this tag will be considered as decks.") "Headings with this tag will be considered as decks.")
(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.")
(defcustom anki-editor-anki-connect-listening-address (defcustom anki-editor-anki-connect-listening-address
"127.0.0.1" "127.0.0.1"
"The network address anki-connect is listening.") "The network address AnkiConnect is listening.")
(defcustom anki-editor-anki-connect-listening-port (defcustom anki-editor-anki-connect-listening-port
"8765" "8765"
"The port number anki-connect is listening.") "The port number AnkiConnect is listening.")
;;; anki-connect ;;; AnkiConnect
(defun anki-editor--anki-connect-invoke (action version &optional params) (defun anki-editor--anki-connect-invoke (action version &optional params)
"Invoke anki-connect with ACTION, VERSION and PARAMS." "Invoke AnkiConnect with ACTION, VERSION and PARAMS."
(let* ((data `(("action" . ,action) (let* ((data `(("action" . ,action)
("version" . ,version))) ("version" . ,version)))
(request-body (json-encode (request-body (json-encode
@ -113,21 +118,19 @@
(shell-quote-argument anki-editor-anki-connect-listening-port) (shell-quote-argument anki-editor-anki-connect-listening-port)
(shell-quote-argument request-tempfile)))) (shell-quote-argument request-tempfile))))
resp error) resp error)
(when (file-exists-p request-tempfile) (delete-file request-tempfile))
(condition-case err (condition-case err
(let ((json-array-type 'list)) (let ((json-array-type 'list))
(setq resp (json-read-from-string raw-resp) (setq resp (json-read-from-string raw-resp)
error (alist-get 'error resp))) error (alist-get 'error resp)))
(error (setq error (error (setq error
(format "Unexpected error communicating with anki-connect: %s, the response was %s" (format "Unexpected error communicating with AnkiConnect: %s, the response was %s"
(error-message-string err) (error-message-string err)
(prin1-to-string raw-resp))))) (prin1-to-string raw-resp)))))
`((result . ,(alist-get 'result resp)) `((result . ,(alist-get 'result resp))
(error . ,error))))) (error . ,error)))))
(defmacro anki-editor--anki-connect-invoke-result (&rest args) (defmacro anki-editor--anki-connect-invoke-result (&rest args)
"Invoke anki-connect 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* ((resp (anki-editor--anki-connect-invoke ,@args)) `(let* ((resp (anki-editor--anki-connect-invoke ,@args))
(rslt (alist-get 'result resp)) (rslt (alist-get 'result resp))
(err (alist-get 'error resp))) (err (alist-get 'error resp)))
@ -135,19 +138,19 @@
rslt)) rslt))
(defun anki-editor--anki-connect-map-note (note) (defun anki-editor--anki-connect-map-note (note)
"Convert NOTE to the form that anki-connect accepts." "Convert NOTE to the form that AnkiConnect accepts."
(let-alist note (let-alist note
(list (cons "id" .note-id) (list (cons "id" .note-id)
(cons "deckName" .deck) (cons "deckName" .deck)
(cons "modelName" .note-type) (cons "modelName" .note-type)
(cons "fields" .fields) (cons "fields" .fields)
;; Convert tags to a vector since empty list is identical to nil ;; Convert tags to a vector since empty list is identical to nil
;; which will become None in Python, but anki-connect requires it ;; which will become None in Python, but AnkiConnect requires it
;; to be type of list. ;; to be type of list.
(cons "tags" (vconcat .tags))))) (cons "tags" (vconcat .tags)))))
(defun anki-editor--anki-connect-heading-to-note (heading) (defun anki-editor--anki-connect-heading-to-note (heading)
"Convert HEADING to a note in the form that anki-connect accepts." "Convert HEADING to a note in the form that AnkiConnect accepts."
(anki-editor--anki-connect-map-note (anki-editor--anki-connect-map-note
(anki-editor--heading-to-note heading))) (anki-editor--heading-to-note heading)))
@ -183,6 +186,7 @@ of that heading."
(setq failed (1+ failed)) (setq failed (1+ failed))
(anki-editor--set-failure-reason (error-message-string err)))))))))) (anki-editor--set-failure-reason (error-message-string err))))))))))
(mapconcat 'identity `(,anki-editor-deck-tag ,anki-editor-note-tag) "|")) (mapconcat 'identity `(,anki-editor-deck-tag ,anki-editor-note-tag) "|"))
(message (with-output-to-string (message (with-output-to-string
(princ (format "Submitted %d notes, with %d failed." total failed)) (princ (format "Submitted %d notes, with %d failed." total failed))
(when (> failed 0) (when (> failed 0)
@ -200,7 +204,7 @@ With PREFIX, only insert the deck name at point."
(if prefix (if prefix
(insert deckname) (insert deckname)
(let (inserted) (let (inserted)
(anki-editor--visit-superior-headings (anki-editor--visit-ancestor-headings
(lambda () (lambda ()
(when (member anki-editor-deck-tag (org-get-tags)) (when (member anki-editor-deck-tag (org-get-tags))
(anki-editor--insert-deck-heading deckname) (anki-editor--insert-deck-heading deckname)
@ -229,7 +233,7 @@ of this heading.
Or when the point is inside a deck heading, the behavior is the Or when the point is inside a deck heading, the behavior is the
same as above. same as above.
Otherwise, it's inserted at point." Otherwise, it's inserted below current heading at point."
(interactive) (interactive)
(message "Fetching note types...") (message "Fetching note types...")
(let ((note-types (sort (anki-editor--anki-connect-invoke-result "modelNames" 5) #'string-lessp)) (let ((note-types (sort (anki-editor--anki-connect-invoke-result "modelNames" 5) #'string-lessp))
@ -243,7 +247,7 @@ Otherwise, it's inserted at point."
(let ((cur-point (point)) (let ((cur-point (point))
pt-of-grp pt-of-grp
inserted) inserted)
(anki-editor--visit-superior-headings (anki-editor--visit-ancestor-headings
(lambda () (lambda ()
(let ((tags (org-get-tags))) (let ((tags (org-get-tags)))
(cond (cond
@ -274,51 +278,64 @@ Otherwise, it's inserted at point."
;;;###autoload ;;;###autoload
(defun anki-editor-insert-tags () (defun anki-editor-insert-tags ()
"Insert a tag at point with autocompletion." "Insert tags at point with autocompletion."
(interactive) (interactive)
(let ((tags (sort (anki-editor--anki-connect-invoke-result "getTags" 5) #'string-lessp))) (let ((tags (sort (anki-editor--anki-connect-invoke-result "getTags" 5) #'string-lessp)))
(while t (insert (format " %s" (completing-read "Choose a tag: " tags)))))) (while t (insert (format " %s" (completing-read "Choose a tag: " tags))))))
;;;###autoload
(defun anki-editor-cloze-region (&optional arg)
"Cloze region with number ARG."
(interactive "p")
(unless (region-active-p) (error "No active region"))
(let ((region (buffer-substring (region-beginning) (region-end)))
(hint (read-from-minibuffer "Hint (optional): ")))
(save-excursion
(delete-region (region-beginning) (region-end))
(insert (with-output-to-string
(princ (format "{{c%d::%s" (or arg 1) region))
(unless (string-empty-p (string-trim hint)) (princ (format "::%s" hint)))
(princ "}}"))))))
;;;###autoload ;;;###autoload
(defun anki-editor-export-heading-contents-to-html () (defun anki-editor-export-heading-contents-to-html ()
"Export the contents of the heading at point to HTML." "Export the contents of the heading at point to HTML."
(interactive) (interactive)
(let ((tree (org-element-at-point)) (let* ((tree (org-element-at-point))
contents) (contents (or (and (org-element-property :contents-begin tree)
(org-element-property :contents-end tree)
(buffer-substring (org-element-property :contents-begin tree)
(org-element-property :contents-end tree)))
"")))
(if (or (null tree) (if (or (null tree)
(not (eq (org-element-type tree) 'headline))) (not (eq (org-element-type tree) 'headline)))
(error "No element at point or it's not a heading") (error "No element at point or it's not a heading")
(setq contents (buffer-substring-no-properties (org-element-property :contents-begin tree)
(org-element-property :contents-end tree)))
(when (buffer-live-p (get-buffer anki-editor-buffer-html-output)) (when (buffer-live-p (get-buffer anki-editor-buffer-html-output))
(kill-buffer anki-editor-buffer-html-output)) (kill-buffer anki-editor-buffer-html-output))
(switch-to-buffer-other-window (get-buffer-create anki-editor-buffer-html-output)) (switch-to-buffer-other-window (get-buffer-create anki-editor-buffer-html-output))
(insert (anki-editor--generate-html contents))))) (insert (org-export-string-as contents anki-editor--ox-anki-html-backend t)))))
;;;###autoload ;;;###autoload
(defun anki-editor-convert-region-to-html () (defun anki-editor-convert-region-to-html ()
"Convert and replace region to HTML." "Convert and replace region to HTML."
(interactive) (interactive)
(unless (region-active-p) (error "No active region")) (org-export-replace-region-by anki-editor--ox-anki-html-backend))
(insert (anki-editor--generate-html
(delete-and-extract-region (region-beginning) (region-end)))))
;;;###autoload ;;;###autoload
(defun anki-editor-anki-connect-upgrade () (defun anki-editor-anki-connect-upgrade ()
"Upgrade anki-connect to the latest version. "Upgrade AnkiConnect to the latest version.
This will display a confirmation dialog box in Anki asking if you This will display a confirmation dialog box in Anki asking if you
want to continue. The upgrading is done by downloading the latest want to continue. The upgrading is done by downloading the latest
code in the master branch of its Github repo. code in the master branch of its Github repo.
This is useful when new version of this package depends on the This is useful when new version of this package depends on the
bugfixes or new features of anki-connect." bugfixes or new features of AnkiConnect."
(interactive) (interactive)
(when (yes-or-no-p "NOTE: This will download the latest codebase of anki-connect 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 anki-connect that are not released yet. Do you still want to continue?") (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?")
(let ((result (anki-editor--anki-connect-invoke-result "upgrade" 5))) (let ((result (anki-editor--anki-connect-invoke-result "upgrade" 5)))
(when (and (booleanp result) result) (when (and (booleanp result) result)
(message "anki-connect 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 to make it in effect.")))))
;;; Core Functions ;;; Core Functions
@ -339,6 +356,7 @@ DECK is used when the action is note creation."
(org-element-property :begin note-elem) (org-element-property :begin note-elem)
(org-element-property :end note-elem)))) (org-element-property :end note-elem))))
(with-temp-buffer (with-temp-buffer
(org-mode)
(insert content) (insert content)
(car (org-element-contents (org-element-parse-buffer))))) (car (org-element-contents (org-element-parse-buffer)))))
note (anki-editor--heading-to-note note-elem)) note (anki-editor--heading-to-note note-elem))
@ -357,35 +375,28 @@ If DEMOTE is t, demote the inserted note heading."
(save-excursion (save-excursion
(org-insert-heading-respect-content) (org-insert-heading-respect-content)
(org-do-demote) (org-do-demote)
(insert field))) (insert field))))
;; TODO: Is it a good idea to automatically move to the first field
;; heading and open a new line ?
;; (org-next-visible-heading 1)
;; (end-of-line)
;; (newline-and-indent)
)
(defun anki-editor--save-note (note) (defun anki-editor--save-note (note)
"Request anki-connect for updating or creating NOTE." "Request AnkiConnect for updating or creating NOTE."
(if (= (alist-get 'note-id note) -1) (if (= (alist-get 'note-id note) -1)
(anki-editor--create-note note) (anki-editor--create-note note)
(anki-editor--update-note note))) (anki-editor--update-note note)))
(defun anki-editor--create-note (note) (defun anki-editor--create-note (note)
"Request anki-connect for creating NOTE." "Request AnkiConnect for creating NOTE."
(let* ((response (anki-editor--anki-connect-invoke (let* ((response (anki-editor--anki-connect-invoke
"addNote" 5 `((note . ,(anki-editor--anki-connect-map-note note))))) "addNote" 5 `((note . ,(anki-editor--anki-connect-map-note note)))))
(result (alist-get 'result response)) (result (alist-get 'result response))
(err (alist-get 'error response))) (err (alist-get 'error response)))
(if result (if result
;; put ID of newly created note in property drawer
(org-set-property (substring (symbol-name anki-editor-prop-note-id) 1) (org-set-property (substring (symbol-name anki-editor-prop-note-id) 1)
(format "%d" (alist-get 'result response))) (format "%d" (alist-get 'result response)))
(error (or err "Sorry, the operation was unsuccessful and detailed information is unavailable."))))) (error (or err "Sorry, the operation was unsuccessful and detailed information is unavailable.")))))
(defun anki-editor--update-note (note) (defun anki-editor--update-note (note)
"Request anki-connect for updating fields and tags of NOTE." "Request AnkiConnect for updating fields and tags of NOTE."
(anki-editor--anki-connect-invoke-result (anki-editor--anki-connect-invoke-result
"updateNoteFields" 5 `((note . ,(anki-editor--anki-connect-map-note note)))) "updateNoteFields" 5 `((note . ,(anki-editor--anki-connect-map-note note))))
@ -407,11 +418,11 @@ If DEMOTE is t, demote the inserted note heading."
(defun anki-editor--set-failure-reason (reason) (defun anki-editor--set-failure-reason (reason)
"Set failure reason to REASON in property drawer at point." "Set failure reason to REASON in property drawer at point."
(org-set-property (substring (symbol-name anki-editor-prop-failure-reason) 1) reason)) (org-entry-put nil (substring (symbol-name anki-editor-prop-failure-reason) 1) reason))
(defun anki-editor--clear-failure-reason () (defun anki-editor--clear-failure-reason ()
"Clear failure reason in property drawer at point." "Clear failure reason in property drawer at point."
(org-delete-property (substring (symbol-name anki-editor-prop-failure-reason) 1))) (org-entry-delete nil (substring (symbol-name anki-editor-prop-failure-reason) 1)))
(defun anki-editor--heading-to-note (heading) (defun anki-editor--heading-to-note (heading)
"Construct an alist representing a note for HEADING." "Construct an alist representing a note for HEADING."
@ -429,11 +440,6 @@ If DEMOTE is t, demote the inserted note heading."
(tags . ,(and tags (split-string tags " "))) (tags . ,(and tags (split-string tags " ")))
(fields . ,fields)))) (fields . ,fields))))
(defun anki-editor--get-subheadings (heading)
"Get all the subheadings of HEADING."
(org-element-map (org-element-contents heading)
'headline 'identity nil nil 'headline))
(defun anki-editor--heading-to-note-field (heading) (defun anki-editor--heading-to-note-field (heading)
"Convert HEADING to field data, a cons cell, the car of which is the field name, the cdr of which is contens represented in HTML." "Convert HEADING to field data, a cons cell, the car of which is the field name, the cdr of which is contens represented in HTML."
(let ((field-name (substring-no-properties (let ((field-name (substring-no-properties
@ -441,46 +447,19 @@ If DEMOTE is t, demote the inserted note heading."
:raw-value :raw-value
heading))) heading)))
(contents (org-element-contents heading))) (contents (org-element-contents heading)))
`(,field-name . ,(anki-editor--generate-html `(,field-name . ,(org-export-string-as
(org-element-interpret-data contents))))) (org-element-interpret-data contents)
anki-editor--ox-anki-html-backend t))))
(defun anki-editor--generate-html (contents) ;;; Org Export Backend
"Convert CONTENTS to HTML."
(with-temp-buffer
(org-mode)
(insert contents)
(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 (defconst anki-editor--ox-anki-html-backend
(org-export-create-backend
:parent 'html
:transcoders '((latex-fragment . anki-editor--ox-latex)
(latex-environment . anki-editor--ox-latex))))
(defun anki-editor--buffer-to-html () (defconst anki-editor--anki-latex-syntax-map
"Transform contents of buffer to HTML."
(when (> (buffer-size) 0)
(insert
(org-export-string-as
(delete-and-extract-region (point-min) (point-max)) 'html t))))
(defun anki-editor--replace-latex ()
"Replace latex objects with the hash of it's content."
(let (object type memo)
(while (setq object (org-element-map
(org-element-parse-buffer)
'(latex-fragment latex-environment) 'identity nil t))
(setq type (org-element-type object)
memo (anki-editor--replace-node object
(lambda (original)
(anki-editor--hash type
original))))
(push `(,(cdr memo) . ((type . ,type)
(original . ,(car memo))))
anki-editor--replacement-records))))
(defvar 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 "$")) . "[$]")
@ -494,68 +473,40 @@ If DEMOTE is t, demote the inserted note heading."
"Wrap CONTENT with Anki-style latex markers." "Wrap CONTENT with Anki-style latex markers."
(format "[latex]%s[/latex]" content)) (format "[latex]%s[/latex]" content))
(defun anki-editor--convert-latex-fragment (frag) (defun anki-editor--ox-latex (latex contents info)
"Convert latex fragment FRAG to Anki-style." "Transcode LATEX from Org to HTML.
(let ((copy frag)) CONTENTS is nil. INFO is a plist holding contextual information."
(let* ((code (org-element-property :value latex))
(copy code))
;; translate delimiters
(dolist (map anki-editor--anki-latex-syntax-map) (dolist (map anki-editor--anki-latex-syntax-map)
(setq frag (replace-regexp-in-string (car map) (cdr map) frag t t))) (setq code (replace-regexp-in-string (car map) (cdr map) code t t)))
(if (equal copy frag)
(anki-editor--wrap-latex frag)
frag)))
(defun anki-editor--translate-latex () (when (equal copy code)
"Transform latex objects that were previously replaced with hashes to Anki-style." (setq code (anki-editor--wrap-latex
(let (ele-data translated) (if (eq (org-element-type latex) 'latex-fragment)
(dolist (record anki-editor--replacement-records) code
(setq ele-data (cdr record)) (format "\n<pre>\n%s</pre>\n"
(goto-char (point-min)) (org-remove-indentation code))))))
(when (search-forward (car record) nil t)
(pcase (alist-get 'type ele-data) (if anki-editor-break-consecutive-braces-in-latex
('latex-fragment (replace-match (anki-editor--convert-latex-fragment (alist-get 'original ele-data)) t t)) (replace-regexp-in-string "}}" "} } " code)
('latex-environment (replace-match (anki-editor--wrap-latex (alist-get 'original ele-data)) t t))) code)))
(push record translated)))
(setq anki-editor--replacement-records (cl-set-difference anki-editor--replacement-records translated))))
;;; Utilities ;;; Utilities
(defun anki-editor--hash (type text)
"Compute hash of object, whose type and contens is TYPE and TEXT respectively."
(sha1 (format "%s %s" (symbol-name type) text)))
(defun anki-editor--set-tags-fix (tags) (defun anki-editor--set-tags-fix (tags)
"Set tags to TAGS and fix tags on the fly." "Set tags to TAGS and fix tags on the fly."
(org-set-tags-to tags) (org-set-tags-to tags)
(org-fix-tags-on-the-fly)) (org-fix-tags-on-the-fly))
(defun anki-editor--effective-end (node) (defun anki-editor--get-subheadings (heading)
"Get the effective end of NODE. "Get all the subheadings of HEADING."
(org-element-map (org-element-contents heading)
'headline 'identity nil nil 'headline))
org-element considers whitespaces or newlines after an element or (defun anki-editor--visit-ancestor-headings (visitor &optional level)
object still belong to it, which is to say :end property of an "Move point to and call VISITOR at each ancestor heading from point.
element matches :begin property of the following one at the same
level, if any. This will make it unable to separate elements with
their following ones after replacing. This function 'fixes' this
by resetting the end to the point after the last character that's
not blank. I'm not sure if this works for all cases though :)"
(let ((end (org-element-property :end node)))
(while (and (>= end (point-min))
;; check if character before END is blank
(string-match-p "[[:blank:]\r\n]" (buffer-substring (1- end) end)))
(setq end (1- end)))
end))
(defun anki-editor--replace-node (node replacer)
"Replace contents of NODE with the result from applying REPLACER to the contents of NODE."
(let* ((begin (org-element-property :begin node))
(end (anki-editor--effective-end node))
(original (delete-and-extract-region begin end))
(replacement (funcall replacer original)))
(goto-char begin)
(insert replacement)
(cons original replacement)))
(defun anki-editor--visit-superior-headings (visitor &optional level)
"Move point to and call VISITOR at each superior heading from point.
Don't pass LEVEL, it's only used in recursion. Don't pass LEVEL, it's only used in recursion.
Stops when VISITOR returns t or point reaches the beginning of buffer." Stops when VISITOR returns t or point reaches the beginning of buffer."
(let (stop) (let (stop)
@ -566,7 +517,7 @@ Stops when VISITOR returns t or point reaches the beginning of buffer."
stop (funcall visitor))))) stop (funcall visitor)))))
(when (and (not stop) (/= (point) (point-min))) (when (and (not stop) (/= (point) (point-min)))
(org-previous-visible-heading 1) (org-previous-visible-heading 1)
(anki-editor--visit-superior-headings visitor level)))) (anki-editor--visit-ancestor-headings visitor level))))
(provide 'anki-editor) (provide 'anki-editor)