zotxt.el 11.9 KB
Newer Older
Erik Hetzner's avatar
Erik Hetzner committed
1
;;; zotxt.el --- Interface emacs with Zotero via the zotxt extension
2

Erik Hetzner's avatar
Erik Hetzner committed
3
;; Copyright (C) 2010-2016 Erik Hetzner
Erik Hetzner's avatar
Erik Hetzner committed
4 5 6

;; Author: Erik Hetzner <egh@e6h.org>
;; Keywords: bib
7
;; Version: 0.1.35
Erik Hetzner's avatar
Erik Hetzner committed
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27

;; This file is not part of GNU Emacs.

;; 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.

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

;;; Commentary:

;;; Code:

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

Erik Hetzner's avatar
Erik Hetzner committed
33
(defvar zotxt-default-bibliography-style
34
  "chicago-note-bibliography"
Erik Hetzner's avatar
Erik Hetzner committed
35 36
  "Default bibliography style to use.")

37
(defconst zotxt-url-base
Erik Hetzner's avatar
Erik Hetzner committed
38 39
  "http://127.0.0.1:23119/zotxt"
  "Base URL to contact.")
40

Erik Hetzner's avatar
Erik Hetzner committed
41
(defconst zotxt--json-formats
42
  '(:easykey :betterbibtexkey :json :paths :quickBib)
Erik Hetzner's avatar
Erik Hetzner committed
43
  "Formats to parse as JSON.")
44

45 46 47
(defvar zotxt--debug-sync nil
  "Use synchronous requests.  For debug only!")

48
(defun zotxt-mapcar-deferred (func lst)
Erik Hetzner's avatar
Erik Hetzner committed
49 50 51 52
  "Apply FUNC (which must return a deferred object), to each element of LST.

Will pass on a list of results.
Runs in parallel using `deferred:parallel'."
53 54
  (apply #'deferred:parallel
         (mapcar func lst)))
55

56 57 58 59 60 61
(defun zotxt--json-read ()
  "UTF-8 aware `json-read'.

request.el is not decoding our responses as UTF-8.  Recode text as UTF-8 and parse."
  (recode-region (point-min) (point-max) 'utf-8 'raw-text)
  (json-read))
62 63 64

