org-zotxt.el 10.6 KB
Newer Older
Erik Hetzner's avatar
Erik Hetzner committed
1
;;; org-zotxt.el --- Interface org-mode with Zotero via the zotxt extension
Erik Hetzner's avatar
Erik Hetzner committed
2 3

;; Copyright (C) 2010-2016 Erik Hetzner
Erik Hetzner's avatar
Erik Hetzner committed
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27

;; Author: Erik Hetzner <egh@e6h.org>
;; Keywords: bib

;; This file is not part of GNU Emacs.

;; org-zotxt.el 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.

;; org-zotxt.el is distributed 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 org-zotxt.el. If not, see
;; <http://www.gnu.org/licenses/>.

;;; Commentary:

;;; Code:

28 29
(eval-when-compile
  (require 'cl))
30
(require 'request-deferred)
31
(require 'org-element)
Erik Hetzner's avatar
Erik Hetzner committed
32
(require 'zotxt)
Erik Hetzner's avatar
Erik Hetzner committed
33

34
(defcustom org-zotxt-link-description-style
35 36 37 38
  :citation
  "Style to use for org zotxt link texts."
  :group 'org-zotxt
  :type '(choice (const :tag "easykey" :easykey)
39
                 (const :tag "better BibTeX" :betterbibtexkey)
40 41
                 (const :tag "citation" :citation)))

42 43 44 45 46 47 48 49 50 51 52 53
(defcustom org-zotxt-default-search-method nil
  "Default method to use for searching with `org-zotxt-insert-reference-link'.
If nil, the user is prompted to choose each time.

A selected default method can be bypassed by giving a double
prefix argument (C-u C-u) to `org-zotxt-insert-reference-link'"
  :group 'org-zotxt
  :type (append '(choice) '((const :tag "Choose each time" nil))
                (mapcar
                 (lambda (c) (list 'const :tag (car c) (cdr c)))
                 zotxt-quicksearch-method-names)))

54 55 56 57
(defun org-zotxt-extract-link-id-at-point ()
  "Extract the Zotero key of the link at point."
  (let ((ct (org-element-context)))
    (if (eq 'link (org-element-type ct))
58
        (org-zotxt-extract-link-id-from-path (org-element-property :path ct))
59 60
      nil)))

61
(defun org-zotxt-extract-link-id-from-path (path)
62
  "Return the zotxt ID from a link PATH."
63 64
  (if (string-match "^\\(zotero:\\)?//select/items/\\(.*\\)$" path)
      (match-string 2 path)
65 66
    nil))

67
(defun org-zotxt-insert-reference-link-to-item (item)
68
  "Insert link to Zotero ITEM in buffer."
69 70
  (insert (org-make-link-string (format "zotero://select/items/%s"
                                        (plist-get item :key))
71 72 73
                                (if (or (eq org-zotxt-link-description-style :easykey)
                                        (eq org-zotxt-link-description-style :betterbibtexkey))
                                    (concat "@" (plist-get item org-zotxt-link-description-style))
74
                                  (plist-get item :citation)))))
75

76 77 78 79 80 81 82 83
(defun org-zotxt-insert-reference-links-to-items (items)
  "Insert links to Zotero ITEMS in buffer."
  (mapc (lambda (item)
          (org-zotxt-insert-reference-link-to-item item)
          (insert "\n")
          (forward-line 1))
        items))

Erik Hetzner's avatar
Erik Hetzner committed
84
(defun org-zotxt-update-reference-link-at-point ()
85
  "Update the zotero:// link at point."
Erik Hetzner's avatar
Erik Hetzner committed
86
  (interactive)
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
  (lexical-let ((mk (point-marker))
                (item-id (org-zotxt-extract-link-id-at-point)))
    (if item-id
        (deferred:$
          (deferred:next (lambda () `(:key ,item-id)))
          (deferred:nextc it
            (lambda (item)
              (org-zotxt-get-item-link-text-deferred item)))
          (deferred:nextc it
            (lambda (item)
              (save-excursion
                (with-current-buffer (marker-buffer mk)
                  (goto-char (marker-position mk))
                  (let ((ct (org-element-context)))
                    (goto-char (org-element-property :begin ct))
                    (delete-region (org-element-property :begin ct)
                                   (org-element-property :end ct))
104 105
                    (org-zotxt-insert-reference-link-to-item item))))))
          (if zotxt--debug-sync (deferred:sync! it))))))