(defun zotxt-make-quick-bib-string (item)
  "Make a useful quick bibliography string from ITEM."
Erik Hetzner's avatar
Erik Hetzner committed
65 66 67 68 69 70 71 72 73
  (let* ((json (plist-get item :json))
         (author (cdr (assq 'author json)))
         (title (cdr (assq 'title json)))
         (author-string (if (= (length author) 0)
                            ""
                          (let* ((first (elt author 0))
                                 (given (cdr (assq 'given first)))
                                 (family (cdr (assq 'family first))))
                            (format "%s, %s" family given)))))
74
    (format "%s - %s" author-string title)))
Erik Hetzner's avatar
Erik Hetzner committed
75

76 77 78 79 80
(defun zotxt--id2key (id)
  "Turn an ID, as returned by Zotero, into a key."
  (if (string-match "/\\([^/]+\\)$" id)
      (format "0_%s" (match-string 1 id))))

81
(defun zotxt-get-item-bibliography-deferred (item)
82 83 84 85
  "Retrieve the generated bibliography for ITEM (a plist).
Use STYLE to specify a custom bibliography style.
Adds a plist entry with the name of the style as a self-quoting symbol, e.g.
:chicago-note-bibliography.
86
Also adds :citation entry if STYLE is the default.
Erik Hetzner's avatar
Erik Hetzner committed
87 88 89
Also adds HTML versions, suffixed with -html.

For use only in a `deferred:$' chain."
90
  (lexical-let ((d (deferred:new))
91
                (style zotxt-default-bibliography-style)
92
                (item item))
93 94 95 96 97
    (if (and (string= style zotxt-default-bibliography-style)
             (plist-get item :citation))
        ;; item already has citation, no need to fetch
        (deferred:callback-post d item)
      (request
98
       (format "%s/items" zotxt-url-base)
99 100 101
       :params `(("key" . ,(plist-get item :key))
                 ("format" . "bibliography")
                 ("style" . ,style))
102
       :parser #'zotxt--json-read
103 104 105
       :success (function*
                 (lambda (&key data &allow-other-keys)
                   (let* ((style-key (intern (format ":%s" style)))
106
                          (style-key-html (intern (format ":%s-html" style)))
107
                          (first (elt data 0))
108
                          (text (cdr (assq 'text first)))
109
                          (html (cdr (assq 'html first))))
110
                     (if (string= style zotxt-default-bibliography-style)
111 112 113
                         (progn
                           (plist-put item :citation text)
                           (plist-put item :citation-html html)))
114
                     (plist-put item style-key text)
115
                     (plist-put item style-key-html html)
116
                     (deferred:callback-post d item))))))
117
    d))
Erik Hetzner's avatar
Erik Hetzner committed
118

119
(defun zotxt-get-selected-items-deferred ()
Erik Hetzner's avatar
Erik Hetzner committed
120 121 122
  "Return the currently selected items in Zotero.

For use only in a `deferred:$' chain."
123
  (lexical-let ((d (deferred:new)))
124
    (request
125
     (format "%s/items" zotxt-url-base)
126 127
     :params '(("selected" . "selected")
               ("format" . "key"))
128
     :parser #'zotxt--json-read
129 130 131 132 133 134 135
     :success (function*
               (lambda (&key data &allow-other-keys)
                     (deferred:callback-post
                       d (mapcar (lambda (k)
                                   (list :key k))
                                 data)))))
      d))
Erik Hetzner's avatar
Erik Hetzner committed
136

137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
(defconst zotxt-quicksearch-method-names
  '(("title, creator, year" . :title-creator-year)
    ("fields" . :fields)
    ("everything" . :everything)))

(defconst  zotxt-quicksearch-method-params
  '((:title-creator-year . "titleCreatorYear")
    (:fields . "fields")
    (:everything . "everything")))

(defconst zotxt-quicksearch-method-to-names
  '((:title-creator-year . "title, creator, year")
    (:fields . "fields")
    (:everything . "everything")))

(defun zotxt-choose-deferred (&optional method search-string)
Erik Hetzner's avatar
Erik Hetzner committed
153
  "Allow the user to select an item interactively.
154

Erik Hetzner's avatar
Erik Hetzner committed
155 156
If METHOD is supplied, it should be one
of :title-creator-year, :fields, or :everything.
157 158
If SEARCH-STRING is supplied, it should be the search string."
  (if (null method)
Erik Hetzner's avatar
Erik Hetzner committed
159
      (let ((method-name
160
             (completing-read
161 162 163 164
              "Zotero search method (nothing for title, creator, year): "
              zotxt-quicksearch-method-names
              nil t nil nil "title, creator, year")))
        (setq method (cdr (assoc method-name zotxt-quicksearch-method-names)))))
165 166
  (if (null search-string)
      (setq search-string
167
            (read-string (format "Zotero quicksearch (%s) query: " (cdr (assq method zotxt-quicksearch-method-to-names))))))
168 169
  (lexical-let ((d (deferred:new)))
    (request
170
     (format "%s/search" zotxt-url-base)
171
     :params `(("q" . ,search-string)
172
               ("method" . ,(cdr (assq method zotxt-quicksearch-method-params)))
173
               ("format" . "quickBib"))
174
     :parser #'zotxt--json-read
175 176
     :success (function*
               (lambda (&key data &allow-other-keys)
Erik Hetzner's avatar
Erik Hetzner committed
177
                 (let* ((results (mapcar (lambda (e)
178 179
                                           (cons (cdr (assq 'quickBib e))
                                                 (cdr (assq 'key e))))
180 181 182 183 184 185
                                         data))
                        (count (length results))
                        (citation (if (= 0 count)
                                      nil
                                    (if (= 1 count)
                                        (car (car results))
186
                                      (completing-read "Select item: " results))))
187 188
                        (key (cdr (assoc-string citation results))))
                   (deferred:callback-post
189
                     d (if (null citation) nil
190
                         `((:key ,key))))))))
191
    d))
Erik Hetzner's avatar
Erik Hetzner committed
192

193
(defun zotxt-select-easykey (easykey)
Erik Hetzner's avatar
Erik Hetzner committed
194
  "Select the item identified by EASYKEY in Zotero."
195
  (request
196
   (format "%s/select" zotxt-url-base)
197
   :params `(("easykey" . ,easykey))))
198 199

(defun zotxt-select-key (key)
Erik Hetzner's avatar
Erik Hetzner committed
200
  "Select the item identified by KEY in Zotero."
201
  (request
202
   (format "%s/select" zotxt-url-base)
203
   :params `(("key" . ,key))))
204

205 206 207 208 209 210 211 212 213 214
(defvar zotxt-easykey-regex
  "[@{]\\([[:alnum:]:]+\\)")

(defvar zotxt-easykey-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "C-c \" o") 'zotxt-easykey-select-item-at-point)
    (define-key map (kbd "C-c \" k") 'zotxt-easykey-insert)
    map))

(defun zotxt-easykey-at-point-match ()
Erik Hetzner's avatar
Erik Hetzner committed
215
  "Match an easykey at point."
216 217 218 219 220 221 222 223 224 225
  (or (looking-at zotxt-easykey-regex)
      (save-excursion
        ;; always try to back up one char
        (backward-char)
        (while (and (not (looking-at zotxt-easykey-regex))
                    (looking-at "[[:alnum:]:]"))
          (backward-char))
        (looking-at zotxt-easykey-regex))))

(defun zotxt-easykey-at-point ()
Erik Hetzner's avatar
Erik Hetzner committed
226 227 228 229
  "Return the value of the easykey at point.

Easykey must start with a @ or { to be recognized, but this will
not be returned."
230 231 232 233
  (save-excursion
    (if (zotxt-easykey-at-point-match)
        (match-string 1)
      nil)))
234

235
(defun zotxt-easykey-complete-at-point ()
Erik Hetzner's avatar
Erik Hetzner committed
236
  "Complete the easykey at point."
237 238 239
  (save-excursion
    (if (not (zotxt-easykey-at-point-match))
        nil
240 241 242 243 244 245
      (let* ((start (match-beginning 0))
             (end (match-end 0))
             (key (match-string 1))
             (completions
              (deferred:$
                (request-deferred
246
                 (format "%s/complete" zotxt-url-base)
247
                 :params `(("easykey" . ,key))
248
                 :parser #'zotxt--json-read)
249 250 251 252 253 254 255 256
                (deferred:nextc it
                  (lambda (response)
                    (mapcar (lambda (k) (format "@%s" k))
                            (request-response-data response))))
                (deferred:sync! it))))
        (if (null completions)
            nil
          (list start end completions))))))
257

258
(defun zotxt-get-item-deferred (item format)
Erik Hetzner's avatar
Erik Hetzner committed
259 260 261
  "Given a plist ITEM, add the FORMAT.

For use only in a `deferred:$' chain."
262
  (lexical-let ((item item)
263
                (format format)
264
                (d (deferred:new)))
265
    (request
266
     (format "%s/items" zotxt-url-base)
Erik Hetzner's avatar
Erik Hetzner committed
267
     :params `(("key" . ,(plist-get item :key))
268
               ("format" . ,(substring (symbol-name format) 1)))
Erik Hetzner's avatar
Erik Hetzner committed
269
     :parser (if (member format zotxt--json-formats)
270
                 #'zotxt--json-read
271
               #'buffer-string)
272 273
     :success (function*
               (lambda (&key data &allow-other-keys)
Erik Hetzner's avatar
Erik Hetzner committed
274
                 (if (member format zotxt--json-formats)
275 276 277
                     ;; json data
                     (plist-put item format (elt data 0))
                   (plist-put item format data))
278
                 (deferred:callback-post d item))))
279
    d))
280

Erik Hetzner's avatar
Erik Hetzner committed
281 282 283 284 285
(defun zotxt-easykey-insert (&optional selected)
  "Prompt for a search string and insert an easy key.

If SELECTED is non-nill (interactively, With prefix argument), insert easykeys for the currently selected items in Zotero."
  (interactive (if current-prefix-arg t))
286 287
  (lexical-let ((mk (point-marker)))
    (deferred:$
Erik Hetzner's avatar
Erik Hetzner committed
288
      (if selected
289 290
          (zotxt-get-selected-items-deferred)
        (zotxt-choose-deferred))
291 292
      (deferred:nextc it
        (lambda (items)
293
          (zotxt-mapcar-deferred (lambda (item)
Erik Hetzner's avatar
Erik Hetzner committed
294
                                   (zotxt-get-item-deferred item :easykey))
295
                                 items)))
296 297 298 299 300 301
      (deferred:nextc it
        (lambda (items)
          (with-current-buffer (marker-buffer mk)
            (goto-char (marker-position mk))
            (insert (mapconcat
                     (lambda (item)
302
                       (format "@%s" (plist-get item :easykey)))
303 304
                     items " ")))))
      (if zotxt--debug-sync (deferred:sync! it)))))
305 306

(defun zotxt-easykey-select-item-at-point ()
Erik Hetzner's avatar
Erik Hetzner committed
307
  "Select the item referred to by the easykey at point in Zotero."
308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327
  (interactive)
  (zotxt-select-easykey (zotxt-easykey-at-point)))

;;;###autoload
(define-minor-mode zotxt-easykey-mode
  "Toggle zotxt-easykey-mode.
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 easykey citations,
including completion."
  :init-value nil
  :lighter " ZotEasykey"
  :keymap zotxt-easykey-mode-map
  (if zotxt-easykey-mode
      (setq-local completion-at-point-functions
                  (cons 'zotxt-easykey-complete-at-point
                        completion-at-point-functions))
    (setq-local completion-at-point-functions
Erik Hetzner's avatar
Erik Hetzner committed
328
                (remove 'zotxt-easykey-complete-at-point
329 330
                        completion-at-point-functions))))

Erik Hetzner's avatar
Erik Hetzner committed
331
(provide 'zotxt)
Erik Hetzner's avatar
Erik Hetzner committed
332
;;; zotxt.el ends here