Erik Hetzner's avatar
Erik Hetzner committed
106

Erik Hetzner's avatar
Erik Hetzner committed
107
(defun org-zotxt-update-all-reference-links ()
Erik Hetzner's avatar
Erik Hetzner committed
108
  "Update all zotero:// links in a document."
109 110
  (interactive)
  (save-excursion
111 112 113 114 115 116 117 118
    (widen)
    (goto-char (point-max))
    (while (re-search-backward org-any-link-re nil t)
      (let* ((parse (org-element-link-parser))
             (path (org-element-property :path parse)))
        (when (org-zotxt-extract-link-id-from-path path)
          (message "[zotxt] updating path: %s" path)
          (org-zotxt-update-reference-link-at-point))))))
119

120 121 122
(defun org-zotxt-get-item-link-text-deferred (item)
  "Get the link text for ITEM.
May be either an easy key or bibliography, depending on the value
123 124 125 126
of `org-zotxt-link-description-style'."
  (if (or (eq org-zotxt-link-description-style :easykey)
          (eq org-zotxt-link-description-style :betterbibtexkey))
      (zotxt-get-item-deferred item org-zotxt-link-description-style)
127 128
    (zotxt-get-item-bibliography-deferred item)))

Erik Hetzner's avatar
Erik Hetzner committed
129 130 131 132 133 134
(defun org-zotxt-insert-reference-link (&optional arg)
  "Insert a zotero link in the `org-mode' document.

Prompts for search to choose item.  If prefix argument ARG is used,
will insert the currently selected item from Zotero.  If double
prefix argument is used the search method will have to be
135
selected even if `org-zotxt-default-search-method' is non-nil"
136
  (interactive "P")
137
  (lexical-let ((mk (point-marker)))
138
    (deferred:$
Erik Hetzner's avatar
Erik Hetzner committed
139
      (if (equal '(4) arg)
140
          (zotxt-get-selected-items-deferred)
141
        (zotxt-choose-deferred (unless (equal '(16) arg) org-zotxt-default-search-method)))
142 143
      (deferred:nextc it
        (lambda (items)
144 145 146
          (if (null items)
              (error "No item found for search")
            (zotxt-mapcar-deferred #'org-zotxt-get-item-link-text-deferred items))))
147 148 149 150
      (deferred:nextc it
        (lambda (items)
          (with-current-buffer (marker-buffer mk)
            (goto-char (marker-position mk))
151 152 153
            (org-zotxt-insert-reference-links-to-items items))))
      (deferred:error it
        (lambda (err)
154 155
          (error (error-message-string err))))
      (if zotxt--debug-sync (deferred:sync! it)))))
156

157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
(defun org-zotxt--link-follow (path)
  "Function used for zotero links to follow the link to PATH."
  (zotxt-select-key (substring path 15)))

(defun org-zotxt--link-export (path desc format)
  "Function used for zotero links to export the link.

PATH is the path of the link, the text after the prefix (like \"http:\")
DESC is the description of the link, if any
FORMAT is the export format, a symbol like ‘html’ or ‘latex’ or ‘ascii’."
  (if (string-match "^@\\(.*\\)$" desc)
      (pcase format
        (`latex (format "\\cite{%s}"
                        ;; hack to replace all the escaping that latex
                        ;; gives us in the desc with _
                        (replace-regexp-in-string
                         "\\([{}]\\|\\\\text\\|\\\\(\\|\\\\)\\)" ""
                         (match-string 1 desc))))
        (`md desc)
        (_ nil))))

178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193
(defvar org-zotxt--links-defined nil)

;; We need to support org 9 and org 8, but this code will generate compiler
;; warnings without this
(with-no-warnings
  (defun org-zotxt--define-links ()
    "Set up the links for zotxt."
    (when (not org-zotxt--links-defined)
      (setq org-zotxt--links-defined t)
      (if (functionp #'org-link-set-parameters)
          (org-link-set-parameters "zotero"
                                   :follow #'org-zotxt--link-follow
                                   :export #'org-zotxt--link-export)
        (org-add-link-type "zotero"
                           #'org-zotxt--link-follow
                           #'org-zotxt--link-export)))))
Erik Hetzner's avatar
Erik Hetzner committed
194

Erik Hetzner's avatar
Erik Hetzner committed
195
(defvar org-zotxt-mode-map
Erik Hetzner's avatar
Erik Hetzner committed
196
  (let ((map (make-sparse-keymap)))
197
    (define-key map (kbd "C-c \" i") 'org-zotxt-insert-reference-link)
198
    (define-key map (kbd "C-c \" u") 'org-zotxt-update-reference-link-at-point)
199
    (define-key map (kbd "C-c \" a") 'org-zotxt-open-attachment)
Erik Hetzner's avatar
Erik Hetzner committed
200 201
    map))

202 203 204 205 206 207 208 209
(defun org-zotxt-choose-path (paths)
  "Prompt user to select a path from the PATHS.
If only path is available, return it.  If no paths are available, error."
  (if (= 0 (length paths))
      (progn (message "No attachments for item!")
             (error "No attachments for item!"))
    (if (= 1 (length paths))
        (elt paths 0)
210
      (completing-read "File: " (append paths nil)))))
211

Erik Hetzner's avatar
Erik Hetzner committed
212
(defun org-zotxt-open-attachment (&optional arg)
213
  "Open attachment of Zotero items linked at point.
Erik Hetzner's avatar
Erik Hetzner committed
214 215

Opens with `org-open-file', see for more information about ARG."
216
  (interactive "P")
217 218
  (lexical-let ((item-id (org-zotxt-extract-link-id-at-point))
                (arg arg))
219
    (deferred:$
220
      (request-deferred
221
       (format "%s/items" zotxt-url-base)
222
       :params `(("key" . ,item-id) ("format" . "paths"))
223
       :parser 'json-read)
224 225 226
      (deferred:nextc it
        (lambda (response)
          (let ((paths (cdr (assq 'paths (elt (request-response-data response) 0)))))
227 228
            (org-open-file (org-zotxt-choose-path paths) arg))))
      (if zotxt--debug-sync (deferred:sync! it)))))
229 230 231 232 233 234 235 236 237

(defun org-zotxt-noter (arg)
  "Like `org-noter', but use Zotero.

If no document path propery is found, will prompt for a Zotero
search to choose an attachment to annotate, then calls `org-noter'.

If a document path property is found, simply call `org-noter'."
  (interactive "P")
Erik Hetzner's avatar
Erik Hetzner committed
238 239 240
  (when (and (eq major-mode 'org-mode)
             (boundp 'org-noter-property-doc-file)
             (fboundp 'org-noter))
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257
    (when (org-before-first-heading-p)
      (error "`org-zotxt-noter' must be issued inside a heading"))
    (let* ((document-property (org-entry-get nil org-noter-property-doc-file (not (equal arg '(4)))))
           (document-path (when (stringp document-property) (expand-file-name document-property))))
      (if (and document-path (not (file-directory-p document-path)) (file-readable-p document-path))
          (org-noter arg)
        (lexical-let ((arg arg))
          (deferred:$
            (zotxt-choose-deferred)
            (deferred:nextc it
              (lambda (item-ids)
                (zotxt-get-item-deferred (car item-ids) :paths)))
            (deferred:nextc it
              (lambda (resp)
                (let ((path (org-zotxt-choose-path (cdr (assq 'paths (plist-get resp :paths))))))
                  (org-entry-put nil org-noter-property-doc-file path))
                (org-noter arg)))))))))
258

259
;;;###autoload
Erik Hetzner's avatar
Erik Hetzner committed
260 261
(define-minor-mode org-zotxt-mode
  "Toggle org-zotxt-mode.
Erik Hetzner's avatar
Erik Hetzner committed
262 263 264 265 266
With no argument, this command toggles the mode.
Non-null prefix argument turns on the mode.
Null prefix argument turns off the mode.

This is a minor mode for managing your citations with Zotero in a
Erik Hetzner's avatar
Erik Hetzner committed
267
org-mode document."
Erik Hetzner's avatar
Erik Hetzner committed
268
  nil
269
  " OrgZot"
270 271
  org-zotxt-mode-map
  (org-zotxt--define-links))
Erik Hetzner's avatar
Erik Hetzner committed
272

Erik Hetzner's avatar
Erik Hetzner committed
273
(provide 'org-zotxt)
Erik Hetzner's avatar
Erik Hetzner committed
274
;;; org-zotxt.el ends here